Module: Issuable

Extended by:
ActiveSupport::Concern
Includes:
AfterCommitQueue, Awardable, CacheMarkdownField, ClosedAtFilterable, CreatedAtFilterable, Editable, Gitlab::SQL::Pattern, Importable, Mentionable, Milestoneable, Participable, Redactable, Sortable, SortableTitle, StripAttribute, Subscribable, Taskable, UpdatedAtFilterable, VersionedDescription
Included in:
Issue, MergeRequest
Defined in:
app/models/concerns/issuable.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/export_csv/base_service.rb,
app/services/issuable/import_csv/base_service.rb,
app/services/issuable/clone/attributes_rewriter.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 follows the rules specified in the specs. See spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb

Defined Under Namespace

Modules: Clone, ExportCsv, ImportCsv Classes: BulkUpdateService, CommonSystemNotesService, DestroyLabelLinksService, DestroyService, 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
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

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_WORD

Instance Attribute Summary

Attributes included from Importable

#imported, #importing

Instance Method Summary collapse

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from Editable

#edited?, #last_edited_by

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?, #parent_user, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #skip_project_check?, #store_mentions!, #updated_cached_html_for

Instance Method Details

#assignee_listObject


556
557
558
# File 'app/models/concerns/issuable.rb', line 556

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

#assignee_or_author?(user) ⇒ Boolean

Returns:

  • (Boolean)

431
432
433
# File 'app/models/concerns/issuable.rb', line 431

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

#assignee_username_listObject


560
561
562
# File 'app/models/concerns/issuable.rb', line 560

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

#can_assign_epic?(user) ⇒ Boolean

Returns:

  • (Boolean)

471
472
473
# File 'app/models/concerns/issuable.rb', line 471

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)

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

def can_move?(*)
  false
end

#card_attributesObject

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


549
550
551
552
553
554
# File 'app/models/concerns/issuable.rb', line 549

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

#created_hours_agoObject


439
440
441
# File 'app/models/concerns/issuable.rb', line 439

def created_hours_ago
  (Time.now.utc.to_i - created_at.utc.to_i) / 3600
end

#draftless_title_changed(old_title) ⇒ Object

Overridden in MergeRequest


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

def draftless_title_changed(old_title)
  old_title != title
end

#ensure_metricsObject


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

def ensure_metrics
  self.metrics || create_metrics
end

#first_contribution?Boolean

Override in issuable specialization

Returns:

  • (Boolean)

601
602
603
# File 'app/models/concerns/issuable.rb', line 601

def first_contribution?
  false
end

#hook_association_changes(old_associations) ⇒ Object


475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'app/models/concerns/issuable.rb', line 475

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

#label_namesObject


530
531
532
# File 'app/models/concerns/issuable.rb', line 530

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

#labels_arrayObject


526
527
528
# File 'app/models/concerns/issuable.rb', line 526

def labels_array
  labels.to_a
end

#labels_hook_attrsObject


534
535
536
# File 'app/models/concerns/issuable.rb', line 534

def labels_hook_attrs
  labels.map(&:hook_attrs)
end

#new?Boolean

Returns:

  • (Boolean)

443
444
445
# File 'app/models/concerns/issuable.rb', line 443

def new?
  created_hours_ago < 24
end

#notes_with_associationsObject


564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'app/models/concerns/issuable.rb', line 564

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)

447
448
449
# File 'app/models/concerns/issuable.rb', line 447

def open?
  opened?
end

#overdue?Boolean

Returns:

  • (Boolean)

451
452
453
454
455
# File 'app/models/concerns/issuable.rb', line 451

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

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

#resource_parentObject


427
428
429
# File 'app/models/concerns/issuable.rb', line 427

def resource_parent
  project
end

#stateObject


419
420
421
# File 'app/models/concerns/issuable.rb', line 419

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

#state=(value) ⇒ Object


423
424
425
# File 'app/models/concerns/issuable.rb', line 423

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

#subscribed_without_subscriptions?(user, project) ⇒ Boolean

Returns:

  • (Boolean)

467
468
469
# File 'app/models/concerns/issuable.rb', line 467

def subscribed_without_subscriptions?(user, project)
  participant?(user)
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"

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

def to_ability_name
  self.class.to_ability_name
end

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


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

def to_hook_data(user, old_associations: {})
  changes = previous_changes

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

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

#today?Boolean

Returns:

  • (Boolean)

435
436
437
# File 'app/models/concerns/issuable.rb', line 435

def today?
  Date.today == created_at.to_date
end

#updated_tasksObject


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

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

#user_notes_countObject


457
458
459
460
461
462
463
464
465
# File 'app/models/concerns/issuable.rb', line 457

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