summaryrefslogtreecommitdiff
path: root/lib/gitlab/issuable_metadata.rb
blob: e946fc00c4d4d37a9ff5f8b0c3f95e68544f6238 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# frozen_string_literal: true

module Gitlab
  class IssuableMetadata
    include Gitlab::Utils::StrongMemoize

    # data structure to store issuable meta data like
    # upvotes, downvotes, notes and closing merge requests counts for issues and merge requests
    # this avoiding n+1 queries when loading issuable collections on frontend
    IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do
      def merge_requests_count(user = nil)
        mrs_count
      end
    end

    attr_reader :current_user, :issuable_collection

    def initialize(current_user, issuable_collection)
      @current_user = current_user
      @issuable_collection = issuable_collection

      validate_collection!
    end

    def data
      return {} if issuable_ids.empty?

      issuable_ids.each_with_object({}) do |id, issuable_meta|
        issuable_meta[id] = metadata_for_issuable(id)
      end
    end

    private

    def metadata_for_issuable(id)
      downvotes = group_issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
      upvotes = group_issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
      notes = grouped_issuable_notes_count.find { |notes| notes.noteable_id == id }
      merge_requests = grouped_issuable_merge_requests_count.find { |mr| mr.first == id }

      IssuableMeta.new(
        upvotes.try(:count).to_i,
        downvotes.try(:count).to_i,
        notes.try(:count).to_i,
        merge_requests.try(:last).to_i
      )
    end

    def validate_collection!
      # ActiveRecord uses Object#extend for null relations.
      if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) &&
          issuable_collection.respond_to?(:limit_value) &&
          issuable_collection.limit_value.nil?

        raise 'Collection must have a limit applied for preloading meta-data'
      end
    end

    def issuable_ids
      strong_memoize(:issuable_ids) do
        # map has to be used here since using pluck or select will
        # throw an error when ordering issuables by priority which inserts
        # a new order into the collection.
        # We cannot use reorder to not mess up the paginated collection.
        issuable_collection.map(&:id)
      end
    end

    def collection_type
      # Supports relations or paginated arrays
      issuable_collection.try(:model)&.name ||
        issuable_collection.first&.model_name.to_s
    end

    def group_issuable_votes_count
      strong_memoize(:group_issuable_votes_count) do
        AwardEmoji.votes_for_collection(issuable_ids, collection_type)
      end
    end

    def grouped_issuable_notes_count
      strong_memoize(:grouped_issuable_notes_count) do
        ::Note.count_for_collection(issuable_ids, collection_type)
      end
    end

    def grouped_issuable_merge_requests_count
      strong_memoize(:grouped_issuable_merge_requests_count) do
        if collection_type == 'Issue'
          ::MergeRequestsClosingIssues.count_for_collection(issuable_ids, current_user)
        else
          []
        end
      end
    end
  end
end