codetriage/codetriage

View on GitHub
app/models/mail_builder/grouped_issues_docs.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module MailBuilder
  # Takes in Issue Assignment IDs and Doc IDs and groups them together by repo
  #
  # The main purpose of this class is to make data easier to navigate for
  # rendering mail views, specifically the "daily triage" email.
  #
  # The main reason this exists is to minimize query time while rendering
  # mail views without sacraficing view readability.
  #
  # To access these elements you need to be inside of an `each` loop
  #
  #   group = GroupedIssuesDocs.new(User.last, assignment_ids: [99])
  #   group.each do |g|
  #     g.assignments # => [...]
  #     g.read_docs   # => [...]
  #     g.write_docs  # => [...]
  #     g.read_docs.each do |doc|
  #       g.comment_for_doc(doc) # => "..."
  #     end
  #   end
  #
  # There is a method `comment_for_doc` that will retrieve the documentation
  # comment object for a specific doc method.
  #
  # There are helper methods designed to make rendering views easier.
  # For example `GroupedIssuesDocs#actions` will return either "issues", "docs"
  # or "issues and docs" depending on the contents of the group.
  #
  # `GroupedIssuesDocs#any_docs?` returns true if there are docs in the group
  # `GroupedIssuesDocs#any_docs?` returns true if there are issues in the group
  #
  # Internally data is stored in a hash of hashes. The top level key is
  # the id of the repo subscription.
  #
  #   puts @sub_hashes
  #   {
  #     99 => {}
  #   }
  #
  # Each of these hashes has the following keys:
  #
  #   puts @sub_hashes.first.keys
  #   # => [:repo, :assignments, :read_docs, :write_docs]
  #
  # Each of these is an array except for repo.
  #
  # There is a helper hash @repo_id_to_sub that takes a repo id and
  # returns the subscription ID for that repo.
  #
  class GroupedIssuesDocs
    def initialize(user_id:, assignment_ids: [], read_doc_ids: [], write_doc_ids: [], random_seed: Random.new_seed)
      @active            = false
      @sub_hashes        = {}
      @repo_id_to_sub    = {}
      @doc_comments_hash = {}
      @error_hash        = Hash.new { raise "must call within each" }
      @active_hash       = @error_hash

      @any_docs   = read_doc_ids.present? || write_doc_ids.present?
      @any_issues = assignment_ids.present?

      ## Issue assignments
      assignments = IssueAssignment
                    .where(id: assignment_ids)
                    .includes(:issue)
                    .select(:id, :repo_subscription_id, :issue_id)

      ## Docs
      docs = DocMethod
             .where(id: write_doc_ids + read_doc_ids)
             .select(:id, :repo_id, :line, :file, :path)

      ## Comments
      doc_comments = DocComment
                     .where(doc_method_id: docs.map(&:id).uniq)
                     .select(:comment, :doc_method_id)

      ## Subscriptions
      doc_repo_ids = docs.map(&:repo_id).uniq

      subscriptions = RepoSubscription
                      .joins("LEFT OUTER JOIN issue_assignments ON issue_assignments.repo_subscription_id = repo_subscriptions.id")
                      .where("issue_assignments.id in (?) or repo_id in (?)", assignment_ids, doc_repo_ids)
                      .where(user_id: user_id)
                      .select(:id, :repo_id)
                      .includes(:repo)
                      .all
                      .shuffle(random: Random.new(random_seed))

      store_subscriptions!(subscriptions)
      store_assignments!(assignments)
      store_docs!(
        docs: docs,
        write_doc_ids: write_doc_ids,
        doc_comments: doc_comments
      )
    end

    def store_subscriptions!(subscriptions)
      subscriptions.each do |sub|
        @repo_id_to_sub[sub.repo.id] = sub.id

        @sub_hashes[sub.id] ||= {}
        @sub_hashes[sub.id][:repo] = sub.repo
        @sub_hashes[sub.id][:assignments] ||= []
        @sub_hashes[sub.id][:read_docs]   ||= []
        @sub_hashes[sub.id][:write_docs]  ||= []
        @sub_hashes
      end
    end

    def store_assignments!(assignments)
      assignments.each do |assignment|
        @sub_hashes[assignment.repo_subscription_id][:assignments] << assignment
      end
    end

    def store_docs!(write_doc_ids:, docs:, doc_comments:)
      write_docs = []
      read_docs  = []
      docs.each do |doc|
        if write_doc_ids.include?(doc.id)
          write_docs << doc
        else
          read_docs << doc
        end
      end

      doc_comments.each do |comment|
        @doc_comments_hash[comment.doc_method_id] = comment
      end

      write_docs.each do |doc|
        sub_id = @repo_id_to_sub.fetch(doc.repo_id)
        @sub_hashes[sub_id][:write_docs] << doc
      end

      read_docs.each do |doc|
        sub_id = @repo_id_to_sub.fetch(doc.repo_id)
        @sub_hashes[sub_id][:read_docs] << doc
      end
    end

    def each
      @sub_hashes.each do |_, hash|
        @active      = true
        @active_hash = hash
        yield self
        @active      = false
        @active_hash = @error_hash
      end
    end
    include Enumerable

    def repo
      @active_hash[:repo]
    end

    def assignments
      @active_hash[:assignments]
    end

    def read_docs
      @active_hash[:read_docs]
    end

    def write_docs
      @active_hash[:write_docs]
    end

    def any_docs
      return @any_docs if !@active
      return read_docs.any? || write_docs.any?
    end

    def any_issues
      return @any_issues if !@active
      return assignments.any?
    end

    alias :any_docs?   :any_docs
    alias :any_issues? :any_issues

    def actions(delimiter: "and")
      return "issues" if !self.any_docs
      return "docs"   if !self.any_issues
      "issues #{delimiter} docs"
    end

    def comment_for_doc(doc)
      @doc_comments_hash[doc.id]
    end

    def count
      @sub_hashes.length
    end
  end
end