opf/openproject

View on GitHub
modules/bim/lib/open_project/bim/bcf_xml/issue_writer.rb

Summary

Maintainability
A
0 mins
Test Coverage
##
# Creates or updates a BCF issue and markup from a work package
module OpenProject::Bim::BcfXml
  class IssueWriter < BaseWriter
    attr_reader :work_package, :issue, :markup_doc, :markup_node

    TOPIC_SEQUENCE = [
      "ReferenceLink",
      "Title",
      "Priority",
      "Index",
      "Labels",
      "CreationDate",
      "CreationAuthor",
      "ModifiedDate",
      "ModifiedAuthor",
      "DueDate",
      "AssignedTo",
      "Stage",
      "Description",
      "BimSnippet",
      "DocumentReference",
      "RelatedTopic"
    ].freeze

    def self.update_from!(work_package)
      writer = new(work_package)
      writer.update

      writer.issue
    end

    def initialize(work_package)
      @work_package = work_package
      @issue = find_or_initialize_issue

      # Create markup document
      super()

      # Remember root markup node for easier access
      @markup_node = markup_doc.at_xpath("/Markup")
    end

    def update
      # update or create topic node
      topic

      # Override all current comments
      replace_comments

      # Override all current Viewpoints
      replace_viewpoints

      # Replace the markup XML
      issue.markup = markup_doc.to_xml(indent: 2)

      # Save issue and potential new associations
      issue.save!
    end

    protected

    def root_node
      :Markup
    end

    ##
    # Get the nokogiri document from the markup xml
    def build_markup_document
      if issue.markup
        Nokogiri::XML issue.markup, &:noblanks
      else
        super
      end
    end

    ##
    # Update the topic node, or create it
    def topic
      topic_node = fetch(markup_node, "Topic")

      topic_attributes      topic_node

      topic_reference_link  topic_node
      topic_title           topic_node
      topic_priority        topic_node
      topic_creation_date   topic_node
      topic_creation_author topic_node
      topic_modified_date   topic_node
      topic_modified_author topic_node
      topic_due_date        topic_node
      topic_assigned_to     topic_node
      topic_description     topic_node

      enforce_child_order(topic_node, TOPIC_SEQUENCE)
    end

    def enforce_child_order(parent_node, sequence)
      children_by_name = parent_node
        .children
        .select(&:element?)
        .group_by(&:name)

      sequence.reverse_each do |name|
        if children_with_name = children_by_name[name]
          children_with_name.each do |child|
            parent_node.delete child
            prepend_into_or_insert(parent_node, child)
          end
        end
      end
    end

    def prepend_into_or_insert(parent_node, node)
      if first_child = parent_node.children.select(&:element?)&.first
        first_child.previous = node
      else
        node.parent = parent_node
      end
    end

    def topic_attributes(topic_node)
      topic_node["Guid"] = issue.uuid
      topic_node["TopicType"] = work_package.type.name # TODO: Looks wrong to me. Probably better to use original TopicType?
      topic_node["TopicStatus"] = work_package.status.name
    end

    def topic_title(topic_node)
      target = fetch(topic_node, "Title")
      target.content = work_package.subject
    end

    def topic_creation_date(topic_node)
      target = fetch(topic_node, "CreationDate")
      target.content = to_bcf_datetime(work_package.created_at)
    end

    def topic_modified_date(topic_node)
      target = fetch(topic_node, "ModifiedDate")
      target.content = to_bcf_datetime(work_package.updated_at)
    end

    def topic_description(topic_node)
      target = fetch(topic_node, "Description")
      target.content = work_package.description
    end

    def topic_creation_author(topic_node)
      target = fetch(topic_node, "CreationAuthor")
      target.content = work_package.author.mail
    end

    def topic_reference_link(topic_node)
      target = fetch(topic_node, "ReferenceLink")
      target.content = url_helpers.work_package_url(work_package)
    end

    def topic_priority(topic_node)
      if priority = work_package.priority
        target = fetch(topic_node, "Priority")
        target.content = priority.name
      end
    end

    def topic_assigned_to(topic_node)
      if assignee = work_package.assigned_to
        target = fetch(topic_node, "AssignedTo")
        target.content = assignee.mail
      end
    end

    def topic_modified_author(topic_node)
      if journal = work_package.journals.select(:user_id).last
        target = fetch(topic_node, "ModifiedAuthor")
        target.content = journal.user.mail if journal.user_id
      end
    end

    def topic_due_date(topic_node)
      if work_package.due_date
        target = fetch(topic_node, "DueDate")
        target.content = to_bcf_date(work_package.due_date.to_datetime)
      end
    end

    def replace_comments
      markup_node.xpath("./Comment").remove

      Nokogiri::XML::Builder.with(markup_node, &method(:comments))
    end

    def replace_viewpoints
      markup_node.xpath("./Viewpoints").remove

      Nokogiri::XML::Builder.with(markup_node, &method(:viewpoints))
    end

    ##
    # Render the comments of the work package as XML nodes
    def comments(xml)
      work_package.journals.select(:id, :notes, :user_id, :created_at).map do |journal|
        next if journal.notes.empty?

        # Create BCF comment reference for the journal
        comment = journal.bcf_comment || issue.comments.create(issue_id: issue, journal_id: journal.id)
        comment_node xml, comment.uuid, journal
      end
    end

    ##
    # Create a single comment node
    def comment_node(xml, uuid, journal)
      xml.Comment "Guid" => uuid do
        xml.Date to_bcf_datetime(journal.created_at)
        xml.Author(journal.user.mail) if journal.user_id && journal&.user&.mail.present?
        xml.Comment journal.notes
      end
    end

    ##
    # Write the current set of viewpoints
    def viewpoints(xml)
      issue.viewpoints.find_each do |vp|
        xml.Viewpoints "Guid" => vp.uuid do
          xml.Viewpoint "#{vp.uuid}.bcfv"
          xml.Snapshot "#{vp.uuid}#{vp.snapshot.extension}" if vp.snapshot
        end
      end
    end

    ##
    # Find existing issue or create new
    def find_or_initialize_issue
      ::Bim::Bcf::Issue.find_or_initialize_by(work_package:)
    end
  end
end