Class: Issue

Defined Under Namespace

Classes: Metrics

Constant Summary collapse

DueDateStruct =
Struct.new(:title, :name).freeze
NoDueDate =
DueDateStruct.new('No Due Date', '0').freeze
AnyDueDate =
DueDateStruct.new('Any Due Date', '').freeze
Overdue =
DueDateStruct.new('Overdue', 'overdue').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

Constants included from ThrottledTouch

ThrottledTouch::TOUCH_INTERVAL

Constants included from RelativePositioning

RelativePositioning::IDEAL_DISTANCE, RelativePositioning::MAX_GAP, RelativePositioning::MAX_POSITION, RelativePositioning::MIN_GAP, RelativePositioning::MIN_POSITION, RelativePositioning::NoSpaceLeft, RelativePositioning::START_POSITION, RelativePositioning::STEPS

Constants included from Noteable

Noteable::MAX_NOTES_LIMIT

Constants included from Issuable

Issuable::DESCRIPTION_HTML_LENGTH_MAX, Issuable::DESCRIPTION_LENGTH_MAX, 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

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 Noteable

#system_note_timestamp

Attributes included from Importable

#imported, #importing

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ThrottledTouch

#touch

Methods included from TimeTrackable

#human_time_estimate, #human_total_time_spent, #spend_time, #time_estimate=, #total_time_spent

Methods included from RelativePositioning

#max_relative_position, #min_relative_position, #move_after, #move_before, #move_between, #move_sequence_after, #move_sequence_before, #move_to_end, #move_to_start, #next_relative_position, #prev_relative_position

Methods included from FasterCacheKeys

#cache_key

Methods included from Spammable

#allow_possible_spam?, #clear_spam_flags!, #invalidate_if_spam, #needs_recaptcha!, #recaptcha_error!, #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, #discussion_ids_relation, #discussion_notes, #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, #preloads_discussion_diff_highlighting?, #resolvable_discussions, #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, #first_contribution?, #label_names, #labels_array, #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, #wipless_title_changed

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

Methods included from Participable

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

Methods included from IidRoutes

#to_param

Methods included from AtomicInternalId

#internal_id_read_scope, #internal_id_scope_attrs, #internal_id_scope_usage

Methods inherited from ApplicationRecord

at_most, 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!, underscore, without_order

Class Method Details


198
199
200
# File 'app/models/issue.rb', line 198

def self.link_reference_pattern
  @link_reference_pattern ||= super("issues", Gitlab::Regex.issue)
end

.order_by_position_and_priority(with_cte: false) ⇒ Object

`with_cte` argument allows sorting when using CTE queries and prevents errors in postgres when using CTE search optimisation


237
238
239
240
241
242
# File 'app/models/issue.rb', line 237

def self.order_by_position_and_priority(with_cte: false)
  order_labels_priority(with_cte: with_cte)
    .reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
            Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
            "id DESC")
end

.project_foreign_keyObject


206
207
208
# File 'app/models/issue.rb', line 206

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.


191
192
193
194
195
196
# File 'app/models/issue.rb', line 191

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

.reference_prefixObject


184
185
186
# File 'app/models/issue.rb', line 184

def self.reference_prefix
  '#'
end

.reference_valid?(reference) ⇒ Boolean

Returns:

  • (Boolean)

202
203
204
# File 'app/models/issue.rb', line 202

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

.relative_positioning_parent_columnObject


180
181
182
# File 'app/models/issue.rb', line 180

def self.relative_positioning_parent_column
  :project_id
end

.relative_positioning_query_base(issue) ⇒ Object


176
177
178
# File 'app/models/issue.rb', line 176

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

.simple_sortsObject


210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/issue.rb', line 210

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_relative_position_asc.with_order_id_desc },
      'relative_position_asc' => -> { order_relative_position_asc.with_order_id_desc }
    }
  )
end

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


224
225
226
227
228
229
230
231
232
233
# File 'app/models/issue.rb', line 224

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_relative_position_asc.with_order_id_desc
  else
    super
  end
end

Instance Method Details

#as_json(options = {}) ⇒ Object


352
353
354
355
356
357
358
359
360
361
362
# File 'app/models/issue.rb', line 352

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


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

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

#can_be_worked_on?Boolean

Returns:

  • (Boolean)

329
330
331
# File 'app/models/issue.rb', line 329

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

#can_move?(user, to_project = nil) ⇒ Boolean

Returns:

  • (Boolean)

285
286
287
288
289
290
291
292
# File 'app/models/issue.rb', line 285

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?Boolean

Returns:

  • (Boolean)

347
348
349
350
# File 'app/models/issue.rb', line 347

def check_for_spam?
  publicly_visible? &&
    (title_changed? || description_changed? || confidential_changed?)
end

#design_collectionObject


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

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

#discussions_rendered_on_frontend?Boolean

Returns:

  • (Boolean)

368
369
370
# File 'app/models/issue.rb', line 368

def discussions_rendered_on_frontend?
  true
end

#duplicated?Boolean

Returns:

  • (Boolean)

281
282
283
# File 'app/models/issue.rb', line 281

def duplicated?
  !duplicated_to_id.nil?
end

#etag_caching_enabled?Boolean

Returns:

  • (Boolean)

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

def etag_caching_enabled?
  true
end

#from_service_desk?Boolean

Returns:

  • (Boolean)

398
399
400
# File 'app/models/issue.rb', line 398

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

Returns:

  • (Boolean)

266
267
268
269
270
# File 'app/models/issue.rb', line 266

def has_related_branch?
  project.repository.branch_names.any? do |branch|
    /\A#{iid}-(?!\d+-stable)/i =~ branch
  end
end

#hook_attrsObject


244
245
246
# File 'app/models/issue.rb', line 244

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

402
403
404
405
406
407
408
409
# File 'app/models/issue.rb', line 402

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

#labels_hook_attrsObject


382
383
384
# File 'app/models/issue.rb', line 382

def labels_hook_attrs
  labels.map(&:hook_attrs)
end

#merge_requests_count(user = nil) ⇒ Object

rubocop: enable CodeReuse/ServiceClass


378
379
380
# File 'app/models/issue.rb', line 378

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

#moved?Boolean

Returns:

  • (Boolean)

277
278
279
# File 'app/models/issue.rb', line 277

def moved?
  !moved_to_id.nil?
end

#previous_updated_atObject


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

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

311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'app/models/issue.rb', line 311

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'])
                     .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')

  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

#source_projectObject

To allow polymorphism with MergeRequest.


273
274
275
# File 'app/models/issue.rb', line 273

def source_project
  project
end

#suggested_branch_nameObject


255
256
257
258
259
260
261
262
# File 'app/models/issue.rb', line 255

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

#to_branch_nameObject


294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'app/models/issue.rb', line 294

def to_branch_name
  if self.confidential?
    "#{iid}-confidential-issue"
  else
    branch_name = "#{iid}-#{title.parameterize}"

    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[0, truncated_string.rindex("-")]
    end

    branch_name
  end
end

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

`from` argument can be a Namespace or Project.


249
250
251
252
253
# File 'app/models/issue.rb', line 249

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_cachesObject

rubocop: disable CodeReuse/ServiceClass


373
374
375
# File 'app/models/issue.rb', line 373

def update_project_counter_caches
  Projects::OpenIssuesCountService.new(project).refresh_cache
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)

335
336
337
338
339
340
341
342
343
344
345
# File 'app/models/issue.rb', line 335

def visible_to_user?(user = nil)
  return false unless project && project.feature_available?(:issues, user)

  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