Module: Issuable

Extended by:
ActiveSupport::Concern
Includes:
AfterCommitQueue, Awardable, CacheMarkdownField, ClosedAtFilterable, CreatedAtFilterable, Editable, Exportable, Gitlab::SQL::Pattern, Importable, Mentionable, Milestoneable, Participable, Redactable, 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/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/import_csv/base_service.rb,
app/services/issuable/discussions_list_service.rb,
app/workers/issuable/label_links_destroy_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, DestroyLabelLinksService, DestroyService, DiscussionsListService, LabelLinksDestroyWorker, ProcessAssignees

Constant Summary collapse

TITLE_LENGTH_MAX =
255
TITLE_HTML_LENGTH_MAX =
800
DESCRIPTION_LENGTH_MAX =
1.megabyte
DESCRIPTION_HTML_LENGTH_MAX =
5.megabytes
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 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

#imported, #importing

Attributes included from CacheMarkdownField

#skip_markdown_cache_validation

Instance Method Summary collapse

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, #local_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!, #updated_cached_html_for

Methods included from Gitlab::SQL::Pattern

split_query_to_search_terms

Instance Method Details

#allows_scoped_labels?Boolean

Returns:

  • (Boolean)


589
590
591
# File 'app/models/concerns/issuable.rb', line 589

def allows_scoped_labels?
  false
end

#assignee?(user) ⇒ Boolean

Returns:

  • (Boolean)


477
478
479
480
481
482
483
484
# File 'app/models/concerns/issuable.rb', line 477

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



611
612
613
# File 'app/models/concerns/issuable.rb', line 611

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

#assignee_or_author?(user) ⇒ Boolean

Returns:

  • (Boolean)


473
474
475
# File 'app/models/concerns/issuable.rb', line 473

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

#assignee_username_listObject



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

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

#can_assign_epic?(user) ⇒ Boolean

Returns:

  • (Boolean)


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

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)


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

def can_move?(*)
  false
end

#card_attributesObject

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



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

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

#draftless_title_changed(old_title) ⇒ Object

Overridden in MergeRequest



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

def draftless_title_changed(old_title)
  old_title != title
end

#first_contribution?Boolean

Override in issuable specialization

Returns:

  • (Boolean)


658
659
660
# File 'app/models/concerns/issuable.rb', line 658

def first_contribution?
  false
end

#hook_association_changes(old_associations) ⇒ Object



514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'app/models/concerns/issuable.rb', line 514

def hook_association_changes(old_associations)
  changes = {}

  old_labels = old_associations.fetch(:labels, labels)
  old_assignees = old_associations.fetch(:assignees, assignees)
  old_severity = old_associations.fetch(:severity, severity)

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

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

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

  if supports_escalation? && escalation_status
    current_escalation_status = escalation_status.status_name
    old_escalation_status = old_associations.fetch(:escalation_status, current_escalation_status)

    if old_escalation_status != current_escalation_status
      changes[:escalation_status] = [old_escalation_status, current_escalation_status]
    end
  end

  if self.respond_to?(:total_time_spent)
    old_total_time_spent = old_associations.fetch(:total_time_spent, total_time_spent)
    old_time_change = old_associations.fetch(:time_change, time_change)

    if old_total_time_spent != total_time_spent
      changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
      changes[:time_change] = [old_time_change, time_change]
    end
  end

  changes
end

#hook_reviewer_changes(old_associations) ⇒ Object



555
556
557
558
559
560
561
562
563
564
# File 'app/models/concerns/issuable.rb', line 555

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



581
582
583
# File 'app/models/concerns/issuable.rb', line 581

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

#labels_arrayObject



577
578
579
# File 'app/models/concerns/issuable.rb', line 577

def labels_array
  labels.to_a
end

#labels_hook_attrsObject



585
586
587
# File 'app/models/concerns/issuable.rb', line 585

def labels_hook_attrs
  labels.map(&:hook_attrs)
end

#notes_with_associationsObject



619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
# File 'app/models/concerns/issuable.rb', line 619

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

#open?Boolean

Returns:

  • (Boolean)


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

def open?
  opened?
end

#overdue?Boolean

Returns:

  • (Boolean)


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

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

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

#read_ability_for(participable_source) ⇒ Object



669
670
671
672
673
674
675
676
# File 'app/models/concerns/issuable.rb', line 669

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



469
470
471
# File 'app/models/concerns/issuable.rb', line 469

def resource_parent
  project
end

#stateObject



461
462
463
# File 'app/models/concerns/issuable.rb', line 461

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

#state=(value) ⇒ Object



465
466
467
# File 'app/models/concerns/issuable.rb', line 465

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

#subscribed_without_subscriptions?(user, project) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#supports_health_status?Boolean

Returns:

  • (Boolean)


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

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"


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

def to_ability_name
  self.class.to_ability_name
end

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



566
567
568
569
570
571
572
573
574
575
# File 'app/models/concerns/issuable.rb', line 566

def to_hook_data(user, old_associations: {})
  changes = previous_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)
end

#updated_tasksObject



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

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

#user_notes_countObject



496
497
498
499
500
501
502
503
504
# File 'app/models/concerns/issuable.rb', line 496

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