Class: Note

Overview

A note on the root of an issue, merge request, commit, or snippet.

A note of this type is never resolvable.

Defined Under Namespace

Modules: SpecialRole

Constant Summary collapse

TYPES_RESTRICTED_BY_ABILITY =
{
  branch: :download_code
}.freeze

Constants included from ThrottledTouch

ThrottledTouch::TOUCH_INTERVAL

Constants included from Gitlab::SQL::Pattern

Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_WORD

Constants included from ResolvableNote

ResolvableNote::RESOLVABLE_TYPES

Constants included from CacheMarkdownField

CacheMarkdownField::INVALIDATED_BY

Constants included from Redactable

Redactable::UNSUBSCRIBE_PATTERN

Instance Attribute Summary collapse

Attributes included from Importable

#imported, #importing

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Editable

#last_edited_by

Methods included from ResolvableNote

#potentially_resolvable?, #resolvable?, #resolve!, #resolve_without_save, #resolved?, #to_be_resolved?, #unresolve!, #unresolve_without_save

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from CacheMarkdownField

#attribute_invalidated?, #cached_html_for, #cached_html_up_to_date?, #can_cache_field?, #invalidated_markdown_cache?, #latest_cached_markdown_version, #local_version, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #updated_cached_html_for

Methods included from FasterCacheKeys

#cache_key

Methods included from Awardable

#awarded_emoji?, #downvotes, #grouped_awards, #upvotes, #user_authored?, #user_can_award?

Methods included from Mentionable

#all_references, #create_cross_references!, #create_new_cross_references!, #directly_addressed_users, #extractors, #gfm_reference, #local_reference, #matches_cross_reference_regex?, #mentioned_users, #referenced_group_users, #referenced_groups, #referenced_mentionables, #referenced_project_users, #referenced_projects, #referenced_users, #store_mentions!

Methods included from Participable

#participants

Methods included from Gitlab::Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Methods inherited from ApplicationRecord

at_most, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, underscore, without_order

Instance Attribute Details

#commands_changesObject

Attribute used to store the attributes that have been changed by quick actions.


61
62
63
# File 'app/models/note.rb', line 61

def commands_changes
  @commands_changes
end

#redacted_note_htmlObject

Attribute containing rendered and redacted Markdown as generated by Banzai::ObjectRenderer.


52
53
54
# File 'app/models/note.rb', line 52

def redacted_note_html
  @redacted_note_html
end

#special_roleObject

A special role that may be displayed on issuable's discussions


64
65
66
# File 'app/models/note.rb', line 64

def special_role
  @special_role
end

#total_reference_countObject

Total of all references as generated by Banzai::ObjectRenderer


55
56
57
# File 'app/models/note.rb', line 55

def total_reference_count
  @total_reference_count
end

#user_visible_reference_countObject

Number of user visible references as generated by Banzai::ObjectRenderer


58
59
60
# File 'app/models/note.rb', line 58

def user_visible_reference_count
  @user_visible_reference_count
end

Class Method Details

.count_for_collection(ids, type) ⇒ Object


217
218
219
220
221
# File 'app/models/note.rb', line 217

def count_for_collection(ids, type)
  user.select('noteable_id', 'COUNT(*) as count')
    .group(:noteable_id)
    .where(noteable_type: type, noteable_id: ids)
end

.discussions(context_noteable = nil) ⇒ Object


174
175
176
# File 'app/models/note.rb', line 174

def discussions(context_noteable = nil)
  Discussion.build_collection(all.includes(:noteable).fresh, context_noteable)
end

.find_discussion(discussion_id) ⇒ Object

Note: Where possible consider using Discussion#lazy_find to return Discussions in order to benefit from having records batch loaded.


180
181
182
183
184
185
186
# File 'app/models/note.rb', line 180

def find_discussion(discussion_id)
  notes = where(discussion_id: discussion_id).fresh.to_a

  return if notes.empty?

  Discussion.build(notes)
end

.grouped_diff_discussions(diff_refs = nil) ⇒ Object

Group diff discussions by line code or file path. It is not needed to group by line code when comment is on an image.


191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'app/models/note.rb', line 191

def grouped_diff_discussions(diff_refs = nil)
  groups = {}

  diff_notes.fresh.discussions.each do |discussion|
    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] ||= []
      discussions << discussion
    end
  end

  groups
end

.has_special_role?(role, note) ⇒ Boolean

Returns:

  • (Boolean)

223
224
225
# File 'app/models/note.rb', line 223

def has_special_role?(role, note)
  note.special_role == role
end

.model_nameObject


170
171
172
# File 'app/models/note.rb', line 170

def model_name
  ActiveModel::Name.new(self, nil, 'note')
end

.positionsObject


211
212
213
214
215
# File 'app/models/note.rb', line 211

def positions
  where.not(position: nil)
    .select(:id, :type, :position) # ActiveRecord needs id and type for typecasting.
    .map(&:position)
end

.search(query) ⇒ Object


227
228
229
# File 'app/models/note.rb', line 227

def search(query)
  fuzzy_search(query, [:note])
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)

248
249
250
# File 'app/models/note.rb', line 248

def active?
  true
end

#award_emoji?Boolean

Returns:

  • (Boolean)

385
386
387
# File 'app/models/note.rb', line 385

def award_emoji?
  can_be_award_emoji? && contains_emoji_only?
end

#banzai_render_context(field) ⇒ Object


540
541
542
# File 'app/models/note.rb', line 540

def banzai_render_context(field)
  super.merge(noteable: noteable, system_note: system?, label_url_method: noteable_label_url_method)
end

#can_be_award_emoji?Boolean

Returns:

  • (Boolean)

393
394
395
# File 'app/models/note.rb', line 393

def can_be_award_emoji?
  noteable.is_a?(Awardable) && !part_of_discussion?
end

#can_be_discussion_note?Boolean

Returns:

  • (Boolean)

415
416
417
# File 'app/models/note.rb', line 415

def can_be_discussion_note?
  self.noteable.supports_discussions? && !part_of_discussion?
end

#can_create_todo?Boolean

Returns:

  • (Boolean)

419
420
421
422
# File 'app/models/note.rb', line 419

def can_create_todo?
  # Skip system notes, and notes on snippets
  !system? && !for_snippet?
end

#commitObject


304
305
306
# File 'app/models/note.rb', line 304

def commit
  @commit ||= project.commit(commit_id) if commit_id.present?
end

#confidential?(include_noteable: false) ⇒ Boolean

Returns:

  • (Boolean)

361
362
363
364
365
# File 'app/models/note.rb', line 361

def confidential?(include_noteable: false)
  return true if confidential

  include_noteable && noteable.try(:confidential?)
end

#contains_emoji_only?Boolean

Returns:

  • (Boolean)

397
398
399
# File 'app/models/note.rb', line 397

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

#diff_note?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)

244
245
246
# File 'app/models/note.rb', line 244

def diff_note?
  false
end

#discussionObject

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.


451
452
453
454
455
456
# File 'app/models/note.rb', line 451

def discussion
  strong_memoize(:discussion) do
    full_discussion = self.noteable.notes.find_discussion(self.discussion_id) if part_of_discussion?
    full_discussion || to_discussion
  end
end

#discussion_class(noteable = nil) ⇒ Object


424
425
426
427
428
429
430
431
432
433
# File 'app/models/note.rb', line 424

def discussion_class(noteable = nil)
  # When commit notes are rendered on an MR's Discussion page, they are
  # displayed in one discussion instead of individually.
  # See also `#discussion_id` and `Discussion.override_discussion_id`.
  if noteable && noteable != self.noteable
    OutOfContextDiscussion
  else
    IndividualNoteDiscussion
  end
end

#discussion_id(noteable = nil) ⇒ Object

See `Discussion.override_discussion_id` for details.


436
437
438
# File 'app/models/note.rb', line 436

def discussion_id(noteable = nil)
  discussion_class(noteable).override_discussion_id(self) || super() || ensure_discussion_id
end

#editable?Boolean

Returns:

  • (Boolean)

367
368
369
# File 'app/models/note.rb', line 367

def editable?
  !system?
end

#edited?Boolean

Since we're using `updated_at` as `last_edited_at`, it could be touched by transforming / resolving a note. This makes sure it is only marked as edited when the note body is updated.

Returns:

  • (Boolean)

373
374
375
376
377
# File 'app/models/note.rb', line 373

def edited?
  return false if updated_by.blank?

  super
end

#emoji_awardable?Boolean

Returns:

  • (Boolean)

389
390
391
# File 'app/models/note.rb', line 389

def emoji_awardable?
  !system?
end

#expire_etag_cacheObject


493
494
495
# File 'app/models/note.rb', line 493

def expire_etag_cache
  noteable&.expire_note_etag_cache
end

#for_alert_mangement_alert?Boolean

Returns:

  • (Boolean)

280
281
282
# File 'app/models/note.rb', line 280

def for_alert_mangement_alert?
  noteable_type == 'AlertManagement::Alert'
end

#for_commit?Boolean

Returns:

  • (Boolean)

264
265
266
# File 'app/models/note.rb', line 264

def for_commit?
  noteable_type == "Commit"
end

#for_design?Boolean

Returns:

  • (Boolean)

292
293
294
# File 'app/models/note.rb', line 292

def for_design?
  noteable_type == DesignManagement::Design.name
end

#for_issuable?Boolean

Returns:

  • (Boolean)

296
297
298
# File 'app/models/note.rb', line 296

def for_issuable?
  for_issue? || for_merge_request?
end

#for_issue?Boolean

Returns:

  • (Boolean)

268
269
270
# File 'app/models/note.rb', line 268

def for_issue?
  noteable_type == "Issue"
end

#for_merge_request?Boolean

Returns:

  • (Boolean)

272
273
274
# File 'app/models/note.rb', line 272

def for_merge_request?
  noteable_type == "MergeRequest"
end

#for_personal_snippet?Boolean

Returns:

  • (Boolean)

284
285
286
# File 'app/models/note.rb', line 284

def for_personal_snippet?
  noteable.is_a?(PersonalSnippet)
end

#for_project_noteable?Boolean

Returns:

  • (Boolean)

288
289
290
# File 'app/models/note.rb', line 288

def for_project_noteable?
  !for_personal_snippet?
end

#for_snippet?Boolean

Returns:

  • (Boolean)

276
277
278
# File 'app/models/note.rb', line 276

def for_snippet?
  noteable_type == "Snippet"
end

#has_special_role?(role) ⇒ Boolean

Returns:

  • (Boolean)

351
352
353
# File 'app/models/note.rb', line 351

def has_special_role?(role)
  self.class.has_special_role?(role, self)
end

#hook_attrsObject


256
257
258
# File 'app/models/note.rb', line 256

def hook_attrs
  Gitlab::HookData::NoteBuilder.new(self).build
end

#in_reply_to?(other) ⇒ Boolean

Returns:

  • (Boolean)

466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'app/models/note.rb', line 466

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
end

#max_attachment_sizeObject


252
253
254
# File 'app/models/note.rb', line 252

def max_attachment_size
  Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end

#merge_requestsObject

Notes on merge requests and commits can be traced back to one or several MRs. This method returns a relation if the note is for one of these types, or nil if it is a note on some other object.


311
312
313
314
315
316
317
318
319
# File 'app/models/note.rb', line 311

def merge_requests
  if for_commit?
    project.merge_requests.by_commit_sha(commit_id)
  elsif for_merge_request?
    MergeRequest.id_in(noteable_id)
  else
    nil
  end
end

#noteableObject

override to return commits, which are not active record


322
323
324
325
326
327
328
329
330
# File 'app/models/note.rb', line 322

def noteable
  return commit if for_commit?

  super
rescue
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
  nil
end

#noteable_ability_nameObject


405
406
407
408
409
410
411
412
413
# File 'app/models/note.rb', line 405

def noteable_ability_name
  if for_snippet?
    'snippet'
  elsif for_alert_mangement_alert?
    'alert_management_alert'
  else
    noteable_type.demodulize.underscore
  end
end

#noteable_assignee_or_author?(user) ⇒ Boolean

Returns:

  • (Boolean)

338
339
340
341
342
343
# File 'app/models/note.rb', line 338

def noteable_assignee_or_author?(user)
  return false unless user
  return noteable.assignee_or_author?(user) if [MergeRequest, Issue].include?(noteable.class)

  noteable.author_id == user.id
end

#noteable_type=(noteable_type) ⇒ Object

FIXME: Hack for polymorphic associations with STI

For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations

334
335
336
# File 'app/models/note.rb', line 334

def noteable_type=(noteable_type)
  super(noteable_type.to_s.classify.constantize.base_class.to_s)
end

#notify_after_createObject


532
533
534
# File 'app/models/note.rb', line 532

def notify_after_create
  noteable&.after_note_created(self)
end

#notify_after_destroyObject


536
537
538
# File 'app/models/note.rb', line 536

def notify_after_destroy
  noteable&.after_note_destroyed(self)
end

#parent_userObject


564
565
566
# File 'app/models/note.rb', line 564

def parent_user
  noteable.author if for_personal_snippet?
end

#part_of_discussion?Boolean

Returns:

  • (Boolean)

462
463
464
# File 'app/models/note.rb', line 462

def part_of_discussion?
  !to_discussion.individual_note?
end

#readable_by?(user) ⇒ Boolean

This method is to be used for checking read permissions on a note instead of `system_note_with_references_visible_for?`

Returns:

  • (Boolean)

380
381
382
383
# File 'app/models/note.rb', line 380

def readable_by?(user)
  # note_policy accounts for #system_note_with_references_visible_for?(user) check when granting read access
  Ability.allowed?(user, :read_note, self)
end

#referencesObject


483
484
485
486
487
488
489
490
491
# File 'app/models/note.rb', line 483

def references
  refs = [noteable]

  if part_of_discussion?
    refs += discussion.notes.take_while { |n| n.id < id }
  end

  refs
end

#resource_parentObject


548
549
550
# File 'app/models/note.rb', line 548

def resource_parent
  project
end

#retrieve_upload(_identifier, paths) ⇒ Object


544
545
546
# File 'app/models/note.rb', line 544

def retrieve_upload(_identifier, paths)
  Upload.find_by(model: self, path: paths)
end

#skip_notification?Boolean

Returns:

  • (Boolean)

568
569
570
# File 'app/models/note.rb', line 568

def skip_notification?
  review.present?
end

#skip_project_check?Boolean

Returns:

  • (Boolean)

300
301
302
# File 'app/models/note.rb', line 300

def skip_project_check?
  !for_project_noteable?
end

#specialize_for_first_contribution!(noteable) ⇒ Object


355
356
357
358
359
# File 'app/models/note.rb', line 355

def specialize_for_first_contribution!(noteable)
  return unless noteable.author_id == self.author_id

  self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end

#start_of_discussion?Boolean

Returns:

  • (Boolean)

458
459
460
# File 'app/models/note.rb', line 458

def start_of_discussion?
  discussion.first_note == self
end

#supports_suggestion?Boolean

Returns:

  • (Boolean)

260
261
262
# File 'app/models/note.rb', line 260

def supports_suggestion?
  false
end

#system_note_with_references?Boolean

rubocop: disable CodeReuse/ServiceClass

Returns:

  • (Boolean)

233
234
235
236
237
238
239
240
241
# File 'app/models/note.rb', line 233

def system_note_with_references?
  return unless system?

  if force_cross_reference_regex_check?
    matches_cross_reference_regex?
  else
    ::SystemNotes::IssuablesService.cross_reference?(note)
  end
end

#system_note_with_references_visible_for?(user) ⇒ Boolean

Returns:

  • (Boolean)

558
559
560
561
562
# File 'app/models/note.rb', line 558

def system_note_with_references_visible_for?(user)
  return true unless system?

  (!system_note_with_references? || all_referenced_mentionables_allowed?(user)) && system_note_viewable_by?(user)
end

#to_ability_nameObject


401
402
403
# File 'app/models/note.rb', line 401

def to_ability_name
  model_name.singular
end

#to_discussion(noteable = nil) ⇒ Object

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.


444
445
446
# File 'app/models/note.rb', line 444

def to_discussion(noteable = nil)
  Discussion.build([self], noteable)
end

#touch(*args) ⇒ Object


497
498
499
500
501
502
503
504
505
506
# File 'app/models/note.rb', line 497

def touch(*args)
  # We're not using an explicit transaction here because this would in all
  # cases result in all future queries going to the primary, even if no writes
  # are performed.
  #
  # We touch the noteable first so its SELECT query can run before our writes,
  # ensuring it runs on a secondary (if no prior write took place).
  touch_noteable
  super
end

#touch_noteableObject

By default Rails will issue an “SELECT *” for the relation, which is overkill for just updating the timestamps. To work around this we manually touch the data so we can SELECT only the columns we need.


511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'app/models/note.rb', line 511

def touch_noteable
  # Commits are not stored in the DB so we can't touch them.
  return if for_commit?

  assoc = association(:noteable)

  noteable_object =
    if assoc.loaded?
      noteable
    else
      # If the object is not loaded (e.g. when notes are loaded async) we
      # _only_ want the data we actually need.
      assoc.scope.select(:id, :updated_at).take
    end

  noteable_object&.touch

  # We return the noteable object so we can re-use it in EE for Elasticsearch.
  noteable_object
end

#user_mentionsObject


552
553
554
555
556
# File 'app/models/note.rb', line 552

def user_mentions
  return Note.none unless noteable.present?

  noteable.user_mentions.where(note: self)
end