Module: Issuable

Extended by:
ActiveSupport::Concern
Includes:
AfterCommitQueue, Awardable, CacheMarkdownField, ClosedAtFilterable, CreatedAtFilterable, Editable, Gitlab::SQL::Pattern, Importable, Mentionable, Milestoneable, Participable, Redactable, Sortable, 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/clone/base_service.rb,
app/services/issuable/bulk_update_service.rb,
app/services/issuable/clone/attributes_rewriter.rb,
app/services/issuable/common_system_notes_service.rb

Overview

Issuable concern

Contains common functionality shared between Issues and MergeRequests

Used by Issue, MergeRequest, Epic

Defined Under Namespace

Modules: Clone Classes: BulkUpdateService, CommonSystemNotesService, DestroyService

Constant Summary collapse

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

#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, #store_mentions!

Methods included from Participable

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

Instance Method Details

#assignee_listObject


500
501
502
# File 'app/models/concerns/issuable.rb', line 500

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

#assignee_or_author?(user) ⇒ Boolean

Returns:

  • (Boolean)

403
404
405
# File 'app/models/concerns/issuable.rb', line 403

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

#assignee_username_listObject


504
505
506
# File 'app/models/concerns/issuable.rb', line 504

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

#can_assign_epic?(user) ⇒ Boolean

Returns:

  • (Boolean)

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

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)

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

def can_move?(*)
  false
end

#card_attributesObject

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


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

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

#created_hours_agoObject


411
412
413
# File 'app/models/concerns/issuable.rb', line 411

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

#ensure_metricsObject


547
548
549
# File 'app/models/concerns/issuable.rb', line 547

def ensure_metrics
  self.metrics || create_metrics
end

#first_contribution?Boolean

Override in issuable specialization

Returns:

  • (Boolean)

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

def first_contribution?
  false
end

#label_namesObject


478
479
480
# File 'app/models/concerns/issuable.rb', line 478

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

#labels_arrayObject


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

def labels_array
  labels.to_a
end

#new?Boolean

Returns:

  • (Boolean)

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

def new?
  created_hours_ago < 24
end

#notes_with_associationsObject


508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'app/models/concerns/issuable.rb', line 508

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?

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

#open?Boolean

Returns:

  • (Boolean)

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

def open?
  opened?
end

#overdue?Boolean

Returns:

  • (Boolean)

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

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

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

#resource_parentObject


399
400
401
# File 'app/models/concerns/issuable.rb', line 399

def resource_parent
  project
end

#stateObject


391
392
393
# File 'app/models/concerns/issuable.rb', line 391

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

#state=(value) ⇒ Object


395
396
397
# File 'app/models/concerns/issuable.rb', line 395

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

#subscribed_without_subscriptions?(user, project) ⇒ Boolean

Returns:

  • (Boolean)

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

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

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

def to_ability_name
  self.class.to_ability_name
end

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


447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'app/models/concerns/issuable.rb', line 447

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

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

    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 self.respond_to?(:total_time_spent)
      old_total_time_spent = old_associations.fetch(:total_time_spent, total_time_spent)

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

  Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end

#today?Boolean

Returns:

  • (Boolean)

407
408
409
# File 'app/models/concerns/issuable.rb', line 407

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

#updated_tasksObject


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

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

#user_notes_countObject


429
430
431
432
433
434
435
436
437
# File 'app/models/concerns/issuable.rb', line 429

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

#wipless_title_changed(old_title) ⇒ Object

Overridden in MergeRequest


554
555
556
# File 'app/models/concerns/issuable.rb', line 554

def wipless_title_changed(old_title)
  old_title != title
end