mentionable.rb 3.3 KB
Newer Older
1 2
# == Mentionable concern
#
3 4
# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by
# GFM references.
5
#
6
# Used by Issue, Note, MergeRequest, and Commit.
7 8 9 10
#
module Mentionable
  extend ActiveSupport::Concern

11 12
  module ClassMethods
    # Indicate which attributes of the Mentionable to search for GFM references.
13
    def attr_mentionable(*attrs)
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
      mentionable_attrs.concat(attrs.map(&:to_s))
    end

    # Accessor for attributes marked mentionable.
    def mentionable_attrs
      @mentionable_attrs ||= []
    end
  end

  # Generate a GFM back-reference that will construct a link back to this Mentionable when rendered. Must
  # be overridden if this model object can be referenced directly by GFM notation.
  def gfm_reference
    raise NotImplementedError.new("#{self.class} does not implement #gfm_reference")
  end

  # Construct a String that contains possible GFM references.
  def mentionable_text
    self.class.mentionable_attrs.map { |attr| send(attr) || '' }.join
  end

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

  # Determine whether or not a cross-reference Note has already been created between this Mentionable and
  # the specified target.
41
  def has_mentioned?(target)
42 43 44
    Note.cross_reference_exists?(target, local_reference)
  end

45 46 47 48 49 50 51
  def mentioned_users
    users = []
    return users if mentionable_text.blank?
    has_project = self.respond_to? :project
    matches = mentionable_text.scan(/@[a-zA-Z][a-zA-Z0-9_\-\.]*/)
    matches.each do |match|
      identifier = match.delete "@"
52
      if identifier == "all"
53
        users.push(*project.team.members.flatten)
D
Douwe Maan 已提交
54
      elsif namespace = Namespace.find_by(path: identifier)
55
        if namespace.is_a?(Group)
D
Douwe Maan 已提交
56 57 58 59
          users.push(*namespace.users)
        else
          users << namespace.owner
        end
60 61 62 63 64
      end
    end
    users.uniq
  end

65
  # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
66
  def references(p = project, text = mentionable_text)
67 68
    return [] if text.blank?
    ext = Gitlab::ReferenceExtractor.new
69
    ext.analyze(text, p)
70 71 72 73

    (ext.issues_for(p)  +
     ext.merge_requests_for(p) +
     ext.commits_for(p)).uniq - [local_reference]
74 75 76
  end

  # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
77
  def create_cross_references!(p = project, a = author, without = [])
78 79 80
    refs = references(p) - without
    refs.each do |ref|
      Note.create_cross_reference_note(ref, local_reference, a, p)
81 82 83
    end
  end

84 85
  # If the mentionable_text field is about to change, locate any *added* references and create cross references for
  # them. Invoke from an observer's #before_save implementation.
86
  def notice_added_references(p = project, a = author)
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
    ch = changed_attributes
    original, mentionable_changed = "", false
    self.class.mentionable_attrs.each do |attr|
      if ch[attr]
        original << ch[attr]
        mentionable_changed = true
      end
    end

    # Only proceed if the saved changes actually include a chance to an attr_mentionable field.
    return unless mentionable_changed

    preexisting = references(p, original)
    create_cross_references!(p, a, preexisting)
  end
102
end