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/clone/base_service.rb,
app/services/issuable/bulk_update_service.rb,
app/services/issuable/callbacks/milestone.rb,
app/services/issuable/callbacks/description.rb,
app/workers/issuable/create_reminder_worker.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, Clone, ImportCsv Classes: BulkUpdateService, CommonSystemNotesService, CreateReminderWorker, 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, Taskable::ITEM_PATTERN_UNTRUSTED, Taskable::REGEX

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

get_tasks, get_updated_tasks, #task_completion_status, #task_list_items, #task_status, #task_status_short, #tasks, #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_project_users, #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_after_commit?, #updated_cached_html_for

Methods included from Gitlab::SQL::Pattern

split_query_to_search_terms

Instance Method Details

#allows_scoped_labels?Boolean

Returns:

  • (Boolean)


603
604
605
# File 'app/models/concerns/issuable.rb', line 603

def allows_scoped_labels?
  false
end

#assignee?(user) ⇒ Boolean

Returns:

  • (Boolean)


499
500
501
502
503
504
505
506
# File 'app/models/concerns/issuable.rb', line 499

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



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

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

#assignee_or_author?(user) ⇒ Boolean

Returns:

  • (Boolean)


495
496
497
# File 'app/models/concerns/issuable.rb', line 495

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

#assignee_username_listObject



629
630
631
# File 'app/models/concerns/issuable.rb', line 629

def assignee_username_list
  assignees.map(&:username).to_sentence
end

#can_assign_epic?(user) ⇒ Boolean

Returns:

  • (Boolean)


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

def can_assign_epic?(user)
  false
end

#can_move?Boolean

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

Should be overridden if issuable can be moved.

Returns:

  • (Boolean)


665
666
667
# File 'app/models/concerns/issuable.rb', line 665

def can_move?(*)
  false
end

#card_attributesObject

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



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

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

#draftless_title_changed(old_title) ⇒ Object

Overridden in MergeRequest



679
680
681
# File 'app/models/concerns/issuable.rb', line 679

def draftless_title_changed(old_title)
  old_title != title
end

#first_contribution?Boolean

Override in issuable specialization

Returns:

  • (Boolean)


672
673
674
# File 'app/models/concerns/issuable.rb', line 672

def first_contribution?
  false
end

#hook_association_changes(old_associations) ⇒ Object

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



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'app/models/concerns/issuable.rb', line 537

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 self.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

rubocop:enable Metrics/PerceivedComplexity



569
570
571
572
573
574
575
576
577
578
# File 'app/models/concerns/issuable.rb', line 569

def hook_reviewer_changes(old_associations)
  changes = {}
  old_reviewers = old_associations.fetch(:reviewers, reviewers)

  if old_reviewers != reviewers
    changes[:reviewers] = [old_reviewers.map(&:hook_attrs), reviewers.map(&:hook_attrs)]
  end

  changes
end

#label_namesObject



595
596
597
# File 'app/models/concerns/issuable.rb', line 595

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

#labels_arrayObject



591
592
593
# File 'app/models/concerns/issuable.rb', line 591

def labels_array
  labels.to_a
end

#labels_hook_attrsObject



599
600
601
# File 'app/models/concerns/issuable.rb', line 599

def labels_hook_attrs
  labels.map(&:hook_attrs)
end

#notes_with_associationsObject



633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'app/models/concerns/issuable.rb', line 633

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?
  includes << :project unless notes.projects_loaded?
  includes << :system_note_metadata unless notes.

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

#old_assignees(assoc) ⇒ Object



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

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

#old_escalation_status(assoc) ⇒ Object



712
713
714
# File 'app/models/concerns/issuable.rb', line 712

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

#old_labels(assoc) ⇒ Object



700
701
702
# File 'app/models/concerns/issuable.rb', line 700

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

#old_severity(assoc) ⇒ Object



704
705
706
# File 'app/models/concerns/issuable.rb', line 704

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

#old_target_branch(assoc) ⇒ Object



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

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

#old_time_change(assoc) ⇒ Object



720
721
722
# File 'app/models/concerns/issuable.rb', line 720

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

#old_total_time_spent(assoc) ⇒ Object



716
717
718
# File 'app/models/concerns/issuable.rb', line 716

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

#open?Boolean

Returns:

  • (Boolean)


508
509
510
# File 'app/models/concerns/issuable.rb', line 508

def open?
  opened?
end

#overdue?Boolean

Returns:

  • (Boolean)


512
513
514
515
516
# File 'app/models/concerns/issuable.rb', line 512

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

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

#read_ability_for(participable_source) ⇒ Object



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

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



491
492
493
# File 'app/models/concerns/issuable.rb', line 491

def resource_parent
  project
end

#stateObject



483
484
485
# File 'app/models/concerns/issuable.rb', line 483

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

#state=(value) ⇒ Object



487
488
489
# File 'app/models/concerns/issuable.rb', line 487

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

#subscribed_without_subscriptions?(user, project) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#supports_health_status?Boolean

Returns:

  • (Boolean)


692
693
694
# File 'app/models/concerns/issuable.rb', line 692

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"


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

def to_ability_name
  self.class.to_ability_name
end

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



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

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



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

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

#user_notes_countObject



518
519
520
521
522
523
524
525
526
# File 'app/models/concerns/issuable.rb', line 518

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