debian-mirror-gitlab/app/models/issue.rb

882 lines
32 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2014-09-02 18:07:02 +05:30
require 'carrierwave/orm/activerecord'
2019-07-07 11:18:12 +05:30
class Issue < ApplicationRecord
2018-05-09 12:01:36 +05:30
include AtomicInternalId
2018-11-08 19:23:39 +05:30
include IidRoutes
2015-09-11 14:41:01 +05:30
include Issuable
2017-08-17 22:00:37 +05:30
include Noteable
2015-09-11 14:41:01 +05:30
include Referable
2016-09-13 17:45:13 +05:30
include Spammable
include FasterCacheKeys
2017-08-17 22:00:37 +05:30
include RelativePositioning
2018-03-17 18:26:18 +05:30
include TimeTrackable
include ThrottledTouch
2018-11-18 11:00:15 +05:30
include LabelEventable
2020-01-01 13:55:28 +05:30
include IgnorableColumns
2020-04-08 14:13:33 +05:30
include MilestoneEventable
2020-04-22 19:07:51 +05:30
include WhereComposite
2020-05-24 23:13:21 +05:30
include StateEventable
2020-11-24 15:15:51 +05:30
include IdInOrdered
2021-01-03 14:25:43 +05:30
include Presentable
include IssueAvailableFeatures
2021-01-29 00:20:46 +05:30
include Todoable
2021-02-22 17:27:13 +05:30
include FromUnion
2021-09-04 01:27:46 +05:30
include EachBatch
2022-05-07 20:08:51 +05:30
include PgFullTextSearchable
2017-09-10 17:25:29 +05:30
2021-04-29 21:17:54 +05:30
extend ::Gitlab::Utils::Override
2018-11-08 19:23:39 +05:30
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
2022-04-04 11:22:00 +05:30
AnyDueDate = DueDateStruct.new('Any Due Date', 'any').freeze
2018-11-08 19:23:39 +05:30
Overdue = DueDateStruct.new('Overdue', 'overdue').freeze
2022-04-04 11:22:00 +05:30
DueToday = DueDateStruct.new('Due Today', 'today').freeze
DueTomorrow = DueDateStruct.new('Due Tomorrow', 'tomorrow').freeze
2018-11-08 19:23:39 +05:30
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
2016-06-02 11:05:42 +05:30
2023-06-20 00:43:36 +05:30
IssueTypeOutOfSyncError = Class.new(StandardError)
2023-07-09 08:55:56 +05:30
ForbiddenColumnUsed = Class.new(StandardError)
2023-06-20 00:43:36 +05:30
2019-03-02 22:35:43 +05:30
SORTING_PREFERENCE_FIELD = :issues_sort
2023-01-13 00:05:48 +05:30
MAX_BRANCH_TEMPLATE = 255
2019-03-02 22:35:43 +05:30
2022-11-25 23:54:43 +05:30
# 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
# https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158
2023-03-17 16:20:25 +05:30
TYPES_FOR_LIST = %w(issue incident test_case task objective key_result).freeze
2022-11-25 23:54:43 +05:30
# Types of issues that should be displayed on issue board lists
TYPES_FOR_BOARD_LIST = %w(issue incident).freeze
2020-11-24 15:15:51 +05:30
2023-06-20 00:43:36 +05:30
# This default came from the enum `issue_type` column. Defined as default in the DB
DEFAULT_ISSUE_TYPE = :issue
2014-09-02 18:07:02 +05:30
belongs_to :project
2022-08-13 15:12:31 +05:30
belongs_to :namespace, inverse_of: :issues
2020-10-24 23:57:45 +05:30
2019-12-04 20:38:33 +05:30
belongs_to :duplicated_to, class_name: 'Issue'
2018-05-09 12:01:36 +05:30
belongs_to :closed_by, class_name: 'User'
2022-03-02 08:16:31 +05:30
belongs_to :work_item_type, class_name: 'WorkItems::Type', inverse_of: :work_items
2020-05-24 23:13:21 +05:30
2023-06-20 00:43:36 +05:30
belongs_to :moved_to, class_name: 'Issue', inverse_of: :moved_from
has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id, inverse_of: :moved_to
2018-05-09 12:01:36 +05:30
2023-05-27 22:25:52 +05:30
has_internal_id :iid, scope: :namespace, track_if: -> { !importing? }, init: ->(issue, scope) do
# we need this init for the case where the IID allocation in internal_ids#last_value
# is higher than the actual issues.max(iid) value for a given project. For instance
# in case of an import where a batch of IIDs may be prealocated
#
# TODO: remove this once the UpdateIssuesInternalIdScope migration completes
if issue
[
InternalId.where(project: issue.project, usage: :issues).pick(:last_value).to_i,
issue.namespace&.issues&.maximum(:iid).to_i
].max
else
[
InternalId.where(**scope, usage: :issues).pick(:last_value).to_i,
where(**scope).maximum(:iid).to_i
].max
end
end
2014-09-02 18:07:02 +05:30
2019-12-04 20:38:33 +05:30
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
has_many :merge_requests_closing_issues,
class_name: 'MergeRequestsClosingIssues',
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
2021-01-03 14:25:43 +05:30
has_many :issue_email_participants
2022-01-26 12:08:38 +05:30
has_one :email
2017-09-10 17:25:29 +05:30
has_many :assignees, class_name: "User", through: :issue_assignees
2019-12-26 22:10:19 +05:30
has_many :zoom_meetings
2020-03-13 15:44:24 +05:30
has_many :user_mentions, class_name: "IssueUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
2020-04-08 14:13:33 +05:30
has_many :sent_notifications, as: :noteable
2020-05-24 23:13:21 +05:30
has_many :designs, class_name: 'DesignManagement::Design', inverse_of: :issue
has_many :design_versions, class_name: 'DesignManagement::Version', inverse_of: :issue do
def most_recent
ordered.first
end
end
2020-04-08 14:13:33 +05:30
2022-05-07 20:08:51 +05:30
has_one :search_data, class_name: 'Issues::SearchData'
2020-11-24 15:15:51 +05:30
has_one :issuable_severity
2020-01-01 13:55:28 +05:30
has_one :sentry_issue
2020-05-24 23:13:21 +05:30
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
2021-10-27 15:23:28 +05:30
has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus'
2020-06-23 00:09:42 +05:30
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
2023-03-04 22:38:38 +05:30
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue, validate: false
2020-06-23 00:09:42 +05:30
has_many :prometheus_alerts, through: :prometheus_alert_events
2021-12-11 22:18:48 +05:30
has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue
has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues
2022-07-16 23:28:13 +05:30
has_many :incident_management_timeline_events, class_name: 'IncidentManagement::TimelineEvent', foreign_key: :issue_id, inverse_of: :incident
2023-06-20 00:43:36 +05:30
has_many :assignment_events, class_name: 'ResourceEvents::IssueAssignmentEvent', inverse_of: :issue
2020-01-01 13:55:28 +05:30
2022-03-02 08:16:31 +05:30
alias_attribute :escalation_status, :incident_management_issuable_escalation_status
2021-09-30 23:02:18 +05:30
accepts_nested_attributes_for :issuable_severity, update_only: true
2020-01-01 13:55:28 +05:30
accepts_nested_attributes_for :sentry_issue
2022-03-02 08:16:31 +05:30
accepts_nested_attributes_for :incident_management_issuable_escalation_status, update_only: true
2016-09-29 09:46:39 +05:30
2023-05-27 22:25:52 +05:30
validates :project, presence: true, if: -> { !namespace || namespace.is_a?(Namespaces::ProjectNamespace) }
2023-03-04 22:38:38 +05:30
validates :namespace, presence: true
2022-08-27 11:52:29 +05:30
validates :work_item_type, presence: true
2023-05-27 22:25:52 +05:30
validates :confidential, inclusion: { in: [true, false], message: 'must be a boolean' }
2022-08-27 11:52:29 +05:30
2023-03-04 22:38:38 +05:30
validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed?
2022-08-27 11:52:29 +05:30
validate :due_date_after_start_date
validate :parent_link_confidentiality
2023-07-09 08:55:56 +05:30
# using a custom validation since we are overwriting the `issue_type` method to use the work_item_types table
validate :issue_type_attribute_present
2020-10-24 23:57:45 +05:30
2022-03-02 08:16:31 +05:30
enum issue_type: WorkItems::Type.base_types
2023-07-09 08:55:56 +05:30
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/402699
WorkItems::Type.base_types.each do |base_type, _value|
define_method "#{base_type}?".to_sym do
error_message = <<~ERROR
`#{base_type}?` uses the `issue_type` column underneath. As we want to remove the column,
its usage is forbidden. You should use the `work_item_types` table instead.
# Before
issue.requirement? => true
# After
issue.work_item_type.requirement? => true
More details in https://gitlab.com/groups/gitlab-org/-/epics/10529
ERROR
raise ForbiddenColumnUsed, error_message
end
end
2019-10-12 21:52:04 +05:30
alias_method :issuing_parent, :project
2022-11-25 23:54:43 +05:30
alias_attribute :issuing_parent_id, :project_id
2018-03-17 18:26:18 +05:30
2021-02-22 17:27:13 +05:30
alias_attribute :external_author, :service_desk_reply_to
2022-05-07 20:08:51 +05:30
pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
2016-04-02 18:10:28 +05:30
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
2021-01-03 14:25:43 +05:30
scope :not_in_projects, ->(project_ids) { where.not(project_id: project_ids) }
2014-09-02 18:07:02 +05:30
2018-11-08 19:23:39 +05:30
scope :with_due_date, -> { where.not(due_date: nil) }
2016-06-02 11:05:42 +05:30
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
2022-04-04 11:22:00 +05:30
scope :due_today, -> { where(due_date: Date.current) }
2018-10-15 14:42:47 +05:30
scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
2022-04-04 11:22:00 +05:30
2020-05-24 23:13:21 +05:30
scope :not_authored_by, ->(user) { where.not(author_id: user) }
2016-06-02 11:05:42 +05:30
2022-06-21 17:19:12 +05:30
scope :order_due_date_asc, -> { reorder(arel_table[:due_date].asc.nulls_last) }
scope :order_due_date_desc, -> { reorder(arel_table[:due_date].desc.nulls_last) }
2023-05-27 22:25:52 +05:30
scope :order_closest_future_date, -> { reorder(Arel.sql("CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC")) }
2020-04-22 19:07:51 +05:30
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
2022-08-13 15:12:31 +05:30
scope :order_severity_asc, -> do
build_keyset_order_on_joined_column(
scope: includes(:issuable_severity),
attribute_name: 'issuable_severities_severity',
column: IssuableSeverity.arel_table[:severity],
direction: :asc,
nullable: :nulls_first
)
end
scope :order_severity_desc, -> do
build_keyset_order_on_joined_column(
scope: includes(:issuable_severity),
attribute_name: 'issuable_severities_severity',
column: IssuableSeverity.arel_table[:severity],
direction: :desc,
nullable: :nulls_last
)
end
2022-06-21 17:19:12 +05:30
scope :order_escalation_status_asc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].asc.nulls_last).references(:incident_management_issuable_escalation_status) }
scope :order_escalation_status_desc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].desc.nulls_last).references(:incident_management_issuable_escalation_status) }
2022-07-23 23:45:48 +05:30
scope :order_closed_at_asc, -> { reorder(arel_table[:closed_at].asc.nulls_last) }
scope :order_closed_at_desc, -> { reorder(arel_table[:closed_at].desc.nulls_last) }
2016-06-02 11:05:42 +05:30
2020-04-22 19:07:51 +05:30
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
2023-05-27 22:25:52 +05:30
scope :with_web_entity_associations, -> { preload(:author, :namespace, project: [:project_feature, :route, namespace: :route]) }
2021-04-29 21:17:54 +05:30
scope :preload_awardable, -> { preload(:award_emoji) }
2020-06-23 00:09:42 +05:30
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) }
2020-10-24 23:57:45 +05:30
scope :with_api_entity_associations, -> {
2023-07-09 08:55:56 +05:30
preload(:work_item_type, :timelogs, :closed_by, :assignees, :author, :labels, :issuable_severity,
namespace: [{ parent: :route }, :route], milestone: { project: [:route, { namespace: :route }] },
2023-05-27 22:25:52 +05:30
project: [:project_namespace, :project_feature, :route, { group: :route }, { namespace: :route }],
2022-07-23 23:45:48 +05:30
duplicated_to: { project: [:project_feature] })
2020-10-24 23:57:45 +05:30
}
scope :with_issue_type, ->(types) { where(issue_type: types) }
2021-11-18 22:05:49 +05:30
scope :without_issue_type, ->(types) { where.not(issue_type: types) }
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
scope :public_only, -> { where(confidential: false) }
2021-11-11 11:23:49 +05:30
2019-07-07 11:18:12 +05:30
scope :confidential_only, -> { where(confidential: true) }
2018-03-17 18:26:18 +05:30
2021-10-27 15:23:28 +05:30
scope :without_hidden, -> {
2023-04-23 21:23:45 +05:30
where('NOT EXISTS (?)', Users::BannedUser.select(1).where('issues.author_id = banned_users.user_id'))
2021-10-27 15:23:28 +05:30
}
2020-01-01 13:55:28 +05:30
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
2020-07-28 23:09:34 +05:30
scope :service_desk, -> { where(author: ::User.support_bot) }
2021-03-11 19:13:27 +05:30
scope :inc_relations_for_view, -> { includes(author: :status, assignees: :status) }
2020-07-28 23:09:34 +05:30
2020-04-22 19:07:51 +05:30
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
# `{project_id: x, iid: y}`.
#
# @see WhereComposite::where_composite
#
# e.g:
#
# .by_project_id_and_iid({project_id: 1, iid: 2})
# .by_project_id_and_iid([]) # returns ActiveRecord::NullRelation
# .by_project_id_and_iid([
# {project_id: 1, iid: 1},
# {project_id: 2, iid: 1},
# {project_id: 1, iid: 2}
# ])
#
scope :by_project_id_and_iid, ->(composites) do
where_composite(%i[project_id iid], composites)
end
2021-11-18 22:05:49 +05:30
scope :with_null_relative_position, -> { where(relative_position: nil) }
scope :with_non_null_relative_position, -> { where.not(relative_position: nil) }
2023-01-13 00:05:48 +05:30
scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') }
2019-12-26 22:10:19 +05:30
2022-08-27 11:52:29 +05:30
before_validation :ensure_namespace_id, :ensure_work_item_type
2023-06-20 00:43:36 +05:30
before_save :check_issue_type_in_sync!
2022-08-13 15:12:31 +05:30
2023-05-27 22:25:52 +05:30
after_save :ensure_metrics!, unless: :importing?
2023-03-04 22:38:38 +05:30
after_commit :expire_etag_cache, unless: :importing?
2021-01-03 14:25:43 +05:30
after_create_commit :record_create_action, unless: :importing?
2017-08-17 22:00:37 +05:30
2016-09-13 17:45:13 +05:30
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
2020-01-01 13:55:28 +05:30
state_machine :state_id, initial: :opened, initialize: false do
2014-09-02 18:07:02 +05:30
event :close do
2017-09-10 17:25:29 +05:30
transition [:opened] => :closed
2014-09-02 18:07:02 +05:30
end
event :reopen do
2017-09-10 17:25:29 +05:30
transition closed: :opened
2014-09-02 18:07:02 +05:30
end
2019-12-21 20:55:43 +05:30
state :opened, value: Issue.available_states[:opened]
state :closed, value: Issue.available_states[:closed]
2017-08-17 22:00:37 +05:30
2021-06-08 01:23:25 +05:30
before_transition any => :closed do |issue, transition|
args = transition.args
2019-07-31 22:56:46 +05:30
issue.closed_at = issue.system_note_timestamp
2021-06-08 01:23:25 +05:30
next if args.empty?
next unless args.first.is_a?(User)
issue.closed_by = args.first
2017-08-17 22:00:37 +05:30
end
2018-05-09 12:01:36 +05:30
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
2021-12-11 22:18:48 +05:30
issue.clear_closure_reason_references
2018-05-09 12:01:36 +05:30
end
2014-09-02 18:07:02 +05:30
end
2019-12-21 20:55:43 +05:30
class << self
2021-09-30 23:02:18 +05:30
extend ::Gitlab::Utils::Override
# Alias to state machine .with_state_id method
# This needs to be defined after the state machine block to avoid errors
2019-12-21 20:55:43 +05:30
alias_method :with_state, :with_state_id
alias_method :with_states, :with_state_ids
2021-09-30 23:02:18 +05:30
override :order_upvotes_desc
def order_upvotes_desc
reorder(upvotes_count: :desc)
end
override :order_upvotes_asc
def order_upvotes_asc
reorder(upvotes_count: :asc)
end
2022-05-07 20:08:51 +05:30
2023-01-13 00:05:48 +05:30
override :full_search
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
)
2022-05-07 20:08:51 +05:30
end
2019-12-21 20:55:43 +05:30
end
2022-11-25 23:54:43 +05:30
def self.participant_includes
[:assignees] + super
end
2022-01-26 12:08:38 +05:30
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
def relative_positioning_parent_projects
project.group&.root_ancestor&.all_projects&.select(:id) || Project.id_in(project).select(:id)
end
2019-10-12 21:52:04 +05:30
def self.relative_positioning_query_base(issue)
2022-01-26 12:08:38 +05:30
in_projects(issue.relative_positioning_parent_projects)
2015-04-26 12:48:37 +05:30
end
2019-10-12 21:52:04 +05:30
def self.relative_positioning_parent_column
2019-02-15 15:39:39 +05:30
:project_id
end
2015-09-11 14:41:01 +05:30
def self.reference_prefix
'#'
end
2023-04-23 21:23:45 +05:30
# 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")
def self.alternative_reference_prefix
'GL-'
end
2015-09-11 14:41:01 +05:30
# Pattern used to extract `#123` issue references from text
#
# This pattern supports cross-project references.
def self.reference_pattern
2016-06-02 11:05:42 +05:30
@reference_pattern ||= %r{
2023-04-23 21:23:45 +05:30
(?:
(#{Project.reference_pattern})?#{Regexp.escape(reference_prefix)} |
#{Regexp.escape(alternative_reference_prefix)}
)#{Gitlab::Regex.issue}
2015-09-11 14:41:01 +05:30
}x
end
2015-12-23 02:04:40 +05:30
def self.link_reference_pattern
2023-05-27 22:25:52 +05:30
@link_reference_pattern ||= compose_link_reference_pattern(%r{issues(?:\/incident)?}, Gitlab::Regex.issue)
2016-06-02 11:05:42 +05:30
end
2016-06-22 15:30:34 +05:30
def self.reference_valid?(reference)
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
2016-11-03 12:29:30 +05:30
def self.project_foreign_key
'project_id'
end
2020-03-13 15:44:24 +05:30
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 },
2021-11-18 22:05:49 +05:30
'relative_position' => -> { order_by_relative_position },
'relative_position_asc' => -> { order_by_relative_position }
2020-03-13 15:44:24 +05:30
}
)
end
2018-05-09 12:01:36 +05:30
def self.sort_by_attribute(method, excluded_labels: [])
2016-06-02 11:05:42 +05:30
case method.to_s
2019-12-04 20:38:33 +05:30
when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
2019-12-26 22:10:19 +05:30
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
2021-11-18 22:05:49 +05:30
when 'relative_position', 'relative_position_asc' then order_by_relative_position
2022-08-13 15:12:31 +05:30
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
2022-07-23 23:45:48 +05:30
when 'closed_at_desc' then order_closed_at_desc
2016-06-02 11:05:42 +05:30
else
super
end
2015-12-23 02:04:40 +05:30
end
2021-11-18 22:05:49 +05:30
def self.order_by_relative_position
reorder(Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_id_asc]))
2021-09-30 23:02:18 +05:30
end
def self.column_order_relative_position
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'relative_position',
column_expression: arel_table[:relative_position],
2022-06-21 17:19:12 +05:30
order_expression: Issue.arel_table[:relative_position].asc.nulls_last,
2021-09-30 23:02:18 +05:30
nullable: :nulls_last,
distinct: false
)
end
2021-11-11 11:23:49 +05:30
def self.column_order_id_asc
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: arel_table[:id].asc
)
end
2023-01-13 00:05:48 +05:30
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
2021-10-27 15:23:28 +05:30
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
2021-06-08 01:23:25 +05:30
# Temporary disable moving null elements because of performance problems
# For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
def check_repositioning_allowed!
if blocked_for_repositioning?
raise ::Gitlab::RelativePositioning::IssuePositioningDisabled, "Issue relative position changes temporarily disabled."
end
end
def blocked_for_repositioning?
resource_parent.root_namespace&.issue_repositioning_disabled?
end
2017-08-17 22:00:37 +05:30
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
2023-05-27 22:25:52 +05:30
"#{namespace.to_reference_base(from, full: full)}#{reference}"
2014-09-02 18:07:02 +05:30
end
2018-10-15 14:42:47 +05:30
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
2022-09-01 20:07:04 +05:30
branch_name_generator = -> (counter) do
suffix = counter > 5 ? SecureRandom.hex(8) : counter
"#{to_branch_name}-#{suffix}"
end
2023-05-27 22:25:52 +05:30
Gitlab::Utils::Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
2018-10-15 14:42:47 +05:30
project.repository.branch_exists?(suggested_branch_name)
end
end
2015-04-26 12:48:37 +05:30
# To allow polymorphism with MergeRequest.
def source_project
project
end
2015-10-24 18:46:33 +05:30
2016-06-02 11:05:42 +05:30
def moved?
2019-12-04 20:38:33 +05:30
!moved_to_id.nil?
end
def duplicated?
!duplicated_to_id.nil?
2016-06-02 11:05:42 +05:30
end
2021-12-11 22:18:48 +05:30
def clear_closure_reason_references
self.moved_to_id = nil
self.duplicated_to_id = nil
end
2016-06-02 11:05:42 +05:30
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
2021-02-22 17:27:13 +05:30
alias_method :can_clone?, :can_move?
2016-06-02 11:05:42 +05:30
def to_branch_name
if self.confidential?
"#{iid}-confidential-issue"
else
2023-01-13 00:05:48 +05:30
self.class.to_branch_name(iid, title, project: project)
2016-06-02 11:05:42 +05:30
end
end
2020-11-24 15:15:51 +05:30
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',
2021-02-22 17:27:13 +05:30
'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'])
2020-11-24 15:15:51 +05:30
.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')
2021-04-29 21:17:54 +05:30
related_issues = yield related_issues if block_given?
2020-11-24 15:15:51 +05:30
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
2018-10-15 14:42:47 +05:30
def can_be_worked_on?
!self.closed? && !self.project.forked?
2016-06-02 11:05:42 +05:30
end
2016-09-13 17:45:13 +05:30
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
2019-07-07 11:18:12 +05:30
return publicly_visible? unless user
return false unless readable_by?(user)
2020-01-01 13:55:28 +05:30
user.can_read_all_resources? ||
2019-07-07 11:18:12 +05:30
::Gitlab::ExternalAuthorization.access_allowed?(
user, project.external_authorization_classification_label)
2016-09-13 17:45:13 +05:30
end
2021-10-27 15:23:28 +05:30
def check_for_spam?(user:)
2021-09-30 23:02:18 +05:30
# content created via support bots is always checked for spam, EVEN if
# the issue is not publicly visible and/or confidential
2021-10-27 15:23:28 +05:30
return true if user.support_bot? && spammable_attribute_changed?
2021-09-30 23:02:18 +05:30
# Only check for spam on issues which are publicly visible (and thus indexed in search engines)
return false unless publicly_visible?
# Only check for spam if certain attributes have changed
spammable_attribute_changed?
2016-11-24 13:41:30 +05:30
end
def as_json(options = {})
super(options).tap do |json|
2017-09-10 17:25:29 +05:30
if options.key?(:labels)
2016-11-24 13:41:30 +05:30
json[:labels] = labels.as_json(
project: project,
2017-08-17 22:00:37 +05:30
only: [:id, :title, :description, :color, :priority],
2016-11-24 13:41:30 +05:30
methods: [:text_color]
)
end
end
end
2018-11-08 19:23:39 +05:30
def etag_caching_enabled?
true
end
2018-03-17 18:26:18 +05:30
def discussions_rendered_on_frontend?
true
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def update_project_counter_caches
2023-06-20 00:43:36 +05:30
# TODO: Fix counter cache for issues in group
2023-07-09 08:55:56 +05:30
# TODO: see https://gitlab.com/gitlab-org/gitlab/-/work_items/393125
2023-06-20 00:43:36 +05:30
return unless project
2018-03-17 18:26:18 +05:30
Projects::OpenIssuesCountService.new(project).refresh_cache
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
2019-07-07 11:18:12 +05:30
def merge_requests_count(user = nil)
::MergeRequestsClosingIssues.count_for_issue(self.id, user)
end
2020-04-08 14:13:33 +05:30
def previous_updated_at
previous_changes['updated_at']&.first || updated_at
end
2020-06-23 00:09:42 +05:30
def banzai_render_context(field)
super.merge(label_url_method: :project_issues_url)
end
2020-05-24 23:13:21 +05:30
def design_collection
@design_collection ||= ::DesignManagement::DesignCollection.new(self)
end
2020-07-28 23:09:34 +05:30
def from_service_desk?
author.id == User.support_bot.id
end
2020-11-24 15:15:51 +05:30
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
2021-01-03 14:25:43 +05:30
def relocation_target
moved_to || duplicated_to
end
2021-03-08 18:12:59 +05:30
def supports_assignee?
2023-07-09 08:55:56 +05:30
work_item_type_with_default.supports_assignee?
2021-03-08 18:12:59 +05:30
end
2021-09-04 01:27:46 +05:30
def supports_time_tracking?
issue_type_supports?(:time_tracking)
end
2021-11-18 22:05:49 +05:30
def supports_move_and_clone?
issue_type_supports?(:move_and_clone)
end
2021-04-17 20:07:23 +05:30
def email_participants_emails
issue_email_participants.pluck(:email)
end
def email_participants_emails_downcase
issue_email_participants.pluck(IssueEmailParticipant.arel_table[:email].lower)
end
2021-04-29 21:17:54 +05:30
def issue_assignee_user_ids
issue_assignees.pluck(:user_id)
end
2021-09-30 23:02:18 +05:30
def update_upvotes_count
self.lock!
self.update_column(:upvotes_count, self.upvotes)
end
2016-09-13 17:45:13 +05:30
# Returns `true` if the given User can read the current Issue.
2016-11-24 13:41:30 +05:30
#
# 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
2016-09-13 17:45:13 +05:30
def readable_by?(user)
2022-11-25 23:54:43 +05:30
if !project.issues_enabled?
false
elsif user.can_read_all_resources?
2016-09-13 17:45:13 +05:30
true
2022-04-04 11:22:00 +05:30
elsif project.personal? && project.team.owner?(user)
2016-09-13 17:45:13 +05:30
true
2020-03-28 13:19:24 +05:30
elsif confidential? && !assignee_or_author?(user)
2023-07-09 08:55:56 +05:30
project.member?(user, Gitlab::Access::REPORTER)
2021-10-27 15:23:28 +05:30
elsif hidden?
false
2022-01-26 12:08:38 +05:30
elsif project.public? || (project.internal? && !user.external?)
project.feature_available?(:issues, user)
2016-09-13 17:45:13 +05:30
else
2023-07-09 08:55:56 +05:30
project.member?(user)
2016-09-13 17:45:13 +05:30
end
end
2021-10-27 15:23:28 +05:30
def hidden?
author&.banned?
end
2022-07-23 23:45:48 +05:30
def expire_etag_cache
2023-06-20 00:43:36 +05:30
# TODO: Fix this for the case when issues is created at group level
2023-07-09 08:55:56 +05:30
# TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/395814
2023-06-20 00:43:36 +05:30
return unless project
2022-07-23 23:45:48 +05:30
key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
Gitlab::EtagCaching::Store.new.touch(key)
end
2023-01-13 00:05:48 +05:30
def supports_confidentiality?
true
end
2023-04-23 21:23:45 +05:30
# 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.
def to_work_item_global_id
::Gitlab::GlobalId.as_global_id(id, model_name: WorkItem.name)
end
2023-06-20 00:43:36 +05:30
def resource_parent
project || namespace
end
# 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
def work_item_type_with_default
work_item_type || WorkItems::Type.default_by_type(DEFAULT_ISSUE_TYPE)
end
2023-07-09 08:55:56 +05:30
def issue_type
if ::Feature.enabled?(:issue_type_uses_work_item_types_table)
work_item_type_with_default.base_type
else
super
end
end
2021-08-04 16:29:09 +05:30
private
2023-06-20 00:43:36 +05:30
def check_issue_type_in_sync!
# We might have existing records out of sync, so we need to skip this check unless the value is changed
# so those records can still be updated until we fix them and remove the issue_type column
2023-07-09 08:55:56 +05:30
# https://gitlab.com/gitlab-org/gitlab/-/work_items/403158
2023-06-20 00:43:36 +05:30
return unless (changes.keys & %w[issue_type work_item_type_id]).any?
2023-07-09 08:55:56 +05:30
# Do not replace the use of attributes with `issue_type` here
if attributes['issue_type'] != work_item_type.base_type
2023-06-20 00:43:36 +05:30
error = IssueTypeOutOfSyncError.new(
<<~ERROR
Issue `issue_type` out of sync with `work_item_type_id` column.
`issue_type` must be equal to `work_item.base_type`.
You can assign the correct work_item_type like this for example:
Issue.new(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident))
More details in https://gitlab.com/gitlab-org/gitlab/-/issues/338005
ERROR
)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
error,
2023-07-09 08:55:56 +05:30
issue_type: attributes['issue_type'],
2023-06-20 00:43:36 +05:30
work_item_type_id: work_item_type_id
)
end
end
2023-07-09 08:55:56 +05:30
def issue_type_attribute_present
return if attributes['issue_type'].present?
errors.add(:issue_type, 'Must be present')
end
2022-08-27 11:52:29 +05:30
def due_date_after_start_date
return unless start_date.present? && due_date.present?
if due_date < start_date
errors.add(:due_date, 'must be greater than or equal to start date')
end
end
# Although parent/child relationship can be set only for WorkItems, we
# still need to validate it for Issue model too, because both models use
# same table.
def parent_link_confidentiality
return unless persisted?
if confidential? && WorkItems::ParentLink.has_public_children?(id)
2022-10-11 01:57:18 +05:30
errors.add(:base, _('A confidential issue cannot have a parent that already has non-confidential children.'))
2022-08-27 11:52:29 +05:30
end
if !confidential? && WorkItems::ParentLink.has_confidential_parent?(id)
2022-10-11 01:57:18 +05:30
errors.add(:base, _('A non-confidential issue cannot have a confidential parent.'))
2022-08-27 11:52:29 +05:30
end
end
2022-05-07 20:08:51 +05:30
override :persist_pg_full_text_search_vector
def persist_pg_full_text_search_vector(search_vector)
2023-06-20 00:43:36 +05:30
# TODO: Fix search vector for issues at group level
2023-07-09 08:55:56 +05:30
# TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/393126
2023-06-20 00:43:36 +05:30
return unless project
2022-05-07 20:08:51 +05:30
Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
end
2021-09-30 23:02:18 +05:30
def spammable_attribute_changed?
title_changed? ||
description_changed? ||
# NOTE: We need to check them for spam when issues are made non-confidential, because spam
# may have been added while they were confidential and thus not being checked for spam.
confidential_changed?(from: true, to: false)
end
2023-05-27 22:25:52 +05:30
def ensure_metrics!
2021-11-11 11:23:49 +05:30
Issue::Metrics.record!(self)
2021-08-04 16:29:09 +05:30
end
def record_create_action
2023-06-20 00:43:36 +05:30
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(
author: author, namespace: namespace.reset
)
2021-08-04 16:29:09 +05:30
end
2016-09-13 17:45:13 +05:30
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
2023-06-20 00:43:36 +05:30
resource_parent.public? && resource_parent.feature_available?(:issues, nil) &&
2022-05-03 16:02:30 +05:30
!confidential? && !hidden? && !::Gitlab::ExternalAuthorization.enabled?
2016-09-13 17:45:13 +05:30
end
2017-08-17 22:00:37 +05:30
2020-11-24 15:15:51 +05:30
def could_not_move(exception)
# Symptom of running out of space - schedule rebalancing
2022-01-26 12:08:38 +05:30
Issues::RebalancingWorker.perform_async(nil, *project.self_or_root_group_ids)
2020-11-24 15:15:51 +05:30
end
2022-08-13 15:12:31 +05:30
def ensure_namespace_id
self.namespace = project.project_namespace if project
end
2022-08-27 11:52:29 +05:30
def ensure_work_item_type
return if work_item_type_id.present? || work_item_type_id_change&.last.present?
2023-06-20 00:43:36 +05:30
# TODO: We should switch to DEFAULT_ISSUE_TYPE here when the issue_type column is dropped
2023-07-09 08:55:56 +05:30
# https://gitlab.com/gitlab-org/gitlab/-/work_items/402700
self.work_item_type = WorkItems::Type.default_by_type(attributes['issue_type'])
2022-08-27 11:52:29 +05:30
end
2023-03-04 22:38:38 +05:30
def allowed_work_item_type_change
return unless changes[:work_item_type_id]
involved_types = WorkItems::Type.where(id: changes[:work_item_type_id].compact).pluck(:base_type).uniq
disallowed_types = involved_types - WorkItems::Type::CHANGEABLE_BASE_TYPES
return if disallowed_types.empty?
errors.add(:work_item_type_id, format(_('can not be changed to %{new_type}'), new_type: work_item_type&.name))
end
2014-09-02 18:07:02 +05:30
end
2019-12-04 20:38:33 +05:30
2021-06-08 01:23:25 +05:30
Issue.prepend_mod_with('Issue')