Class: Issue
- Inherits:
-
ApplicationRecord
show all
- Extended by:
- Gitlab::Utils::Override
- Includes:
- AtomicInternalId, EachBatch, FasterCacheKeys, FromUnion, IdInOrdered, IgnorableColumns, IidRoutes, Issuable, IssueAvailableFeatures, LabelEventable, MilestoneEventable, Noteable, PgFullTextSearchable, Presentable, Referable, RelativePositioning, Spammable, StateEventable, ThrottledTouch, TimeTrackable, Todoable, WhereComposite
- Defined in:
- app/models/issue.rb
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
- SORTING_PREFERENCE_FIELD =
:issues_sort
- TYPES_FOR_LIST =
Types of issues that should be displayed on lists across the app for example, project issues list, group issues list and issue boards. Some issue types, like test cases, should be hidden by default.
%w(issue incident).freeze
PgFullTextSearchable::LONG_WORDS_REGEX, PgFullTextSearchable::TEXT_SEARCH_DICTIONARY, PgFullTextSearchable::TSVECTOR_MAX_LENGTH
ThrottledTouch::TOUCH_INTERVAL
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::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
CacheMarkdownField::INVALIDATED_BY
Constants included
from Redactable
Redactable::UNSUBSCRIBE_PATTERN
Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_WORD
AtomicInternalId::MissingValueError
ApplicationRecord::MAX_PLUCK
Instance Attribute Summary
Attributes included from Noteable
#system_note_timestamp
Attributes included from Importable
#imported, #importing
Class Method Summary
collapse
Instance Method Summary
collapse
extended, extensions, included, method_added, override, prepended, queue_verification, verify!
#update_search_data!
#issue_type_supports?
#present
#touch
#human_time_change, #human_time_estimate, #human_total_time_spent, #spend_time, #time_change, #time_estimate=, #total_time_spent
#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
range
#cache_key
Methods included from Spammable
#allow_possible_spam?, #clear_spam_flags!, #invalidate_if_spam, #needs_recaptcha!, #recaptcha_error!, #render_recaptcha?, #spam, #spam!, #spam_description, #spam_title, #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, #capped_notes_count, #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, #expire_note_etag_cache, #grouped_diff_discussions, #has_any_diff_note_positions?, #human_class_name, #lockable?, #note_etag_key, #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
#assignee_list, #assignee_or_author?, #assignee_username_list, #can_assign_epic?, #card_attributes, #created_hours_ago, #draftless_title_changed, #first_contribution?, #hook_association_changes, #label_names, #labels_array, #labels_hook_attrs, #new?, #notes_with_associations, #open?, #overdue?, #resource_parent, #state, #state=, #subscribed_without_subscriptions?, #to_ability_name, #to_hook_data, #today?, #updated_tasks, #user_notes_count
#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?
#strip_attributes!
#lazy_subscription, #set_subscription, #subscribe, #subscribed?, #subscribed_without_subscriptions?, #subscribers, #toggle_subscription, #unsubscribe
#milestone_available?, #supports_milestone?
#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
#participant?, #participants, #visible_participants
#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?, #parent_user, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #skip_project_check?, #store_mentions!, #updated_cached_html_for
Methods included from IidRoutes
#to_param
group_init, #internal_id_read_scope, #internal_id_scope_attrs, #internal_id_scope_usage, project_init, scope_attrs, scope_usage
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
#serializable_hash
Class Method Details
.column_order_id_asc ⇒ Object
.column_order_relative_position ⇒ Object
341
342
343
344
345
346
347
348
349
350
351
|
# File 'app/models/issue.rb', line 341
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,
reversed_order_expression: Issue.arel_table[:relative_position].desc.nulls_last,
order_direction: :asc,
nullable: :nulls_last,
distinct: false
)
end
|
.link_reference_pattern ⇒ Object
296
297
298
|
# File 'app/models/issue.rb', line 296
def self.link_reference_pattern
@link_reference_pattern ||= super("issues", Gitlab::Regex.issue)
end
|
.order_by_relative_position ⇒ Object
337
338
339
|
# File 'app/models/issue.rb', line 337
def self.order_by_relative_position
reorder(Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_id_asc]))
end
|
.order_upvotes_asc ⇒ Object
237
238
239
|
# File 'app/models/issue.rb', line 237
def order_upvotes_asc
reorder(upvotes_count: :asc)
end
|
.order_upvotes_desc ⇒ Object
232
233
234
|
# File 'app/models/issue.rb', line 232
def order_upvotes_desc
reorder(upvotes_count: :desc)
end
|
.pg_full_text_search(search_term) ⇒ Object
242
243
244
|
# File 'app/models/issue.rb', line 242
def pg_full_text_search(search_term)
super.where('issue_search_data.project_id = issues.project_id')
end
|
.project_foreign_key ⇒ Object
304
305
306
|
# File 'app/models/issue.rb', line 304
def self.project_foreign_key
'project_id'
end
|
.reference_pattern ⇒ Object
Pattern used to extract `#123` issue references from text
This pattern supports cross-project references.
289
290
291
292
293
294
|
# File 'app/models/issue.rb', line 289
def self.reference_pattern
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}#{Gitlab::Regex.issue}
}x
end
|
.reference_prefix ⇒ Object
282
283
284
|
# File 'app/models/issue.rb', line 282
def self.reference_prefix
'#'
end
|
.reference_valid?(reference) ⇒ Boolean
300
301
302
|
# File 'app/models/issue.rb', line 300
def self.reference_valid?(reference)
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
|
.relative_positioning_parent_column ⇒ Object
278
279
280
|
# File 'app/models/issue.rb', line 278
def self.relative_positioning_parent_column
:project_id
end
|
.relative_positioning_query_base(issue) ⇒ Object
274
275
276
|
# File 'app/models/issue.rb', line 274
def self.relative_positioning_query_base(issue)
in_projects(issue.relative_positioning_parent_projects)
end
|
.simple_sorts ⇒ Object
308
309
310
311
312
313
314
315
316
317
318
319
320
|
# File 'app/models/issue.rb', line 308
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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
|
# File 'app/models/issue.rb', line 322
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.with_order_id_desc
when 'severity_desc' then order_severity_desc.with_order_id_desc
when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc
when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc
else
super
end
end
|
.to_branch_name(*args) ⇒ Object
360
361
362
363
364
365
366
367
368
369
370
371
372
373
|
# File 'app/models/issue.rb', line 360
def self.to_branch_name(*args)
branch_name = args.map(&:to_s).each_with_index.map do |arg, i|
arg.parameterize(preserve_case: i == 0).presence
end.compact.join('-')
if branch_name.length > 100
truncated_string = branch_name[0, 100]
branch_name = truncated_string.sub(/-[^-]*\Z/, '')
end
branch_name
end
|
Instance Method Details
#as_json(options = {}) ⇒ Object
497
498
499
500
501
502
503
504
505
506
507
|
# File 'app/models/issue.rb', line 497
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
531
532
533
|
# File 'app/models/issue.rb', line 531
def banzai_render_context(field)
super.merge(label_url_method: :project_issues_url)
end
|
#blocked_for_repositioning? ⇒ Boolean
383
384
385
|
# File 'app/models/issue.rb', line 383
def blocked_for_repositioning?
resource_parent.root_namespace&.issue_repositioning_disabled?
end
|
#can_be_worked_on? ⇒ Boolean
469
470
471
|
# File 'app/models/issue.rb', line 469
def can_be_worked_on?
!self.closed? && !self.project.forked?
end
|
#can_move?(user, to_project = nil) ⇒ Boolean
Also known as:
can_clone?
429
430
431
432
433
434
435
436
|
# File 'app/models/issue.rb', line 429
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_for_spam?(user:) ⇒ Boolean
485
486
487
488
489
490
491
492
493
494
495
|
# File 'app/models/issue.rb', line 485
def check_for_spam?(user:)
return true if user.support_bot? && spammable_attribute_changed?
return false unless publicly_visible?
spammable_attribute_changed?
end
|
#check_repositioning_allowed! ⇒ Object
377
378
379
380
381
|
# File 'app/models/issue.rb', line 377
def check_repositioning_allowed!
if blocked_for_repositioning?
raise ::Gitlab::RelativePositioning::IssuePositioningDisabled, "Issue relative position changes temporarily disabled."
end
end
|
#clear_closure_reason_references ⇒ Object
424
425
426
427
|
# File 'app/models/issue.rb', line 424
def clear_closure_reason_references
self.moved_to_id = nil
self.duplicated_to_id = nil
end
|
#design_collection ⇒ Object
#discussions_rendered_on_frontend? ⇒ Boolean
513
514
515
|
# File 'app/models/issue.rb', line 513
def discussions_rendered_on_frontend?
true
end
|
#duplicated? ⇒ Boolean
420
421
422
|
# File 'app/models/issue.rb', line 420
def duplicated?
!duplicated_to_id.nil?
end
|
#email_participants_emails ⇒ Object
568
569
570
|
# File 'app/models/issue.rb', line 568
def email_participants_emails
issue_email_participants.pluck(:email)
end
|
#email_participants_emails_downcase ⇒ Object
572
573
574
|
# File 'app/models/issue.rb', line 572
def email_participants_emails_downcase
issue_email_participants.pluck(IssueEmailParticipant.arel_table[:email].lower)
end
|
#etag_caching_enabled? ⇒ Boolean
509
510
511
|
# File 'app/models/issue.rb', line 509
def etag_caching_enabled?
true
end
|
#from_service_desk? ⇒ Boolean
539
540
541
|
# File 'app/models/issue.rb', line 539
def from_service_desk?
author.id == User.support_bot.id
end
|
Returns boolean if a related branch exists for the current issue ignores merge requests branchs
405
406
407
408
409
|
# File 'app/models/issue.rb', line 405
def has_related_branch?
project.repository.branch_names.any? do |branch|
/\A#{iid}-(?!\d+-stable)/i =~ branch
end
end
|
#hidden? ⇒ Boolean
606
607
608
|
# File 'app/models/issue.rb', line 606
def hidden?
author&.banned?
end
|
#issue_assignee_user_ids ⇒ Object
576
577
578
|
# File 'app/models/issue.rb', line 576
def issue_assignee_user_ids
issue_assignees.pluck(:user_id)
end
|
#issue_link_type ⇒ Object
543
544
545
546
547
548
549
550
|
# File 'app/models/issue.rb', line 543
def issue_link_type
return unless respond_to?(:issue_link_type_value) && respond_to?(:issue_link_source_id)
type = IssueLink.link_types.key(issue_link_type_value) || IssueLink::TYPE_RELATES_TO
return type if issue_link_source_id == id
IssueLink.inverse_link_type(type)
end
|
#merge_requests_count(user = nil) ⇒ Object
rubocop: enable CodeReuse/ServiceClass
#moved? ⇒ Boolean
416
417
418
|
# File 'app/models/issue.rb', line 416
def moved?
!moved_to_id.nil?
end
|
#next_object_by_relative_position(ignoring: nil, order: :asc) ⇒ Object
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
|
# File 'app/models/issue.rb', line 247
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_at ⇒ Object
527
528
529
|
# File 'app/models/issue.rb', line 527
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
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
|
# File 'app/models/issue.rb', line 590
def readable_by?(user)
if user.can_read_all_resources?
true
elsif project.personal? && project.team.owner?(user)
true
elsif confidential? && !assignee_or_author?(user)
project.team.member?(user, Gitlab::Access::REPORTER)
elsif hidden?
false
elsif project.public? || (project.internal? && !user.external?)
project.feature_available?(:issues, user)
else
project.team.member?(user)
end
end
|
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
|
# File 'app/models/issue.rb', line 447
def related_issues(current_user, preload: nil)
related_issues = ::Issue
.select(['issues.*', 'issue_links.id AS issue_link_id',
'issue_links.link_type as issue_link_type_value',
'issue_links.target_id as issue_link_source_id',
'issue_links.created_at as issue_link_created_at',
'issue_links.updated_at as issue_link_updated_at'])
.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?
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_projects ⇒ Object
270
271
272
|
# File 'app/models/issue.rb', line 270
def relative_positioning_parent_projects
project.group&.root_ancestor&.all_projects&.select(:id) || Project.id_in(project).select(:id)
end
|
#relocation_target ⇒ Object
552
553
554
|
# File 'app/models/issue.rb', line 552
def relocation_target
moved_to || duplicated_to
end
|
#source_project ⇒ Object
To allow polymorphism with MergeRequest.
412
413
414
|
# File 'app/models/issue.rb', line 412
def source_project
project
end
|
#suggested_branch_name ⇒ Object
394
395
396
397
398
399
400
401
|
# File 'app/models/issue.rb', line 394
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
|
#supports_assignee? ⇒ Boolean
556
557
558
|
# File 'app/models/issue.rb', line 556
def supports_assignee?
issue_type_supports?(:assignee)
end
|
#supports_move_and_clone? ⇒ Boolean
564
565
566
|
# File 'app/models/issue.rb', line 564
def supports_move_and_clone?
issue_type_supports?(:move_and_clone)
end
|
#supports_time_tracking? ⇒ Boolean
560
561
562
|
# File 'app/models/issue.rb', line 560
def supports_time_tracking?
issue_type_supports?(:time_tracking)
end
|
#to_branch_name ⇒ Object
439
440
441
442
443
444
445
|
# File 'app/models/issue.rb', line 439
def to_branch_name
if self.confidential?
"#{iid}-confidential-issue"
else
self.class.to_branch_name(iid, title)
end
end
|
#to_reference(from = nil, full: false) ⇒ Object
`from` argument can be a Namespace or Project.
388
389
390
391
392
|
# File 'app/models/issue.rb', line 388
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
"#{project.to_reference_base(from, full: full)}#{reference}"
end
|
#update_project_counter_caches ⇒ Object
rubocop: disable CodeReuse/ServiceClass
#update_upvotes_count ⇒ Object
580
581
582
583
|
# File 'app/models/issue.rb', line 580
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.
475
476
477
478
479
480
481
482
483
|
# File 'app/models/issue.rb', line 475
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 ⇒ Object
Necessary until all issues are backfilled and we add a NOT NULL constraint on the DB