summaryrefslogtreecommitdiff
path: root/app/models/concerns/participable.rb
blob: b140fca9b836be83090a92ea20f50744ca94f2da (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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# frozen_string_literal: true

# == Participable concern
#
# Contains functionality related to objects that can have participants, such as
# an author, an assignee and people mentioned in its description or comments.
#
# Usage:
#
#     class Issue < ApplicationRecord
#       include Participable
#
#       # ...
#
#       participant :author
#       participant :assignee
#       participant :notes
#
#       participant -> (current_user, ext) do
#         ext.analyze('...')
#       end
#     end
#
#     issue = Issue.last
#     users = issue.participants
module Participable
  extend ActiveSupport::Concern

  class_methods do
    # Adds a list of participant attributes. Attributes can either be symbols or
    # Procs.
    #
    # When using a Proc instead of a Symbol the Proc will be given two
    # arguments:
    #
    # 1. The current user (as an instance of User)
    # 2. An instance of `Gitlab::ReferenceExtractor`
    #
    # It is expected that a Proc populates the given reference extractor
    # instance with data. The return value of the Proc is ignored.
    #
    # attr - The name of the attribute or a Proc
    def participant(attr)
      participant_attrs << attr
    end
  end

  included do
    # Accessor for participant attributes.
    cattr_accessor :participant_attrs, instance_accessor: false do
      []
    end
  end

  # Returns the users participating in a discussion.
  #
  # This method processes attributes of objects in breadth-first order.
  #
  # Returns an Array of User instances.
  def participants(current_user = nil)
    all_participants[current_user]
  end

  private

  def all_participants
    @all_participants ||= Hash.new do |hash, user|
      hash[user] = raw_participants(user)
    end
  end

  def raw_participants(current_user = nil)
    current_user ||= author
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
    participants = Set.new
    process = [self]

    until process.empty?
      source = process.pop

      case source
      when User
        participants << source
      when Participable
        source.class.participant_attrs.each do |attr|
          if attr.respond_to?(:call)
            source.instance_exec(current_user, ext, &attr)
          else
            process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend
          end
        end
      when Enumerable, ActiveRecord::Relation
        # This uses reverse_each so we can use "pop" to get the next value to
        # process (in order). Using unshift instead of pop would require
        # moving all Array values one index to the left (which can be
        # expensive).
        source.reverse_each { |obj| process << obj }
      end
    end

    participants.merge(ext.users)

    filter_by_ability(participants)
  end

  def filter_by_ability(participants)
    case self
    when PersonalSnippet
      Ability.users_that_can_read_personal_snippet(participants.to_a, self)
    else
      Ability.users_that_can_read_project(participants.to_a, project)
    end
  end
end