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
2023-04-23 21:23:45 +05:30
include Exportable
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 )
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 ) }
2020-10-24 23:57:45 +05:30
validates :issue_type , presence : true
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
2020-10-24 23:57:45 +05:30
2022-03-02 08:16:31 +05:30
enum issue_type : WorkItems :: Type . base_types
2016-01-14 18:37:52 +05:30
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-05-27 22:25:52 +05:30
preload ( :timelogs , :closed_by , :assignees , :author , :labels , :issuable_severity , namespace : [ { parent : :route } , :route ] ,
2020-10-24 23:57:45 +05:30
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
# TODO: see https://gitlab.com/gitlab-org/gitlab/-/work_items/393125?iid_path=true
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?
issue_type_supports? ( :assignee )
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 )
project . team . 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
2022-01-26 12:08:38 +05:30
project . team . 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
# TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/395814?iid_path=true
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
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
# https://gitlab.com/gitlab-org/gitlab/-/work_items/403158?iid_path=true
return unless ( changes . keys & %w[ issue_type work_item_type_id ] ) . any?
if issue_type != work_item_type . base_type
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 ,
issue_type : issue_type ,
work_item_type_id : work_item_type_id
)
end
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
# TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/393126?iid_path=true
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
# https://gitlab.com/gitlab-org/gitlab/-/work_items/402700?iid_path=true
2022-08-27 11:52:29 +05:30
self . work_item_type = WorkItems :: Type . default_by_type ( issue_type )
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' )