Module: Issuable

Extended by:
ActiveSupport::Concern
Includes:
AfterCommitQueue, Awardable, CacheMarkdownField, ClosedAtFilterable, CreatedAtFilterable, Editable, Exportable, Gitlab::SQL::Pattern, Import::HasImportSource, Importable, Mentionable, Milestoneable, Participable, Redactable, ReportableChanges, Sortable, SortableTitle, StripAttribute, Subscribable, Taskable, Transitionable, UpdatedAtFilterable, VersionedDescription
Included in:
Issue, MergeRequest
Defined in:
app/models/concerns/issuable.rb,
app/services/issuable/callbacks/base.rb,
app/services/issuable/destroy_service.rb,
app/services/issuable/callbacks/labels.rb,
app/services/issuable/process_assignees.rb,
app/services/issuable/bulk_update_service.rb,
app/services/issuable/callbacks/milestone.rb,
app/services/issuable/callbacks/description.rb,
app/services/issuable/callbacks/time_tracking.rb,
app/services/issuable/import_csv/base_service.rb,
app/services/issuable/discussions_list_service.rb,
app/workers/issuable/label_links_destroy_worker.rb,
app/workers/issuable/related_links_create_worker.rb,
app/services/issuable/common_system_notes_service.rb,
app/services/issuable/destroy_label_links_service.rb

Overview

This service return notes grouped by discussion ID and paginated per discussion. System notes also have a discussion ID assigned including Synthetic system notes.

Defined Under Namespace

Modules: Callbacks, ImportCsv Classes: BulkUpdateService, CommonSystemNotesService, DestroyLabelLinksService, DestroyService, DiscussionsListService, LabelLinksDestroyWorker, ProcessAssignees, RelatedLinksCreateWorker

Constant Summary collapse

TITLE_LENGTH_MAX =
255
DESCRIPTION_LENGTH_MAX =
1.megabyte
SEARCHABLE_FIELDS =
%w[title description].freeze
MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS =
200
STATE_ID_MAP =
{
  opened: 1,
  closed: 2,
  merged: 3,
  locked: 4
}.with_indifferent_access.freeze

Constants included from Import::HasImportSource

Import::HasImportSource::IMPORT_SOURCES

Constants included from Taskable

Taskable::COMPLETED, Taskable::COMPLETE_PATTERN, Taskable::INCOMPLETE, Taskable::INCOMPLETE_PATTERN, Taskable::ITEM_PATTERN

Constants included from CacheMarkdownField

CacheMarkdownField::INVALIDATED_BY

Constants included from Redactable

Redactable::UNSUBSCRIBE_PATTERN

Constants included from Gitlab::SQL::Pattern

Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_TERM

Instance Attribute Summary

Attributes included from Transitionable

#transitioning

Attributes included from Importable

#importing, #user_contributions

Attributes included from CacheMarkdownField

#skip_markdown_cache_validation

Instance Method Summary collapse

Methods included from Import::HasImportSource

#imported?

Methods included from ReportableChanges

#as_json, #changes_applied, #clear_changes_information, #reload, #reportable_changes

Methods included from Exportable

#exportable_association?, #restricted_associations, #to_authorized_json

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from Editable

#edited?, #last_edited_by

Methods included from Transitionable

#disable_transitioning, #enable_transitioning, #transitioning?

Methods included from Taskable

#complete_task_list_item_count, get_tasks, get_updated_tasks, #task_completion_status, #task_list_items, #task_status, #task_status_short, #tasks?

Methods included from Awardable

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

Methods included from StripAttribute

#strip_attributes!

Methods included from Subscribable

#lazy_subscription, #set_subscription, #subscribe, #subscribed?, #subscribers, #toggle_subscription, #unsubscribe

Methods included from Milestoneable

#milestone_available?, #supports_milestone?

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_projects, #referenced_users, #user_mention_class, #user_mention_identifier

Methods included from Participable

#participant?, #participants, #visible_participants

Methods included from CacheMarkdownField

#attribute_invalidated?, #banzai_render_context, #cached_html_for, #cached_html_up_to_date?, #can_cache_field?, #invalidated_markdown_cache?, #latest_cached_markdown_version, #mentionable_attributes_changed?, #mentioned_filtered_user_ids_for, #parent_user, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #skip_project_check?, #store_mentions!, #store_mentions?, #store_mentions_after_commit?, #updated_cached_html_for

Methods included from Gitlab::SQL::Pattern

split_query_to_search_terms

Instance Method Details

#allows_scoped_labels?Boolean



626
627
628
# File 'app/models/concerns/issuable.rb', line 626

def allows_scoped_labels?
  false
end

#assignee?(user) ⇒ Boolean



514
515
516
517
518
519
520
521
# File 'app/models/concerns/issuable.rb', line 514

def assignee?(user)
  # Necessary so we can preload the association and avoid N + 1 queries
  if assignees.loaded?
    assignees.to_a.include?(user)
  else
    assignees.exists?(user.id)
  end
end

#assignee_listObject



648
649
650
# File 'app/models/concerns/issuable.rb', line 648

def assignee_list
  assignees.map(&:name).to_sentence
end

#assignee_or_author?(user) ⇒ Boolean



510
511
512
# File 'app/models/concerns/issuable.rb', line 510

def assignee_or_author?(user)
  author_id == user.id || assignee?(user)
end

#assignee_username_listObject



652
653
654
# File 'app/models/concerns/issuable.rb', line 652

def assignee_username_list
  assignees.map(&:username).join(',')
end

#can_move?Boolean

Method that checks if issuable can be moved to another project.

Should be overridden if issuable can be moved.



695
696
697
# File 'app/models/concerns/issuable.rb', line 695

def can_move?(*)
  false
end

#card_attributesObject

Returns a Hash of attributes to be used for Twitter card metadata



641
642
643
644
645
646
# File 'app/models/concerns/issuable.rb', line 641

def card_attributes
  {
    'Author' => author.try(:name),
    'Assignee' => assignee_list
  }
end

#draftless_title_changed(old_title) ⇒ Object

Overridden in MergeRequest



709
710
711
# File 'app/models/concerns/issuable.rb', line 709

def draftless_title_changed(old_title)
  old_title != title
end

#first_contribution?Boolean

Override in issuable specialization



702
703
704
# File 'app/models/concerns/issuable.rb', line 702

def first_contribution?
  false
end

#hook_association_changes(old_associations) ⇒ Object

rubocop:disable Metrics/PerceivedComplexity – Related issue: gitlab.com/gitlab-org/gitlab/-/issues/437679



548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
# File 'app/models/concerns/issuable.rb', line 548

def hook_association_changes(old_associations)
  changes = {}

  if old_assignees(old_associations) != assignees
    changes[:assignees] = [old_assignees(old_associations).map(&:hook_attrs), assignees.map(&:hook_attrs)]
  end

  if old_labels(old_associations) != labels
    changes[:labels] = [old_labels(old_associations).map(&:hook_attrs), labels.map(&:hook_attrs)]
  end

  if supports_severity? && old_severity(old_associations) != severity
    changes[:severity] = [old_severity(old_associations), severity]
  end

  if is_a?(MergeRequest) && old_target_branch(old_associations) != target_branch
    changes[:target_branch] = [old_target_branch(old_associations), target_branch]
  end

  if supports_escalation? && escalation_status &&
      old_escalation_status(old_associations) != escalation_status.status_name
    changes[:escalation_status] = [old_escalation_status(old_associations), escalation_status.status_name]
  end

  if respond_to?(:total_time_spent) && old_total_time_spent(old_associations) != total_time_spent
    changes[:total_time_spent] = [old_total_time_spent(old_associations), total_time_spent]
    changes[:time_change] = [old_time_change(old_associations), time_change]
  end

  changes
end

#hook_reviewer_changes(old_associations) ⇒ Object



591
592
593
594
595
596
597
598
599
600
601
# File 'app/models/concerns/issuable.rb', line 591

def hook_reviewer_changes(old_associations)
  changes = {}

  re_requested_reviewer_id = old_associations.fetch(:re_requested_reviewer_id, nil)
  old_reviewer_attrs = old_associations.fetch(:reviewers_hook_attrs, [])
  current_reviewer_attrs = reviewers_hook_attrs(re_requested_reviewer_id: re_requested_reviewer_id)

  changes[:reviewers] = [old_reviewer_attrs, current_reviewer_attrs] if old_reviewer_attrs != current_reviewer_attrs

  changes
end

#label_namesObject



618
619
620
# File 'app/models/concerns/issuable.rb', line 618

def label_names
  labels.order('title ASC').pluck(:title)
end

#labels_arrayObject



614
615
616
# File 'app/models/concerns/issuable.rb', line 614

def labels_array
  labels.to_a
end

#labels_hook_attrsObject



622
623
624
# File 'app/models/concerns/issuable.rb', line 622

def labels_hook_attrs
  labels.map(&:hook_attrs)
end

#notes_for_participantsObject



656
657
658
# File 'app/models/concerns/issuable.rb', line 656

def notes_for_participants
  notes_with_associations.limit(Noteable::MAX_NOTES_LIMIT * 2)
end

#notes_with_associationsObject



660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
# File 'app/models/concerns/issuable.rb', line 660

def notes_with_associations
  # If A has_many Bs, and B has_many Cs, and you do
  # `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
  # will do the inclusion again. So, we check if all notes in the relation
  # already have their authors loaded (possibly because the scope
  # `inc_notes_with_associations` was used) and skip the inclusion if that's
  # the case.
  includes = []
  includes << :author unless notes.authors_loaded?
  includes << :award_emoji unless notes.award_emojis_loaded?

  unless Feature.enabled?(:remove_per_source_permission_from_participants, Feature.current_request)
    includes << :project unless notes.projects_loaded?
    includes << :system_note_metadata unless notes.
  end

  if persisted? && includes.any?
    notes.includes(includes)
  else
    notes
  end
end

#old_assignees(assoc) ⇒ Object



726
727
728
# File 'app/models/concerns/issuable.rb', line 726

def old_assignees(assoc)
  @_old_assignees ||= assoc.fetch(:assignees, assignees)
end

#old_escalation_status(assoc) ⇒ Object



742
743
744
# File 'app/models/concerns/issuable.rb', line 742

def old_escalation_status(assoc)
  @_old_escalation_status ||= assoc.fetch(:escalation_status, escalation_status.status_name)
end

#old_labels(assoc) ⇒ Object



730
731
732
# File 'app/models/concerns/issuable.rb', line 730

def old_labels(assoc)
  @_old_labels ||= assoc.fetch(:labels, labels)
end

#old_severity(assoc) ⇒ Object



734
735
736
# File 'app/models/concerns/issuable.rb', line 734

def old_severity(assoc)
  @_old_severity ||= assoc.fetch(:severity, severity)
end

#old_target_branch(assoc) ⇒ Object



738
739
740
# File 'app/models/concerns/issuable.rb', line 738

def old_target_branch(assoc)
  @_old_target_branch ||= assoc.fetch(:target_branch, target_branch)
end

#old_time_change(assoc) ⇒ Object



750
751
752
# File 'app/models/concerns/issuable.rb', line 750

def old_time_change(assoc)
  @_old_time_change ||= assoc.fetch(:time_change, time_change)
end

#old_total_time_spent(assoc) ⇒ Object



746
747
748
# File 'app/models/concerns/issuable.rb', line 746

def old_total_time_spent(assoc)
  @_old_total_time_spent ||= assoc.fetch(:total_time_spent, total_time_spent)
end

#open?Boolean



523
524
525
# File 'app/models/concerns/issuable.rb', line 523

def open?
  opened?
end

#overdue?Boolean



527
528
529
530
531
# File 'app/models/concerns/issuable.rb', line 527

def overdue?
  return false unless respond_to?(:due_date)

  due_date.try(:past?) || false
end

#read_ability_for(participable_source) ⇒ Object



713
714
715
716
717
718
719
720
# File 'app/models/concerns/issuable.rb', line 713

def read_ability_for(participable_source)
  return super if participable_source == self
  return super if participable_source.is_a?(Note) && participable_source.system?

  name =  participable_source.try(:issuable_ability_name) || :read_issuable_participables

  { name: name, subject: self }
end

#resource_parentObject



506
507
508
# File 'app/models/concerns/issuable.rb', line 506

def resource_parent
  project
end

#reviewers_hook_attrs(re_requested_reviewer_id: nil) ⇒ Object

rubocop:enable Metrics/PerceivedComplexity



581
582
583
584
585
586
587
588
589
# File 'app/models/concerns/issuable.rb', line 581

def reviewers_hook_attrs(re_requested_reviewer_id: nil)
  return [] unless allows_reviewers?

  merge_request_reviewers.includes(:reviewer).map do |mr_reviewer|
    attrs = mr_reviewer.reviewer.hook_attrs.merge(state: mr_reviewer.state)
    attrs[:re_requested] = !!(re_requested_reviewer_id && mr_reviewer.reviewer.id == re_requested_reviewer_id)
    attrs
  end
end

#stateObject



498
499
500
# File 'app/models/concerns/issuable.rb', line 498

def state
  self.class.available_states.key(state_id)
end

#state=(value) ⇒ Object



502
503
504
# File 'app/models/concerns/issuable.rb', line 502

def state=(value)
  self.state_id = self.class.available_states[value]
end

#subscribed_without_subscriptions?(user, _project) ⇒ Boolean



543
544
545
# File 'app/models/concerns/issuable.rb', line 543

def subscribed_without_subscriptions?(user, _project)
  participant?(user)
end

#supports_health_status?Boolean



722
723
724
# File 'app/models/concerns/issuable.rb', line 722

def supports_health_status?
  false
end

#to_ability_nameObject

Convert this Issuable class name to a format usable by Ability definitions

Examples:

issuable.class           # => MergeRequest
issuable.to_ability_name # => "merge_request"


636
637
638
# File 'app/models/concerns/issuable.rb', line 636

def to_ability_name
  self.class.to_ability_name
end

#to_hook_data(user, old_associations: {}, action: nil) ⇒ Object



603
604
605
606
607
608
609
610
611
612
# File 'app/models/concerns/issuable.rb', line 603

def to_hook_data(user, old_associations: {}, action: nil)
  changes = reportable_changes

  if old_associations.present?
    changes.merge!(hook_association_changes(old_associations))
    changes.merge!(hook_reviewer_changes(old_associations)) if allows_reviewers?
  end

  Gitlab::DataBuilder::Issuable.new(self).build(user: user, changes: changes, action: action)
end

#updated_tasksObject



683
684
685
686
687
688
# File 'app/models/concerns/issuable.rb', line 683

def updated_tasks
  Taskable.get_updated_tasks(
    old_content: previous_changes['description'].first,
    new_content: description
  )
end

#user_notes_countObject



533
534
535
536
537
538
539
540
541
# File 'app/models/concerns/issuable.rb', line 533

def user_notes_count
  if notes.loaded?
    # Use the in-memory association to select and count to avoid hitting the db
    notes.to_a.count { |note| !note.system? }
  else
    # do the count query
    notes.user.count
  end
end