Class: Issue

Direct Known Subclasses

WorkItem

Defined Under Namespace

Classes: Email, Metrics

Constant Summary collapse

DueDateStruct =
Struct.new(:title, :name).freeze
NoDueDate =
DueDateStruct.new('No Due Date', '0').freeze
AnyDueDate =
DueDateStruct.new('Any Due Date', 'any').freeze
Overdue =
DueDateStruct.new('Overdue', 'overdue').freeze
DueToday =
DueDateStruct.new('Due Today', 'today').freeze
DueTomorrow =
DueDateStruct.new('Due Tomorrow', 'tomorrow').freeze
DueThisWeek =
DueDateStruct.new('Due This Week', 'week').freeze
DueThisMonth =
DueDateStruct.new('Due This Month', 'month').freeze
DueNextMonthAndPreviousTwoWeeks =
DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
IssueTypeOutOfSyncError =
Class.new(StandardError)
SORTING_PREFERENCE_FIELD =
:issues_sort
MAX_BRANCH_TEMPLATE =
255
TYPES_FOR_LIST =

Types of issues that should be displayed on issue lists across the app for example, project issues list, group issues list, and issues dashboard.

This should be kept consistent with the enums used for the GraphQL issue list query in gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158

%w[issue incident test_case task objective key_result].freeze
TYPES_FOR_BOARD_LIST =

Types of issues that should be displayed on issue board lists

%w[issue incident].freeze
DEFAULT_ISSUE_TYPE =

This default came from the enum ‘issue_type` column. Defined as default in the DB

:issue

Constants included from PgFullTextSearchable

PgFullTextSearchable::LONG_WORDS_REGEX, PgFullTextSearchable::TEXT_SEARCH_DICTIONARY, PgFullTextSearchable::TSQUERY_DISALLOWED_CHARACTERS_REGEX, PgFullTextSearchable::TSVECTOR_MAX_LENGTH, PgFullTextSearchable::URL_SCHEME_REGEX

Constants included from ThrottledTouch

ThrottledTouch::TOUCH_INTERVAL

Constants included from Gitlab::RelativePositioning

Gitlab::RelativePositioning::IDEAL_DISTANCE, Gitlab::RelativePositioning::IllegalRange, Gitlab::RelativePositioning::InvalidPosition, Gitlab::RelativePositioning::IssuePositioningDisabled, Gitlab::RelativePositioning::MAX_GAP, Gitlab::RelativePositioning::MAX_POSITION, Gitlab::RelativePositioning::MIN_GAP, Gitlab::RelativePositioning::MIN_POSITION, Gitlab::RelativePositioning::NoSpaceLeft, Gitlab::RelativePositioning::START_POSITION, Gitlab::RelativePositioning::STEPS

Constants included from Noteable

Noteable::MAX_NOTES_LIMIT

Constants included from Issuable

Issuable::DESCRIPTION_HTML_LENGTH_MAX, Issuable::DESCRIPTION_LENGTH_MAX, Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS, Issuable::SEARCHABLE_FIELDS, Issuable::STATE_ID_MAP, Issuable::TITLE_HTML_LENGTH_MAX, Issuable::TITLE_LENGTH_MAX

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

Constants included from AtomicInternalId

AtomicInternalId::MissingValueError

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Constants included from ResetOnUnionError

ResetOnUnionError::MAX_RESET_PERIOD

Instance Attribute Summary

Attributes included from Noteable

#system_note_timestamp

Attributes included from Transitionable

#transitioning

Attributes included from Importable

#imported, #importing

Attributes included from CacheMarkdownField

#skip_markdown_cache_validation

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Utils::Override

extended, extensions, included, method_added, override, prepended, queue_verification, verify!

Methods included from PgFullTextSearchable

#update_search_data!

Methods included from IssueAvailableFeatures

#issue_type_supports?

Methods included from Presentable

#present

Methods included from ThrottledTouch

#touch

Methods included from TimeTrackable

#human_time_change, #human_time_estimate, #human_total_time_spent, #set_time_estimate_default_value, #spend_time, #time_change, #time_estimate=, #total_time_spent

Methods included from RelativePositioning

#exclude_self, #model_class, #move_after, #move_before, #move_between, #move_to_end, #move_to_start, mover, #relative_positioning_scoped_items, #reset_relative_position, #update_relative_siblings

Methods included from Gitlab::RelativePositioning

range

Methods included from FasterCacheKeys

#cache_key

Methods included from Spammable

#check_for_spam, #check_for_spam?, #clear_spam_flags!, #invalidate_if_spam, #needs_recaptcha!, #recaptcha_error!, #render_recaptcha?, #spam, #spam!, #spam_description, #spam_title, #spammable_attribute_changed?, #spammable_entity_type, #spammable_text, #submittable_as_spam?, #submittable_as_spam_by?, #unrecoverable_spam_error!

Methods included from Referable

#referable_inspect, #reference_link_text, #to_reference_base

Methods included from Noteable

#after_note_created, #after_note_destroyed, #base_class_name, #broadcast_notes_changed, #capped_notes_count, #commenters, #creatable_note_email_address, #discussion_ids_relation, #discussion_notes, #discussion_root_note_ids, #discussions, #discussions_can_be_resolved_by?, #discussions_resolvable?, #discussions_resolved?, #discussions_to_be_resolved, #grouped_diff_discussions, #has_any_diff_note_positions?, #human_class_name, #lockable?, #noteable_target_type_name, #preloads_discussion_diff_highlighting?, #resolvable_discussions, #supports_creating_notes_by_email?, #supports_discussions?, #supports_replying_to_individual_notes?, #supports_resolvable_notes?, #supports_suggestion?

Methods included from Issuable

#allows_scoped_labels?, #assignee?, #assignee_list, #assignee_or_author?, #assignee_username_list, #can_assign_epic?, #card_attributes, #draftless_title_changed, #first_contribution?, #hook_association_changes, #hook_reviewer_changes, #label_names, #labels_array, #labels_hook_attrs, #notes_with_associations, #open?, #overdue?, #read_ability_for, #state, #state=, #subscribed_without_subscriptions?, #supports_health_status?, #to_ability_name, #to_hook_data, #updated_tasks, #user_notes_count

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?, #subscribed_without_subscriptions?, #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?, #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

Methods included from IidRoutes

#to_param

Methods included from AtomicInternalId

group_init, #internal_id_read_scope, #internal_id_scope_attrs, #internal_id_scope_usage, namespace_init, project_init, scope_attrs, scope_usage

Methods inherited from ApplicationRecord

cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from SensitiveSerializableHash

#serializable_hash

Class Method Details

.alternative_reference_prefixObject

Alternative prefix for situations where the standard prefix would be interpreted as a comment, most notably to begin commit messages with (e.g. “GL-123: My commit”)



370
371
372
# File 'app/models/issue.rb', line 370

def self.alternative_reference_prefix
  'GL-'
end

.column_order_id_ascObject



443
444
445
446
447
448
# File 'app/models/issue.rb', line 443

def self.column_order_id_asc
  Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
    attribute_name: 'id',
    order_expression: arel_table[:id].asc
  )
end

.column_order_relative_positionObject



433
434
435
436
437
438
439
440
441
# File 'app/models/issue.rb', line 433

def self.column_order_relative_position
  Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
    attribute_name: 'relative_position',
    column_expression: arel_table[:relative_position],
    order_expression: Issue.arel_table[:relative_position].asc.nulls_last,
    nullable: :nulls_last,
    distinct: false
  )
end

.full_search(query, matched_columns: nil, use_minimum_char_limit: true) ⇒ Object



310
311
312
313
314
315
316
317
# File 'app/models/issue.rb', line 310

def full_search(query, matched_columns: nil, use_minimum_char_limit: true)
  return super if query.match?(IssuableFinder::FULL_TEXT_SEARCH_TERM_REGEX)

  super.where(
    'issues.title NOT SIMILAR TO :pattern OR issues.description NOT SIMILAR TO :pattern',
    pattern: IssuableFinder::FULL_TEXT_SEARCH_TERM_PATTERN
  )
end


386
387
388
# File 'app/models/issue.rb', line 386

def self.link_reference_pattern
  @link_reference_pattern ||= compose_link_reference_pattern(%r{issues(?:\/incident)?}, Gitlab::Regex.issue)
end

.order_by_relative_positionObject



429
430
431
# File 'app/models/issue.rb', line 429

def self.order_by_relative_position
  reorder(Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_id_asc]))
end

.order_upvotes_ascObject



305
306
307
# File 'app/models/issue.rb', line 305

def order_upvotes_asc
  reorder(upvotes_count: :asc)
end

.order_upvotes_descObject



300
301
302
# File 'app/models/issue.rb', line 300

def order_upvotes_desc
  reorder(upvotes_count: :desc)
end

.participant_includesObject



324
325
326
# File 'app/models/issue.rb', line 324

def self.participant_includes
  [:assignees] + super
end

.project_foreign_keyObject



394
395
396
# File 'app/models/issue.rb', line 394

def self.project_foreign_key
  'project_id'
end

.reference_patternObject

Pattern used to extract ‘#123` issue references from text

This pattern supports cross-project references.



377
378
379
380
381
382
383
384
# File 'app/models/issue.rb', line 377

def self.reference_pattern
  @reference_pattern ||= %r{
    (?:
      (#{Project.reference_pattern})?#{Regexp.escape(reference_prefix)} |
      #{Regexp.escape(alternative_reference_prefix)}
    )#{Gitlab::Regex.issue}
  }x
end

.reference_prefixObject



363
364
365
# File 'app/models/issue.rb', line 363

def self.reference_prefix
  '#'
end

.reference_valid?(reference) ⇒ Boolean

Returns:

  • (Boolean)


390
391
392
# File 'app/models/issue.rb', line 390

def self.reference_valid?(reference)
  reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end


319
320
321
# File 'app/models/issue.rb', line 319

def related_link_class
  IssueLink
end

.relative_positioning_parent_columnObject



359
360
361
# File 'app/models/issue.rb', line 359

def self.relative_positioning_parent_column
  :project_id
end

.relative_positioning_query_base(issue) ⇒ Object



355
356
357
# File 'app/models/issue.rb', line 355

def self.relative_positioning_query_base(issue)
  in_projects(issue.relative_positioning_parent_projects)
end

.simple_sortsObject



398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'app/models/issue.rb', line 398

def self.simple_sorts
  super.merge(
    {
      'closest_future_date' => -> { order_closest_future_date },
      'closest_future_date_asc' => -> { order_closest_future_date },
      'due_date' => -> { order_due_date_asc.with_order_id_desc },
      'due_date_asc' => -> { order_due_date_asc.with_order_id_desc },
      'due_date_desc' => -> { order_due_date_desc.with_order_id_desc },
      'relative_position' => -> { order_by_relative_position },
      'relative_position_asc' => -> { order_by_relative_position }
    }
  )
end

.sort_by_attribute(method, excluded_labels: []) ⇒ Object



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'app/models/issue.rb', line 412

def self.sort_by_attribute(method, excluded_labels: [])
  case method.to_s
  when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
  when 'due_date', 'due_date_asc'                       then order_due_date_asc.with_order_id_desc
  when 'due_date_desc'                                  then order_due_date_desc.with_order_id_desc
  when 'relative_position', 'relative_position_asc'     then order_by_relative_position
  when 'severity_asc'                                   then order_severity_asc
  when 'severity_desc'                                  then order_severity_desc
  when 'escalation_status_asc'                          then order_escalation_status_asc
  when 'escalation_status_desc'                         then order_escalation_status_desc
  when 'closed_at', 'closed_at_asc'                     then order_closed_at_asc
  when 'closed_at_desc'                                 then order_closed_at_desc
  else
    super
  end
end

.to_branch_name(id, title, project: nil) ⇒ Object



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

def self.to_branch_name(id, title, project: nil)
  params = {
    'id' => id.to_s.parameterize(preserve_case: true),
    'title' => title.to_s.parameterize
  }
  template = project&.issue_branch_template

  branch_name =
    if template.present?
      Gitlab::StringPlaceholderReplacer.replace_string_placeholders(template, /(#{params.keys.join('|')})/) do |arg|
        params[arg]
      end
    else
      params.values.select(&:present?).join('-')
    end

  if branch_name.length > 100
    truncated_string = branch_name[0, 100]
    # Delete everything dangling after the last hyphen so as not to risk
    # existence of unintended words in the branch name due to mid-word split.
    branch_name = truncated_string.sub(/-[^-]*\Z/, '')
  end

  branch_name
end

Instance Method Details

#allow_possible_spam?(user) ⇒ Boolean

Always enforce spam check for support bot but allow for other users when issue is not publicly visible

Returns:

  • (Boolean)


588
589
590
591
592
593
# File 'app/models/issue.rb', line 588

def allow_possible_spam?(user)
  return true if Gitlab::CurrentSettings.allow_possible_spam
  return false if user.support_bot?

  !publicly_visible?
end

#as_json(options = {}) ⇒ Object



599
600
601
602
603
604
605
606
607
608
609
# File 'app/models/issue.rb', line 599

def as_json(options = {})
  super(options).tap do |json|
    if options.key?(:labels)
      json[:labels] = labels.as_json(
        project: project,
        only: [:id, :title, :description, :color, :priority],
        methods: [:text_color]
      )
    end
  end
end

#banzai_render_context(field) ⇒ Object



637
638
639
# File 'app/models/issue.rb', line 637

def banzai_render_context(field)
  super.merge(label_url_method: :project_issues_url)
end

#blocked_for_repositioning?Boolean

Returns:

  • (Boolean)


484
485
486
# File 'app/models/issue.rb', line 484

def blocked_for_repositioning?
  resource_parent.root_namespace&.issue_repositioning_disabled?
end

#can_be_worked_on?Boolean

Returns:

  • (Boolean)


571
572
573
# File 'app/models/issue.rb', line 571

def can_be_worked_on?
  !self.closed? && !self.project.forked?
end

#can_move?(user, to_project = nil) ⇒ Boolean Also known as: can_clone?

Returns:

  • (Boolean)


528
529
530
531
532
533
534
535
# File 'app/models/issue.rb', line 528

def can_move?(user, to_project = nil)
  if to_project
    return false unless user.can?(:admin_issue, to_project)
  end

  !moved? && persisted? &&
    user.can?(:admin_issue, self.project)
end

#check_repositioning_allowed!Object

Temporary disable moving null elements because of performance problems For more information check gitlab.com/gitlab-com/gl-infra/production/-/issues/4321



478
479
480
481
482
# File 'app/models/issue.rb', line 478

def check_repositioning_allowed!
  if blocked_for_repositioning?
    raise ::Gitlab::RelativePositioning::IssuePositioningDisabled, "Issue relative position changes temporarily disabled."
  end
end

#clear_closure_reason_referencesObject



523
524
525
526
# File 'app/models/issue.rb', line 523

def clear_closure_reason_references
  self.moved_to_id = nil
  self.duplicated_to_id = nil
end

#design_collectionObject



641
642
643
# File 'app/models/issue.rb', line 641

def design_collection
  @design_collection ||= ::DesignManagement::DesignCollection.new(self)
end

#discussions_rendered_on_frontend?Boolean

Returns:

  • (Boolean)


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

def discussions_rendered_on_frontend?
  true
end

#duplicated?Boolean

Returns:

  • (Boolean)


519
520
521
# File 'app/models/issue.rb', line 519

def duplicated?
  !duplicated_to_id.nil?
end

#email_participants_emailsObject



675
676
677
# File 'app/models/issue.rb', line 675

def email_participants_emails
  issue_email_participants.pluck(:email)
end

#email_participants_emails_downcaseObject



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

def email_participants_emails_downcase
  issue_email_participants.pluck(IssueEmailParticipant.arel_table[:email].lower)
end

#expire_etag_cacheObject



719
720
721
722
723
724
725
726
# File 'app/models/issue.rb', line 719

def expire_etag_cache
  # We don't expire the cache for issues that don't have a project, since they are created at the group level
  # and they are only displayed in the new work item view that uses GraphQL subscriptions for real-time updates
  return unless project

  key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
  Gitlab::EtagCaching::Store.new.touch(key)
end

#from_service_desk?Boolean

Returns:

  • (Boolean)


645
646
647
# File 'app/models/issue.rb', line 645

def from_service_desk?
  author.id == Users::Internal.support_bot.id
end

#hidden?Boolean

Returns:

  • (Boolean)


715
716
717
# File 'app/models/issue.rb', line 715

def hidden?
  author&.banned?
end

#hook_attrsObject



758
759
760
# File 'app/models/issue.rb', line 758

def hook_attrs
  Gitlab::HookData::IssueBuilder.new(self).build
end

#issue_assignee_user_idsObject



683
684
685
# File 'app/models/issue.rb', line 683

def issue_assignee_user_ids
  issue_assignees.pluck(:user_id)
end


649
650
651
652
653
654
655
656
657
# File 'app/models/issue.rb', line 649

def issue_link_type
  link_class = self.class.related_link_class
  return unless respond_to?(:issue_link_type_value) && respond_to?(:issue_link_source_id)

  type = link_class.link_types.key(issue_link_type_value) || link_class::TYPE_RELATES_TO
  return type if issue_link_source_id == id

  link_class.inverse_link_type(type)
end

#issue_typeObject



748
749
750
# File 'app/models/issue.rb', line 748

def issue_type
  work_item_type_with_default.base_type
end

#linked_items_countObject



567
568
569
# File 'app/models/issue.rb', line 567

def linked_items_count
  related_issues(authorize: false).size
end

#merge_requests_count(user = nil) ⇒ Object

rubocop: enable CodeReuse/ServiceClass



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

def merge_requests_count(user = nil)
  ::MergeRequestsClosingIssues.count_for_issue(self.id, user)
end

#moved?Boolean

Returns:

  • (Boolean)


515
516
517
# File 'app/models/issue.rb', line 515

def moved?
  !moved_to_id.nil?
end

#next_object_by_relative_position(ignoring: nil, order: :asc) ⇒ Object



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/issue.rb', line 328

def next_object_by_relative_position(ignoring: nil, order: :asc)
  array_mapping_scope = -> (id_expression) do
    relation = Issue.where(Issue.arel_table[:project_id].eq(id_expression))

    if order == :asc
      relation.where(Issue.arel_table[:relative_position].gt(relative_position))
    else
      relation.where(Issue.arel_table[:relative_position].lt(relative_position))
    end
  end

  relation = Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
    scope: Issue.order(relative_position: order, id: order),
    array_scope: relative_positioning_parent_projects,
    array_mapping_scope: array_mapping_scope,
    finder_query: -> (_, id_expression) { Issue.where(Issue.arel_table[:id].eq(id_expression)) }
  ).execute

  relation = exclude_self(relation, excluded: ignoring) if ignoring.present?

  relation.take
end

#previous_updated_atObject



633
634
635
# File 'app/models/issue.rb', line 633

def previous_updated_at
  previous_changes['updated_at']&.first || updated_at
end

#readable_by?(user) ⇒ Boolean

Returns ‘true` if the given User can read the current Issue.

This method duplicates the same check of issue_policy.rb for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8 Make sure to sync this method with issue_policy.rb

Returns:

  • (Boolean)


697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
# File 'app/models/issue.rb', line 697

def readable_by?(user)
  if !project.issues_enabled?
    false
  elsif user.can_read_all_resources?
    true
  elsif project.personal? && project.team.owner?(user)
    true
  elsif confidential? && !assignee_or_author?(user)
    project.member?(user, Gitlab::Access::REPORTER)
  elsif hidden?
    false
  elsif project.public? || (project.internal? && !user.external?)
    project.feature_available?(:issues, user)
  else
    project.member?(user)
  end
end

#real_time_notes_enabled?Boolean

Returns:

  • (Boolean)


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

def real_time_notes_enabled?
  true
end


546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'app/models/issue.rb', line 546

def related_issues(current_user = nil, authorize: true, preload: nil)
  return [] if new_record?

  related_issues =
    linked_issues_select
      .joins("INNER JOIN issue_links ON
         (issue_links.source_id = issues.id AND issue_links.target_id = #{id})
         OR
         (issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
      .preload(preload)
      .reorder('issue_link_id')

  related_issues = yield related_issues if block_given?
  return related_issues unless authorize

  cross_project_filter = -> (issues) { issues.where(project: project) }
  Ability.issues_readable_by_user(related_issues,
    current_user,
    filters: { read_cross_project: cross_project_filter })
end

#relative_positioning_parent_projectsObject



351
352
353
# File 'app/models/issue.rb', line 351

def relative_positioning_parent_projects
  project.group&.root_ancestor&.all_projects&.select(:id) || Project.id_in(project).select(:id)
end

#relocation_targetObject



659
660
661
# File 'app/models/issue.rb', line 659

def relocation_target
  moved_to || duplicated_to
end

#resource_parentObject



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

def resource_parent
  project || namespace
end

#source_projectObject

To allow polymorphism with MergeRequest.



511
512
513
# File 'app/models/issue.rb', line 511

def source_project
  project
end

#suggested_branch_nameObject



495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'app/models/issue.rb', line 495

def suggested_branch_name
  return to_branch_name unless project.repository.branch_exists?(to_branch_name)

  start_counting_from = 2

  branch_name_generator = -> (counter) do
    suffix = counter > 5 ? SecureRandom.hex(8) : counter
    "#{to_branch_name}-#{suffix}"
  end

  Gitlab::Utils::Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
    project.repository.branch_exists?(suggested_branch_name)
  end
end

#supports_assignee?Boolean

Returns:

  • (Boolean)


663
664
665
# File 'app/models/issue.rb', line 663

def supports_assignee?
  work_item_type_with_default.supports_assignee?
end

#supports_confidentiality?Boolean

Returns:

  • (Boolean)


728
729
730
# File 'app/models/issue.rb', line 728

def supports_confidentiality?
  true
end

#supports_move_and_clone?Boolean

Returns:

  • (Boolean)


671
672
673
# File 'app/models/issue.rb', line 671

def supports_move_and_clone?
  issue_type_supports?(:move_and_clone)
end

#supports_recaptcha?Boolean

Returns:

  • (Boolean)


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

def supports_recaptcha?
  true
end

#supports_time_tracking?Boolean

Returns:

  • (Boolean)


667
668
669
# File 'app/models/issue.rb', line 667

def supports_time_tracking?
  issue_type_supports?(:time_tracking)
end

#to_branch_nameObject



538
539
540
541
542
543
544
# File 'app/models/issue.rb', line 538

def to_branch_name
  if self.confidential?
    "#{iid}-confidential-issue"
  else
    self.class.to_branch_name(iid, title, project: project)
  end
end

#to_reference(from = nil, full: false) ⇒ Object

‘from` argument can be a Namespace or Project.



489
490
491
492
493
# File 'app/models/issue.rb', line 489

def to_reference(from = nil, full: false)
  reference = "#{self.class.reference_prefix}#{iid}"

  "#{namespace.to_reference_base(from, full: full)}#{reference}"
end

#to_work_item_global_idObject

we want to have subscriptions working on work items only, legacy issues do not support graphql subscriptions, yet so we need sometimes GID of an issue instance to be represented as WorkItem GID. E.g. notes subscriptions.



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

def to_work_item_global_id
  ::Gitlab::GlobalId.as_global_id(id, model_name: WorkItem.name)
end

#unsubscribe_email_participant(email) ⇒ Object



752
753
754
755
756
# File 'app/models/issue.rb', line 752

def unsubscribe_email_participant(email)
  return if email.blank?

  issue_email_participants.find_by_email(email)&.destroy
end

#update_project_counter_cachesObject

rubocop: disable CodeReuse/ServiceClass



620
621
622
623
624
625
626
# File 'app/models/issue.rb', line 620

def update_project_counter_caches
  # TODO: Fix counter cache for issues in group
  # TODO: see https://gitlab.com/gitlab-org/gitlab/-/work_items/393125
  return unless project

  Projects::OpenIssuesCountService.new(project).refresh_cache
end

#update_upvotes_countObject



687
688
689
690
# File 'app/models/issue.rb', line 687

def update_upvotes_count
  self.lock!
  self.update_column(:upvotes_count, self.upvotes)
end

#visible_to_user?(user = nil) ⇒ Boolean

Returns ‘true` if the current issue can be viewed by either a logged in User or an anonymous user.

Returns:

  • (Boolean)


577
578
579
580
581
582
583
584
585
# File 'app/models/issue.rb', line 577

def visible_to_user?(user = nil)
  return publicly_visible? unless user

  return false unless readable_by?(user)

  user.can_read_all_resources? ||
    ::Gitlab::ExternalAuthorization.access_allowed?(
      user, project.external_authorization_classification_label)
end

#work_item_type_with_defaultObject

Persisted records will always have a work_item_type. This method is useful in places where we use a non persisted issue to perform feature checks



744
745
746
# File 'app/models/issue.rb', line 744

def work_item_type_with_default
  work_item_type || WorkItems::Type.default_by_type(DEFAULT_ISSUE_TYPE)
end