note.rb 11.0 KB
Newer Older
1 2 3
# A note on the root of an issue, merge request, commit, or snippet.
#
# A note of this type is never resolvable.
G
gitlabhq 已提交
4
class Note < ActiveRecord::Base
D
Douwe Maan 已提交
5
  extend ActiveModel::Naming
6
  include Gitlab::CurrentSettings
7
  include Participable
8
  include Mentionable
Z
ZJ van de Weg 已提交
9
  include Awardable
10
  include Importable
11
  include FasterCacheKeys
N
Nick Thomas 已提交
12
  include CacheMarkdownField
13
  include AfterCommitQueue
14
  include ResolvableNote
15
  include IgnorableColumn
16
  include Editable
17

18 19 20 21 22 23 24 25 26 27
  module SpecialRole
    FIRST_TIME_CONTRIBUTOR = :first_time_contributor

    class << self
      def values
        constants.map {|const| self.const_get(const)}
      end
    end
  end

28
  ignore_column :original_discussion_id
N
Nick Thomas 已提交
29

30
  cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
31

B
blackst0ne 已提交
32 33
  # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with notes.
  # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102
B
blackst0ne 已提交
34 35 36
  alias_attribute :last_edited_at, :updated_at
  alias_attribute :last_edited_by, :updated_by

37 38
  # Attribute containing rendered and redacted Markdown as generated by
  # Banzai::ObjectRenderer.
N
Nick Thomas 已提交
39
  attr_accessor :redacted_note_html
40

41 42 43 44
  # An Array containing the number of visible references as generated by
  # Banzai::ObjectRenderer
  attr_accessor :user_visible_reference_count

45
  # Attribute used to store the attributes that have been changed by quick actions.
46 47
  attr_accessor :commands_changes

48 49
  # A special role that may be displayed on issuable's discussions
  attr_accessor :special_role
M
micael.bergeron 已提交
50

51 52
  default_value_for :system, false

Y
Yorick Peterse 已提交
53
  attr_mentionable :note, pipeline: :note
54
  participant :author
55

G
gitlabhq 已提交
56
  belongs_to :project
57
  belongs_to :noteable, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
N
Nihad Abbasov 已提交
58
  belongs_to :author, class_name: "User"
59
  belongs_to :updated_by, class_name: "User"
60
  belongs_to :last_edited_by, class_name: 'User'
G
gitlabhq 已提交
61

62 63
  has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
64
  has_one :system_note_metadata
65

66
  delegate :gfm_reference, :local_reference, to: :noteable
N
Nihad Abbasov 已提交
67 68
  delegate :name, to: :project, prefix: true
  delegate :name, :email, to: :author, prefix: true
69
  delegate :title, to: :noteable, allow_nil: true
70

J
Jarka Kadlecova 已提交
71
  validates :note, presence: true
72
  validates :project, presence: true, if: :for_project_noteable?
Z
Z.J. van de Weg 已提交
73

74 75
  # Attachments are deprecated and are handled by Markdown uploader
  validates :attachment, file_size: { maximum: :max_attachment_size }
G
gitlabhq 已提交
76

77
  validates :noteable_type, presence: true
78
  validates :noteable_id, presence: true, unless: [:for_commit?, :importing?]
79
  validates :commit_id, presence: true, if: :for_commit?
V
Valery Sizov 已提交
80
  validates :author, presence: true
D
Douwe Maan 已提交
81
  validates :discussion_id, presence: true, format: { with: /\A\h{40}\z/ }
82

J
Jarka Kadlecova 已提交
83
  validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
84
    unless note.noteable.try(:project) == note.project
D
Douwe Maan 已提交
85
      errors.add(:project, 'does not match noteable project')
86 87 88
    end
  end

N
Nihad Abbasov 已提交
89
  mount_uploader :attachment, AttachmentUploader
A
Andrey Kumanyaev 已提交
90 91

  # Scopes
D
Dmitriy Zaporozhets 已提交
92
  scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
93 94 95 96 97 98 99
  scope :system, -> { where(system: true) }
  scope :user, -> { where(system: false) }
  scope :common, -> { where(noteable_type: ["", nil]) }
  scope :fresh, -> { order(created_at: :asc, id: :asc) }
  scope :updated_after, ->(time) { where('updated_at > ?', time) }
  scope :inc_author_project, -> { includes(:project, :author) }
  scope :inc_author, -> { includes(:author) }
J
Jarka Kadlecova 已提交
100 101 102
  scope :inc_relations_for_view, -> do
    includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
  end
G
gitlabhq 已提交
103

104 105 106
  scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
  scope :new_diff_notes, -> { where(type: 'DiffNote') }
  scope :non_diff_notes, -> { where(type: ['Note', 'DiscussionNote', nil]) }
D
Douwe Maan 已提交
107

108
  scope :with_associations, -> do
109 110
    # FYI noteable cannot be loaded for LegacyDiffNote for commits
    includes(:author, :noteable, :updated_by,
Y
Yorick Peterse 已提交
111
             project: [:project_members, { group: [:group_members] }])
112
  end
G
gitlabhq 已提交
113

D
Douwe Maan 已提交
114
  after_initialize :ensure_discussion_id
115
  before_validation :nullify_blank_type, :nullify_blank_line_code
D
Douwe Maan 已提交
116
  before_validation :set_discussion_id, on: :create
117
  after_save :keep_around_commit, if: :for_project_noteable?
118
  after_save :expire_etag_cache
119
  after_destroy :expire_etag_cache
120

121
  class << self
D
Douwe Maan 已提交
122 123 124
    def model_name
      ActiveModel::Name.new(self, nil, 'note')
    end
125

126
    def discussions(context_noteable = nil)
D
Douwe Maan 已提交
127
      Discussion.build_collection(all.includes(:noteable).fresh, context_noteable)
128
    end
129

130 131 132 133 134
    def find_discussion(discussion_id)
      notes = where(discussion_id: discussion_id).fresh.to_a
      return if notes.empty?

      Discussion.build(notes)
135
    end
136

F
Felipe Artur 已提交
137 138 139
    # Group diff discussions by line code or file path.
    # It is not needed to group by line code when comment is
    # on an image.
140
    def grouped_diff_discussions(diff_refs = nil)
D
Douwe Maan 已提交
141
      groups = {}
142 143

      diff_notes.fresh.discussions.each do |discussion|
F
Felipe Artur 已提交
144 145 146 147 148 149 150 151 152
        group_key =
          if discussion.on_image?
            discussion.file_new_path
          else
            discussion.line_code_in_diffs(diff_refs)
          end

        if group_key
          discussions = groups[group_key] ||= []
D
Douwe Maan 已提交
153 154
          discussions << discussion
        end
155 156 157
      end

      groups
158
    end
159 160

    def count_for_collection(ids, type)
161 162 163
      user.select('noteable_id', 'COUNT(*) as count')
        .group(:noteable_id)
        .where(noteable_type: type, noteable_id: ids)
164
    end
165 166 167 168

    def has_special_role?(role, note)
      note.special_role == role
    end
R
Robert Speicher 已提交
169
  end
170

R
Robert Speicher 已提交
171
  def cross_reference?
172 173 174 175 176 177 178
    return unless system?

    if force_cross_reference_regex_check?
      matches_cross_reference_regex?
    else
      SystemNoteService.cross_reference?(note)
    end
179 180
  end

D
Douwe Maan 已提交
181 182
  def diff_note?
    false
183 184
  end

185
  def active?
D
Douwe Maan 已提交
186
    true
187 188
  end

D
Douwe Maan 已提交
189 190
  def max_attachment_size
    current_application_settings.max_attachment_size.megabytes.to_i
D
Dmitriy Zaporozhets 已提交
191 192
  end

D
Douwe Maan 已提交
193 194
  def hook_attrs
    attributes
195 196 197 198 199 200
  end

  def for_commit?
    noteable_type == "Commit"
  end

R
Riyad Preukschas 已提交
201 202 203 204
  def for_issue?
    noteable_type == "Issue"
  end

205 206 207 208
  def for_merge_request?
    noteable_type == "MergeRequest"
  end

209
  def for_snippet?
210 211 212
    noteable_type == "Snippet"
  end

J
Jarka Kadlecova 已提交
213
  def for_personal_snippet?
J
Jarka Kadlecova 已提交
214 215 216
    noteable.is_a?(PersonalSnippet)
  end

217 218 219 220
  def for_project_noteable?
    !for_personal_snippet?
  end

J
Jarka Kadlecova 已提交
221 222
  def skip_project_check?
    for_personal_snippet?
J
Jarka Kadlecova 已提交
223 224
  end

R
Riyad Preukschas 已提交
225 226 227
  # override to return commits, which are not active record
  def noteable
    if for_commit?
228
      @commit ||= project.commit(commit_id)
229
    else
R
Riyad Preukschas 已提交
230
      super
231
    end
232 233
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
234
  rescue
D
Dmitriy Zaporozhets 已提交
235
    nil
236
  end
237

A
Andrew8xx8 已提交
238
  # FIXME: Hack for polymorphic associations with STI
S
Steven Burgart 已提交
239
  #        For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
240 241
  def noteable_type=(noteable_type)
    super(noteable_type.to_s.classify.constantize.base_class.to_s)
A
Andrew8xx8 已提交
242
  end
D
Drew Blessing 已提交
243

244
  def special_role=(role)
245 246
    raise "Role is undefined, #{role} not found in #{SpecialRole.values}" unless SpecialRole.values.include?(role)

247 248 249 250
    @special_role = role
  end

  def has_special_role?(role)
251
    self.class.has_special_role?(role, self)
252 253
  end

M
micael.bergeron 已提交
254 255
  def specialize_for_first_contribution!(noteable)
    return unless noteable.author_id == self.author_id
M
micael.bergeron 已提交
256 257

    self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
M
micael.bergeron 已提交
258
  end
M
micael.bergeron 已提交
259

260
  def editable?
261
    !system?
262
  end
263

264
  def cross_reference_not_visible_for?(user)
265 266 267 268 269 270 271 272 273
    cross_reference? && !has_referenced_mentionables?(user)
  end

  def has_referenced_mentionables?(user)
    if user_visible_reference_count.present?
      user_visible_reference_count > 0
    else
      referenced_mentionables(user).any?
    end
274 275
  end

276
  def award_emoji?
277
    can_be_award_emoji? && contains_emoji_only?
278 279
  end

280 281 282 283
  def emoji_awardable?
    !system?
  end

284
  def can_be_award_emoji?
285
    noteable.is_a?(Awardable) && !part_of_discussion?
286 287
  end

288
  def contains_emoji_only?
289
    note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
290 291
  end

J
Jarka Kadlecova 已提交
292 293 294 295
  def to_ability_name
    for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
  end

296
  def can_be_discussion_note?
297
    self.noteable.supports_discussions? && !part_of_discussion?
298 299
  end

300 301
  def discussion_class(noteable = nil)
    # When commit notes are rendered on an MR's Discussion page, they are
D
Douwe Maan 已提交
302 303
    # displayed in one discussion instead of individually.
    # See also `#discussion_id` and `Discussion.override_discussion_id`.
D
Douwe Maan 已提交
304 305
    if noteable && noteable != self.noteable
      OutOfContextDiscussion
306 307 308 309 310
    else
      IndividualNoteDiscussion
    end
  end

D
Douwe Maan 已提交
311
  # See `Discussion.override_discussion_id` for details.
312 313 314 315
  def discussion_id(noteable = nil)
    discussion_class(noteable).override_discussion_id(self) || super()
  end

D
Douwe Maan 已提交
316 317 318 319
  # Returns a discussion containing just this note.
  # This method exists as an alternative to `#discussion` to use when the methods
  # we intend to call on the Discussion object don't require it to have all of its notes,
  # and just depend on the first note or the type of discussion. This saves us a DB query.
320 321 322 323
  def to_discussion(noteable = nil)
    Discussion.build([self], noteable)
  end

D
Douwe Maan 已提交
324 325 326
  # Returns the entire discussion this note is part of.
  # Consider using `#to_discussion` if we do not need to render the discussion
  # and all its notes and if we don't care about the discussion's resolvability status.
327
  def discussion
D
Douwe Maan 已提交
328 329
    full_discussion = self.noteable.notes.find_discussion(self.discussion_id) if part_of_discussion?
    full_discussion || to_discussion
330 331 332
  end

  def part_of_discussion?
D
Douwe Maan 已提交
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
    !to_discussion.individual_note?
  end

  def in_reply_to?(other)
    case other
    when Note
      if part_of_discussion?
        in_reply_to?(other.noteable) && in_reply_to?(other.to_discussion)
      else
        in_reply_to?(other.noteable)
      end
    when Discussion
      self.discussion_id == other.id
    when Noteable
      self.noteable == other
    else
      false
    end
351 352
  end

353
  def expire_etag_cache
D
Douwe Maan 已提交
354
    return unless noteable&.discussions_rendered_on_frontend?
355 356

    key = Gitlab::Routing.url_helpers.project_noteable_notes_path(
D
Douwe Maan 已提交
357
      project,
358
      target_type: noteable_type.underscore,
D
Douwe Maan 已提交
359
      target_id: noteable_id
360 361 362 363
    )
    Gitlab::EtagCaching::Store.new.touch(key)
  end

364 365 366 367 368
  private

  def keep_around_commit
    project.repository.keep_around(self.commit_id)
  end
369 370 371 372 373 374 375 376

  def nullify_blank_type
    self.type = nil if self.type.blank?
  end

  def nullify_blank_line_code
    self.line_code = nil if self.line_code.blank?
  end
377 378 379

  def ensure_discussion_id
    return unless self.persisted?
380 381
    # Needed in case the SELECT statement doesn't ask for `discussion_id`
    return unless self.has_attribute?(:discussion_id)
382 383 384 385 386 387 388
    return if self.discussion_id

    set_discussion_id
    update_column(:discussion_id, self.discussion_id)
  end

  def set_discussion_id
389
    self.discussion_id ||= discussion_class.discussion_id(self)
390
  end
391 392 393 394 395 396

  def force_cross_reference_regex_check?
    return unless system?

    SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action)
  end
G
gitlabhq 已提交
397
end