def setModDateAuthor(element, author="", addUpdate=True):
    """ Update the modAuthor and modDate members of element """

    logger.debug("Updating ModifiedDate and ModifiedAuthor in"\
            " {}".format(element))
    # timestamp used as modification datetime
    modDate = utc.localize(datetime.datetime.now())

    oldDate = element.modDate
    element.modDate = modDate

    # set the modAuthor if `author` is set
    if author != "" and author is not None:
        oldAuthor = element.modAuthor
        element.modAuthor = author
    # if author is left empty, the previous modification author will be
    # overwritten
    elif author == "" or author is None:
        # print info if the author is not set
        logger.info("Author is not set.")
        element.modAuthor = element._modAuthor.defaultValue

    # add the author/date modification as update to the writers module
    if addUpdate:
        element._modDate.state = State.States.MODIFIED
        writer.addProjectUpdate(curProject, element._modDate, oldDate)

        if author != "" and author is not None:
            element._modAuthor.state = State.States.MODIFIED
            writer.addProjectUpdate(curProject, element._modAuthor, oldDate)
def addLabel(topic: Topic, label: str):
    """ Add `label` as new label to `topic`

    Before everything takes place, a backup of the current project is made. If
    an error occurs, the current project will be rolled back to the backup.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Adding new label({}) to topic {}".format(label, topic.title))

    if label == "":
        logger.info("Not adding an empty label.")
        return OperationResults.FAILURE

    if not isProjectOpen():
        return OperationResults.FAILURE

    # get a reference of the tainted, supplied topic reference in the working
    # copy of the project
    realTopic = _searchRealTopic(topic)
    if realTopic is None:
        return OperationResults.FAILURE

    # create and add a new label to curProject
    realTopic.labels.append(label)
    addedLabel = realTopic.labels[-1]  # get reference to added label

    writer.addProjectUpdate(curProject, addedLabel, None)
    return _handleProjectUpdate("Label '{}' could not be added. Returning"\
            " to last valid state...".format(label), projectBackup)
def addTopic(title: str,
             author: str,
             type: str = "",
             description="",
             status: str = "",
             priority: str = "",
             index: int = -1,
             labels: List[str] = list(),
             dueDate: datetime = None,
             assignee: str = "",
             stage: str = "",
             relatedTopics: List[UUID] = list(),
             referenceLinks: List[str] = list(),
             bimSnippet: BimSnippet = None):
    """ Adds a new topic to the project.

    That entails that a new topic folder is created in the bcf file as well as
    a new markup file created, with nothing set but the topic.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Adding new topic({}) to project({})".format(
        title, curProject.name))

    if not isProjectOpen():
        return OperationResults.FAILURE

    # new guid for topic
    guid = uuid4()

    # create and add new markup to curProject, bot nto write yet
    newMarkup = Markup(None,
                       state=State.States.ADDED,
                       containingElement=curProject)
    curProject.topicList.append(newMarkup)

    # create new topic and assign it to newMarkup
    creationDate = utc.localize(datetime.datetime.now())
    newTopic = Topic(guid,
                     title, creationDate, author, type, status, referenceLinks,
                     list(), priority, index, labels, None, "", dueDate,
                     assignee, description, stage, relatedTopics, bimSnippet,
                     newMarkup)
    # state does not have to be set. Topic will be automatically added when
    # adding the markup

    newMarkup.topic = newTopic
    writer.addProjectUpdate(curProject, newMarkup, None)

    return _handleProjectUpdate("Could not add topic {} to"\
            " project.".format(title), projectBackup)
def deleteObject(object):
    """ Deletes an arbitrary object from curProject.

    The heavy lifting is done by writer.processProjectUpdates() and
    project.deleteObject(). Former deletes the object from the file and latter
    one deletes the object from the data model.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Deleting object {} from project".format(object.__class__))

    if not issubclass(type(object), Identifiable):
        logger.error("Cannot delete {} since it doesn't inherit from"\
            " interfaces.Identifiable".format(object))
        return OperationResults.FAILURE

    if not issubclass(type(object), Hierarchy):
        logger.error("Cannot delete {} since it seems to be not part of" \
            " the data model. It has to inherit from"\
            " hierarchy.Hierarchy".format(object))
        return OperationResults.FAILURE

    if not isProjectOpen():
        return OperationResults.FAILURE

    realObject = curProject.searchObject(object)
    if realObject is None:
        # No rollback has to be done here, since the state of the project is not
        # changed anyways.
        logger.error("Object {} could not be found in project {}".format(
            object.__class__, curProject.__class__))
        return OperationResults.FAILURE

    realObject.state = State.States.DELETED
    writer.addProjectUpdate(curProject, realObject, None)
    result = _handleProjectUpdate("Object could not be deleted from "\
            "data model" , projectBackup)

    # `result == None` if the update could not be processed.
    if result == OperationResults.FAILURE:
        curProject = projectBackup
        errMsg = "Couldn't delete {} from the file.".format(object)
        logger.error(errMsg)
        return OperationResults.FAILURE

    # otherwise the updated project is returned
    else:
        curProject = curProject.deleteObject(realObject)
        return OperationResults.SUCCESS
def addComment(topic: Topic,
               text: str,
               author: str,
               viewpoint: Viewpoint = None):
    """ Add a new comment with content `text` to the topic.

    The date of creation is sampled right at the start of this function.
    Before everything takes place, a backup of the current project is made. If
    an error occurs, the current project will be rolled back to the backup.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Adding comment {} to topic {}".format(text, topic.title))

    if not isProjectOpen():
        return OperationResults.FAILURE

    realTopic = _searchRealTopic(topic)
    if realTopic is None:
        return OperationResults.FAILURE

    realMarkup = realTopic.containingObject

    creationDate = datetime.datetime.now()
    localisedDate = utc.localize(creationDate)
    guid = uuid4()  # generate new random id
    state = State.States.ADDED
    comment = Comment(guid,
                      localisedDate,
                      author,
                      text,
                      viewpoint,
                      containingElement=realMarkup,
                      state=state)
    realMarkup.comments.append(comment)

    writer.addProjectUpdate(curProject, comment, None)
    errorenousUpdate = writer.processProjectUpdates()
    if errorenousUpdate is not None:
        logger.error("Error while adding {}".format(errorenousUpdate[1]))
        logger.error("Project is reset to before the addition.")
        logger.info("Please fix comment {}".format(comment))
        curProject = projectBackup

        return OperationResults.FAILURE

    return OperationResults.SUCCESS
def addProject(name: str, extensionSchemaUri: ""):
    """ Adds a new project to the current working directory.

    This means essentially creating a new folder named `name` and placing one
    new file in it, namely `project.bcfp`."""

    global curProject

    logger.info("Adding new project with name {}".format(name))
    newProject = p.Project(uuid4(), name, extensionSchemaUri)
    newProject.state = State.States.ADDED

    writer.addProjectUpdate(newProject, newProject, None)
    result = _handleProjectUpdate("Project could not be created", None)
    if result == OperationResults.SUCCESS:
        curProject = copy.deepcopy(newProject)

    return result
def addViewpointToComment(comment: Comment, viewpoint: ViewpointReference,
                          author: str):
    """ Add a reference to `viewpoint` inside `comment`.

    If `comment` already referenced a viewpoint then it is updated. If no
    viewpoint was refrerenced before then a new xml node is created. In both
    cases `ModifiedAuthor` (`modAuthor`) and `ModifiedDate` (`modDate`) are
    updated/set.
    Before everything takes place, a backup of the current project is made. If
    an error occurs, the current project will be rolled back to the backup.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Adding new viewpoint reference to comment {}".format(comment))

    if author == "":
        logger.info("`author` is empty. Cannot update without an author.")
        return OperationResults.FAILURE

    if not isProjectOpen():
        return OperationResults.FAILURE

    realComment = curProject.searchObject(comment)
    realViewpoint = curProject.searchObject(viewpoint)
    if realComment == None:
        logger.error("No matching comment was found in the current project.")
        return OperationResults.FAILURE

    if realViewpoint == None:
        logger.error("No matching viewpoint was found in the current project.")
        return OperationResults.FAILURE

    modDate = utc.localize(datetime.datetime.now())

    realComment.state = State.States.DELETED
    writer.addProjectUpdate(curProject, realComment, None)

    realComment.viewpoint = viewpoint
    realComment.state = State.States.ADDED
    writer.addProjectUpdate(curProject, realComment, None)

    oldDate = realComment.modDate
    realComment.modDate = modDate
    realComment._modDate.state = State.States.MODIFIED
    writer.addProjectUpdate(curProject, realComment._modDate, oldDate)

    oldAuthor = realComment.modAuthor
    realComment.modAuthor = author
    realComment._modAuthor.state = State.States.MODIFIED
    writer.addProjectUpdate(curProject, realComment._modAuthor, oldAuthor)

    return _handleProjectUpdate("Could not assign viewpoint.", projectBackup)
def modifyComment(comment: Comment, newText: str, author: str):
    """ Change the text of `comment` to `newText` in the data model.

    Alongside with the text, the modAuthor and modDate fields get overwritten
    with `author` and the current datetime respectively.
    If `newText` was left empty then the comment is going to be deleted.
    Before everything takes place, a backup of the current project is made. If
    an error occurs, the current project will be rolled back to the backup.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Modifying comment({})".format(comment))

    if newText == "":
        logger.info("newText is empty. Deleting comment now.")
        deleteObject(comment)
        return OperationResults.SUCCESS

    if not isProjectOpen():
        return OperationResults.FAILURE

    realComment = curProject.searchObject(comment)
    if realComment is None:
        logger.error("Comment {} could not be found in the data model. Not"\
                "modifying anything".format(comment))
        return OperationResulsts.FAILURE

    oldVal = realComment.comment
    realComment.comment = newText
    realComment._comment.state = State.States.MODIFIED
    writer.addProjectUpdate(curProject, realComment._comment, oldVal)

    # update `modDate` and `modAuthor`
    setModDateAuthor(realComment, author)

    return _handleProjectUpdate("Could not modify comment.", projectBackup)
def addFile(topic: Topic,
            ifcProject: str = "",
            ifcSpatialStructureElement: str = "",
            isExternal: bool = False,
            filename: str = "",
            reference: str = ""):
    """ Add a new IFC file to the project.

    This function assumes that the file already exists and only creates a
    reference to it inside the data model. It does not copy an external file
    into the project.
    Before everything takes place, a backup of the current project is made. If
    an error occurs, the current project will be rolled back to the backup.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Adding new file({}) to topic({})".format(
        filename, topic.title))

    if not isExternal:
        if not util.doesFileExistInProject(reference):
            logger.error("{} does not exist inside the project. Please check"\
                    " the path. Or for copiing a new file to the project use: "\
                    " plugin.copyFile(topic, fileAbsPath)".format(reference))
            return OperationResults.FAILURE
    elif not os.path.exists(reference):
        logger.error("{} could not be found. Please check the path for"\
                " typos".format(reference))
        return OperationResults.FAILURE

    if not _isIfcGuid(ifcProject) or ifcProject == "":
        logger.error("{} is not a valid IfcGuid. An Ifc guid has to be of"\
                " length 22 and contain alphanumeric characters including '_'"\
                " and '$'".format(ifcProject))

    if (not _isIfcGuid(ifcSpatialStructureElement)
            or ifcSpatialStructureElement == ""):
        logger.error("{} is not a valid IfcGuid. An Ifc guid has to be of"\
                " length 22 and contain alphanumeric characters including '_'"\
                " and '$'".format(ifcProject))

    if not isProjectOpen():
        return OperationResults.FAILURE

    realTopic = _searchRealTopic(topic)
    if realTopic is None:
        return OperationResults.FAILURE

    realMarkup = realTopic.containingObject

    # create new header file and insert it into the data model
    creationDate = datetime.datetime.now()
    localisedDate = utc.localize(creationDate)
    newFile = HeaderFile(ifcProject,
                         ifcSpatialStructureElement,
                         isExternal,
                         filename,
                         localisedDate,
                         reference,
                         state=State.States.ADDED)
    # create markup.header if needed
    if realMarkup.header is None:
        realMarkup.header = Header([newFile])
        realMarkup.header.state = State.States.ADDED
        realMarkup.header.containingObject = realMarkup
        writer.addProjectUpdate(curProject, realMarkup.header, None)
    else:
        realMarkup.header.files.append(newFile)
    newFile.containingObject = realMarkup.header

    writer.addProjectUpdate(curProject, newFile, None)
    return _handleProjectUpdate("File could not be added. Project is reset to"\
            " last valid state", projectBackup)
def addCurrentViewpoint(topic: Topic):
    """ Reads the current view settings and adds them as viewpoint to `topic`

    The view settings include:
        - selection state of each ifc object
        - color of each ifc object
        - camera position and orientation
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Adding current view settings as viewpoint to topic"\
            " {}".format(topic.title))

    if not (GUI and FREECAD):
        logger.error("Application is running either not inside FreeCAD or without"\
                " GUI. Thus cannot set camera position")
        return OperationResults.FAILURE

    doNotAdd = False
    if not isProjectOpen():
        logger.info("Project is not open. Viewpoint cannot be added to any"\
                " topic")
        doNotAdd = True

    realTopic = _searchRealTopic(topic)
    if realTopic is None:
        logger.info("Viewpoint will not be added.")
        doNotAdd = True

    camSettings = None
    try:
        camSettings = vCtrl.readCamera()
    except AttributeError as err:
        logger.error("Camera settings could not be read. Make sure the 3D"\
                " view is active.")
        logger.error(str(err))
        return OperationResults.FAILURE
    else:
        if camSettings is None:
            return OperationResults.FAILURE

    if not doNotAdd:
        realMarkup = realTopic.containingObject
        vpGuid = uuid4()
        oCamera = None
        pCamera = None
        if isinstance(camSettings, OrthogonalCamera):
            oCamera = camSettings
        elif isinstance(camSettings, PerspectiveCamera):
            pCamera = camSettings

        logger.info(str(camSettings))
        vp = Viewpoint(vpGuid, None, oCamera, pCamera)
        vp.state = State.States.ADDED
        vpFileName = writer.generateViewpointFileName(realMarkup)
        vpRef = ViewpointReference(vpGuid, Uri(vpFileName), None, -1,
                                   realMarkup, State.States.ADDED)
        vpRef.viewpoint = vp
        realMarkup.viewpoints.append(vpRef)

        writer.addProjectUpdate(curProject, vpRef, None)
        return _handleProjectUpdate("Viewpoint could not be added. Rolling"\
                " back to previous state", projectBackup)

    print(camSettings)
    return OperationResults.SUCCESS
def modifyElement(element, author=""):
    """ Replace the old element in the data model with element.

    A reference to the old element in the data model is acquired via the `id`
    member of `element`. This old element is updated with the new values. The
    corresponding XML element is first deleted from file and then added again
    with the new values.

    If `element` is of type Topic or Comment then `author` must be set, and then
    `modAuthor` and `modDate` are updated.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Modifying element {} in the"\
            " project".format(element.__class__))

    # ---- Checks ---- #
    if not isProjectOpen():
        return OperationResults.FAILURE

    # is element of the right type
    if not (issubclass(type(element), Identifiable) and issubclass(
            type(element), State) and issubclass(type(element), XMLName)):
        logger.error("Element is not an object from the data model. Cannot"\
                " update it")
        return OperationResults.FAILURE

    # ---- Operation ---- #
    # get a reference to the real element in the data model
    realElement = curProject.searchObject(element)
    if realElement is None:
        logger.error("{} object, that shall be changed, could not be"\
                " found in the current project.".format(element.xmlName))
        return OperationResults.FAILURE

    # get the associated topic
    realTopic = getTopic(realElement)
    if realTopic is None:
        logger.error("{} currently it is only possible to modify values of"\
                " markup.bcf.")
        return OperationResults.FAILURE

    realElement.state = State.States.DELETED
    writer.addProjectUpdate(curProject, realElement, None)

    # copy the state of the given element to the real element
    for property, value in vars(element).items():
        if property == "containingObject":
            continue
        setattr(realElement, property, copy.deepcopy(value))

    # if topic/comment was modified update `modDate` and `modAuthor`
    if isinstance(realElement, Topic) or isinstance(realElement, Comment):
        setModDateAuthor(realElement, author, False)

    realElement.state = State.States.ADDED
    writer.addProjectUpdate(curProject, realElement, None)
    realElement.state = State.States.ORIGINAL
    return _handleProjectUpdate(
        "Could not modify element {}".format(element.xmlName), projectBackup)
def addDocumentReference(topic: Topic,
                         guid: str = "",
                         isExternal: bool = False,
                         path: str = "",
                         description: str = ""):
    """ Creates a new document reference and adds it to `topic`.

    guid is the guid of the documentreference. If left alone a new random guid
    is generated using uuid.uuid4().
    isExternal == True => `path` is expected to be an absolute url,
    isExternal == False => `path` is expected to be a relative url pointing to
    a file in the project directory.
    `path` to the file, and `description` is a human readable name of the
    document.
    Before everything takes place, a backup of the current project is made. If
    an error occurs, the current project will be rolled back to the backup.
    """

    global curProject
    projectBackup = copy.deepcopy(curProject)
    logger.info("Adding new document reference({}) to topic"\
            " {}".format(description, topic.title))

    if (path == "" and description == ""):
        logger.info("Not adding an empty document reference")
        return OperationResults.FAILURE

    if not isExternal:
        if not util.doesFileExistInProject(topic, path):
            logger.error("{} does not exist inside the project. Please check"\
                    " the path. Or for copiing a new file to the project use: "\
                    " plugin.copyFile(topic, fileAbsPath)".format(path))
            return OperationResults.FAILURE
    elif not os.path.exists(path):
        logger.info("{} could not be found on the file system. Assuming"\
                " that it resides somewhere on a network.".format(path))

    # check if `guid` is a valid UUID and create a UUID object
    guidU = UUID(int=0)
    if isinstance(guid, UUID):
        guidU = guid
    elif guid == "":
        # just generate a new guid
        guidU = uuid4()
    else:
        try:
            guidU = UUID(guid)
        except ValueError as err:
            logger.error("The supplied guid is malformed ({}).".format(guid))
            return OperationResults.FAILURE

    if not isProjectOpen():
        return OperationResults.FAILURE

    # get a reference of the tainted, supplied topic reference in the working
    # copy of the project
    realTopic = _searchRealTopic(topic)
    if realTopic is None:
        return OperationResults.FAILURE

    docRef = DocumentReference(guidU, isExternal, path, description, realTopic,
                               State.States.ADDED)
    realTopic.docRefs.append(docRef)

    writer.addProjectUpdate(curProject, docRef, None)
    return _handleProjectUpdate("Document reference could not be added."\
            " Returning to last valid state...", projectBackup)