debian-mirror-gitlab/app/models/concerns/mentionable.rb

163 lines
5 KiB
Ruby
Raw Normal View History

2014-09-02 18:07:02 +05:30
# == Mentionable concern
#
2017-08-17 22:00:37 +05:30
# Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by
2014-09-02 18:07:02 +05:30
# GFM references.
#
# Used by Issue, Note, MergeRequest, and Commit.
#
module Mentionable
extend ActiveSupport::Concern
module ClassMethods
# Indicate which attributes of the Mentionable to search for GFM references.
2015-12-23 02:04:40 +05:30
def attr_mentionable(attr, options = {})
attr = attr.to_s
mentionable_attrs << [attr, options]
2014-09-02 18:07:02 +05:30
end
2016-08-24 12:49:21 +05:30
end
2014-09-02 18:07:02 +05:30
2016-08-24 12:49:21 +05:30
included do
2014-09-02 18:07:02 +05:30
# Accessor for attributes marked mentionable.
2016-08-24 12:49:21 +05:30
cattr_accessor :mentionable_attrs, instance_accessor: false do
[]
2014-09-02 18:07:02 +05:30
end
2015-10-24 18:46:33 +05:30
if self < Participable
participant -> (user, ext) { all_references(user, extractor: ext) }
2015-10-24 18:46:33 +05:30
end
end
2015-09-11 14:41:01 +05:30
# 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.
2018-03-17 18:26:18 +05:30
def gfm_reference(from = nil)
2015-09-11 14:41:01 +05:30
# "MergeRequest" > "merge_request" > "Merge request" > "merge request"
friendly_name = self.class.to_s.underscore.humanize.downcase
2018-03-17 18:26:18 +05:30
"#{friendly_name} #{to_reference(from)}"
2014-09-02 18:07:02 +05:30
end
# The GFM reference to this Mentionable, which shouldn't be included in its #references.
def local_reference
self
end
2016-11-03 12:29:30 +05:30
def all_references(current_user = nil, extractor: nil)
2017-08-17 22:00:37 +05:30
# Use custom extractor if it's passed in the function parameters.
if extractor
2018-03-17 18:26:18 +05:30
extractors[current_user] = extractor
2017-08-17 22:00:37 +05:30
else
2018-03-17 18:26:18 +05:30
extractor = extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
2017-08-17 22:00:37 +05:30
extractor.reset_memoized_values
end
2015-12-23 02:04:40 +05:30
2016-11-03 12:29:30 +05:30
self.class.mentionable_attrs.each do |attr, options|
2018-03-17 18:26:18 +05:30
text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend
2017-08-17 22:00:37 +05:30
options = options.merge(
cache_key: [self, attr],
author: author,
skip_project_check: skip_project_check?
)
2016-11-03 12:29:30 +05:30
extractor.analyze(text, options)
2015-12-23 02:04:40 +05:30
end
extractor
2014-09-02 18:07:02 +05:30
end
2018-03-17 18:26:18 +05:30
def extractors
@extractors ||= {}
end
2015-12-23 02:04:40 +05:30
def mentioned_users(current_user = nil)
all_references(current_user).users
2014-09-02 18:07:02 +05:30
end
2017-08-17 22:00:37 +05:30
def directly_addressed_users(current_user = nil)
all_references(current_user).directly_addressed_users
end
2014-09-02 18:07:02 +05:30
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
2016-11-03 12:29:30 +05:30
def referenced_mentionables(current_user = self.author)
2017-09-10 17:25:29 +05:30
return [] unless matches_cross_reference_regex?
2016-11-03 12:29:30 +05:30
refs = all_references(current_user)
2015-12-23 02:04:40 +05:30
refs = (refs.issues + refs.merge_requests + refs.commits)
2015-04-26 12:48:37 +05:30
2015-12-23 02:04:40 +05:30
# 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| ref == local_reference }
2014-09-02 18:07:02 +05:30
end
2017-09-10 17:25:29 +05:30
# Uses regex to quickly determine if mentionables might be referenced
# Allows heavy processing to be skipped
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, _|
2018-03-17 18:26:18 +05:30
__send__(attr) =~ reference_pattern # rubocop:disable GitlabSecurity/PublicSend
2017-09-10 17:25:29 +05:30
end
end
2015-12-23 02:04:40 +05:30
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
2016-11-03 12:29:30 +05:30
def create_cross_references!(author = self.author, without = [])
refs = referenced_mentionables(author)
2015-12-23 02:04:40 +05:30
2015-09-11 14:41:01 +05:30
# 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.
2015-10-24 18:46:33 +05:30
refs.reject! { |ref| without.include?(ref) || cross_reference_exists?(ref) }
2015-09-11 14:41:01 +05:30
2014-09-02 18:07:02 +05:30
refs.each do |ref|
2015-10-24 18:46:33 +05:30
SystemNoteService.cross_reference(ref, local_reference, author)
2014-09-02 18:07:02 +05:30
end
end
2015-09-11 14:41:01 +05:30
# When a mentionable field is changed, creates cross-reference notes that
# don't already exist
2015-10-24 18:46:33 +05:30
def create_new_cross_references!(author = self.author)
2015-09-11 14:41:01 +05:30
changes = detect_mentionable_changes
return if changes.empty?
2014-09-02 18:07:02 +05:30
2016-11-03 12:29:30 +05:30
create_cross_references!(author)
2014-09-02 18:07:02 +05:30
end
2015-09-11 14:41:01 +05:30
private
# Returns a Hash of changed mentionable fields
#
# Preference is given to the `changes` Hash, but falls back to
# `previous_changes` if it's empty (i.e., the changes have already been
# persisted).
#
# See ActiveModel::Dirty.
#
# Returns a Hash.
def detect_mentionable_changes
source = (changes.present? ? changes : previous_changes).dup
2015-12-23 02:04:40 +05:30
mentionable = self.class.mentionable_attrs.map { |attr, options| attr }
2015-09-11 14:41:01 +05:30
# Only include changed fields that are mentionable
source.select { |key, val| mentionable.include?(key) }
end
2015-12-23 02:04:40 +05:30
2015-10-24 18:46:33 +05:30
# Determine whether or not a cross-reference Note has already been created between this Mentionable and
# the specified target.
def cross_reference_exists?(target)
SystemNoteService.cross_reference_exists?(target, local_reference)
end
2017-08-17 22:00:37 +05:30
def skip_project_check?
false
end
2014-09-02 18:07:02 +05:30
end