Module: Mentionable

Extended by:
ActiveSupport::Concern
Included in:
Commit, DesignManagement::Design, Issuable, Note, Review, Snippet
Defined in:
app/models/concerns/mentionable.rb,
app/models/concerns/mentionable/reference_regexes.rb

Overview

Mentionable concern

Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by GFM references.

Used by Issue, Note, MergeRequest, and Commit.

Defined Under Namespace

Modules: ReferenceRegexes

Instance Method Summary collapse

Instance Method Details

#all_references(current_user = nil, extractor: nil) ⇒ Object


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
# File 'app/models/concerns/mentionable.rb', line 48

def all_references(current_user = nil, extractor: nil)
  # Use custom extractor if it's passed in the function parameters.
  if extractor
    extractors[current_user] = extractor
  else
    extractor = extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)

    extractor.reset_memoized_values
  end

  self.class.mentionable_attrs.each do |attr, options|
    text    = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend
    options = options.merge(
      cache_key: [self, attr],
      author: author,
      skip_project_check: skip_project_check?
    ).merge(mentionable_params)

    cached_html = self.try(:updated_cached_html_for, attr.to_sym)
    options[:rendered] = cached_html if cached_html

    extractor.analyze(text, options)
  end

  extractor
end

#create_cross_references!(author = self.author, without = []) ⇒ Object

Create a cross-reference Note for each GFM reference to another Mentionable found in the mentionable_attrs.


179
180
181
182
183
184
185
186
187
188
189
190
# File 'app/models/concerns/mentionable.rb', line 179

def create_cross_references!(author = self.author, without = [])
  refs = referenced_mentionables(author)

  # We're using this method instead of Array diffing because that requires
  # both of the object's `hash` values to be the same, which may not be the
  # case for otherwise identical Commit objects.
  refs.reject! { |ref| without.include?(ref) || cross_reference_exists?(ref) }

  refs.each do |ref|
    SystemNoteService.cross_reference(ref, local_reference, author)
  end
end

#create_new_cross_references!(author = self.author) ⇒ Object

When a mentionable field is changed, creates cross-reference notes that don't already exist


194
195
196
197
198
199
200
# File 'app/models/concerns/mentionable.rb', line 194

def create_new_cross_references!(author = self.author)
  changes = detect_mentionable_changes

  return if changes.empty?

  create_cross_references!(author)
end

#directly_addressed_users(current_user = nil) ⇒ Object


148
149
150
# File 'app/models/concerns/mentionable.rb', line 148

def directly_addressed_users(current_user = nil)
  all_references(current_user).directly_addressed_users
end

#extractorsObject


75
76
77
# File 'app/models/concerns/mentionable.rb', line 75

def extractors
  @extractors ||= {}
end

#gfm_reference(from = nil) ⇒ Object

Returns the text used as the body of a Note when this object is referenced

By default this will be the class name and the result of calling `to_reference` on the object.


36
37
38
39
40
41
# File 'app/models/concerns/mentionable.rb', line 36

def gfm_reference(from = nil)
  # "MergeRequest" > "merge_request" > "Merge request" > "merge request"
  friendly_name = self.class.to_s.underscore.humanize.downcase

  "#{friendly_name} #{to_reference(from)}"
end

#local_referenceObject

The GFM reference to this Mentionable, which shouldn't be included in its #references.


44
45
46
# File 'app/models/concerns/mentionable.rb', line 44

def local_reference
  self
end

#matches_cross_reference_regex?Boolean

Uses regex to quickly determine if mentionables might be referenced Allows heavy processing to be skipped

Returns:

  • (Boolean)

166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/concerns/mentionable.rb', line 166

def matches_cross_reference_regex?
  reference_pattern = if !project || project.default_issues_tracker?
                        ReferenceRegexes.default_pattern
                      else
                        ReferenceRegexes.external_pattern
                      end

  self.class.mentionable_attrs.any? do |attr, _|
    __send__(attr) =~ reference_pattern # rubocop:disable GitlabSecurity/PublicSend
  end
end

#mentioned_users(current_user = nil) ⇒ Object


79
80
81
# File 'app/models/concerns/mentionable.rb', line 79

def mentioned_users(current_user = nil)
  all_references(current_user).users
end

#referenced_group_users(current_user = nil) ⇒ Object


144
145
146
# File 'app/models/concerns/mentionable.rb', line 144

def referenced_group_users(current_user = nil)
  User.joins(:group_members).where(members: { source_id: referenced_groups }).distinct
end

#referenced_groups(current_user = nil) ⇒ Object


133
134
135
136
137
138
139
140
141
142
# File 'app/models/concerns/mentionable.rb', line 133

def referenced_groups(current_user = nil)
  # TODO: IMPORTANT: Revisit before using it.
  # Check DB data for max mentioned groups per mentionable:
  #
  # select issue_id, count(mentions_count.men_gr_id) gr_count from
  # (select DISTINCT unnest(mentioned_groups_ids) as men_gr_id, issue_id
  # from issue_user_mentions group by issue_id, mentioned_groups_ids) as mentions_count
  # group by mentions_count.issue_id order by gr_count desc limit 10
  Group.where(id: user_mentions.select("unnest(mentioned_groups_ids)")).public_or_visible_to_user(current_user)
end

#referenced_mentionables(current_user = self.author) ⇒ Object

Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.


153
154
155
156
157
158
159
160
161
162
# File 'app/models/concerns/mentionable.rb', line 153

def referenced_mentionables(current_user = self.author)
  return [] unless matches_cross_reference_regex?

  refs = all_references(current_user)

  # We're using this method instead of Array diffing because that requires
  # both of the object's `hash` values to be the same, which may not be the
  # case for otherwise identical Commit objects.
  extracted_mentionables(refs).reject { |ref| ref == local_reference }
end

#referenced_project_users(current_user = nil) ⇒ Object


129
130
131
# File 'app/models/concerns/mentionable.rb', line 129

def referenced_project_users(current_user = nil)
  User.joins(:project_members).where(members: { source_id: referenced_projects(current_user) }).distinct
end

#referenced_projects(current_user = nil) ⇒ Object


125
126
127
# File 'app/models/concerns/mentionable.rb', line 125

def referenced_projects(current_user = nil)
  Project.where(id: user_mentions.select("unnest(mentioned_projects_ids)")).public_or_visible_to_user(current_user)
end

#referenced_usersObject


121
122
123
# File 'app/models/concerns/mentionable.rb', line 121

def referenced_users
  User.where(id: user_mentions.select("unnest(mentioned_users_ids)"))
end

#store_mentions!Object


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
115
116
117
118
119
# File 'app/models/concerns/mentionable.rb', line 83

def store_mentions!
  # if store_mentioned_users_to_db feature flag is not enabled then consider storing operation as succeeded
  # because we wrap this method in transaction with with_transaction_returning_status, and we need the status to be
  # successful if mentionable.save is successful.
  #
  # This line will get removed when we remove the feature flag.
  return true unless store_mentioned_users_to_db_enabled?

  refs = all_references(self.author)

  references = {}
  references[:mentioned_users_ids] = refs.mentioned_users&.pluck(:id).presence
  references[:mentioned_groups_ids] = refs.mentioned_groups&.pluck(:id).presence
  references[:mentioned_projects_ids] = refs.mentioned_projects&.pluck(:id).presence

  # One retry should be enough as next time `model_user_mention` should return the existing mention record, that
  # threw the `ActiveRecord::RecordNotUnique` exception in first place.
  self.class.safe_ensure_unique(retries: 1) do
    user_mention = model_user_mention

    # this may happen due to notes polymorphism, so noteable_id may point to a record that no longer exists
    # as we cannot have FK on noteable_id
    break if user_mention.blank?

    user_mention.mentioned_users_ids = references[:mentioned_users_ids]
    user_mention.mentioned_groups_ids = references[:mentioned_groups_ids]
    user_mention.mentioned_projects_ids = references[:mentioned_projects_ids]

    if user_mention.has_mentions?
      user_mention.save!
    else
      user_mention.destroy!
    end
  end

  true
end