Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/issue.rb')
-rw-r--r--app/models/issue.rb163
1 files changed, 139 insertions, 24 deletions
diff --git a/app/models/issue.rb b/app/models/issue.rb
index bea86168c8d..b7125617034 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -39,6 +39,9 @@ class Issue < ApplicationRecord
DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
+ IssueTypeOutOfSyncError = Class.new(StandardError)
+ ForbiddenColumnUsed = Class.new(StandardError)
+
SORTING_PREFERENCE_FIELD = :issues_sort
MAX_BRANCH_TEMPLATE = 255
@@ -52,18 +55,37 @@ class Issue < ApplicationRecord
# Types of issues that should be displayed on issue board lists
TYPES_FOR_BOARD_LIST = %w(issue incident).freeze
+ # This default came from the enum `issue_type` column. Defined as default in the DB
+ DEFAULT_ISSUE_TYPE = :issue
+
belongs_to :project
belongs_to :namespace, inverse_of: :issues
belongs_to :duplicated_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
- belongs_to :iteration, foreign_key: 'sprint_id'
belongs_to :work_item_type, class_name: 'WorkItems::Type', inverse_of: :work_items
- belongs_to :moved_to, class_name: 'Issue'
- has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id
-
- has_internal_id :iid, scope: :project, track_if: -> { !importing? }
+ 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
+
+ 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
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
@@ -97,6 +119,7 @@ class Issue < ApplicationRecord
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
has_many :incident_management_timeline_events, class_name: 'IncidentManagement::TimelineEvent', foreign_key: :issue_id, inverse_of: :incident
+ has_many :assignment_events, class_name: 'ResourceEvents::IssueAssignmentEvent', inverse_of: :issue
alias_attribute :escalation_status, :incident_management_issuable_escalation_status
@@ -104,17 +127,41 @@ class Issue < ApplicationRecord
accepts_nested_attributes_for :sentry_issue
accepts_nested_attributes_for :incident_management_issuable_escalation_status, update_only: true
- validates :project, presence: true
- validates :issue_type, presence: true
+ validates :project, presence: true, if: -> { !namespace || namespace.is_a?(Namespaces::ProjectNamespace) }
validates :namespace, presence: true
validates :work_item_type, presence: true
+ validates :confidential, inclusion: { in: [true, false], message: 'must be a boolean' }
validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed?
validate :due_date_after_start_date
validate :parent_link_confidentiality
+ # using a custom validation since we are overwriting the `issue_type` method to use the work_item_types table
+ validate :issue_type_attribute_present
enum issue_type: WorkItems::Type.base_types
+ # 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
+
alias_method :issuing_parent, :project
alias_attribute :issuing_parent_id, :project_id
@@ -136,7 +183,7 @@ class Issue < ApplicationRecord
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) }
- 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')) }
+ 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")) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_severity_asc, -> do
build_keyset_order_on_joined_column(
@@ -162,15 +209,15 @@ class Issue < ApplicationRecord
scope :order_closed_at_desc, -> { reorder(arel_table[:closed_at].desc.nulls_last) }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
- scope :with_web_entity_associations, -> { preload(:author, project: [:project_feature, :route, namespace: :route]) }
+ scope :with_web_entity_associations, -> { preload(:author, :namespace, project: [:project_feature, :route, namespace: :route]) }
scope :preload_awardable, -> { preload(:award_emoji) }
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) }
scope :with_api_entity_associations, -> {
- preload(:timelogs, :closed_by, :assignees, :author, :labels, :issuable_severity,
- milestone: { project: [:route, { namespace: :route }] },
- project: [:project_feature, :route, { namespace: :route }],
+ preload(:work_item_type, :timelogs, :closed_by, :assignees, :author, :labels, :issuable_severity,
+ namespace: [{ parent: :route }, :route], milestone: { project: [:route, { namespace: :route }] },
+ project: [:project_namespace, :project_feature, :route, { group: :route }, { namespace: :route }],
duplicated_to: { project: [:project_feature] })
}
scope :with_issue_type, ->(types) { where(issue_type: types) }
@@ -213,8 +260,9 @@ class Issue < ApplicationRecord
scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') }
before_validation :ensure_namespace_id, :ensure_work_item_type
+ before_save :check_issue_type_in_sync!
- after_save :ensure_metrics, unless: :importing?
+ after_save :ensure_metrics!, unless: :importing?
after_commit :expire_etag_cache, unless: :importing?
after_create_commit :record_create_action, unless: :importing?
@@ -345,7 +393,7 @@ class Issue < ApplicationRecord
end
def self.link_reference_pattern
- @link_reference_pattern ||= super(%r{issues(?:\/incident)?}, Gitlab::Regex.issue)
+ @link_reference_pattern ||= compose_link_reference_pattern(%r{issues(?:\/incident)?}, Gitlab::Regex.issue)
end
def self.reference_valid?(reference)
@@ -450,7 +498,7 @@ class Issue < ApplicationRecord
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
- "#{project.to_reference_base(from, full: full)}#{reference}"
+ "#{namespace.to_reference_base(from, full: full)}#{reference}"
end
def suggested_branch_name
@@ -463,7 +511,7 @@ class Issue < ApplicationRecord
"#{to_branch_name}-#{suffix}"
end
- Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
+ Gitlab::Utils::Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
@@ -576,6 +624,10 @@ class Issue < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def update_project_counter_caches
+ # TODO: Fix counter cache for issues in group
+ # TODO: see https://gitlab.com/gitlab-org/gitlab/-/work_items/393125
+ return unless project
+
Projects::OpenIssuesCountService.new(project).refresh_cache
end
# rubocop: enable CodeReuse/ServiceClass
@@ -614,7 +666,7 @@ class Issue < ApplicationRecord
end
def supports_assignee?
- issue_type_supports?(:assignee)
+ work_item_type_with_default.supports_assignee?
end
def supports_time_tracking?
@@ -655,13 +707,13 @@ class Issue < ApplicationRecord
elsif project.personal? && project.team.owner?(user)
true
elsif confidential? && !assignee_or_author?(user)
- project.team.member?(user, Gitlab::Access::REPORTER)
+ project.member?(user, Gitlab::Access::REPORTER)
elsif hidden?
false
elsif project.public? || (project.internal? && !user.external?)
project.feature_available?(:issues, user)
else
- project.team.member?(user)
+ project.member?(user)
end
end
@@ -670,6 +722,10 @@ class Issue < ApplicationRecord
end
def expire_etag_cache
+ # TODO: Fix this for the case when issues is created at group level
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/395814
+ return unless project
+
key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
Gitlab::EtagCaching::Store.new.touch(key)
end
@@ -684,8 +740,60 @@ class Issue < ApplicationRecord
::Gitlab::GlobalId.as_global_id(id, model_name: WorkItem.name)
end
+ 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
+
+ def issue_type
+ if ::Feature.enabled?(:issue_type_uses_work_item_types_table)
+ work_item_type_with_default.base_type
+ else
+ super
+ end
+ end
+
private
+ 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
+ return unless (changes.keys & %w[issue_type work_item_type_id]).any?
+
+ # Do not replace the use of attributes with `issue_type` here
+ if attributes['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: attributes['issue_type'],
+ work_item_type_id: work_item_type_id
+ )
+ end
+ end
+
+ def issue_type_attribute_present
+ return if attributes['issue_type'].present?
+
+ errors.add(:issue_type, 'Must be present')
+ end
+
def due_date_after_start_date
return unless start_date.present? && due_date.present?
@@ -711,6 +819,10 @@ class Issue < ApplicationRecord
override :persist_pg_full_text_search_vector
def persist_pg_full_text_search_vector(search_vector)
+ # TODO: Fix search vector for issues at group level
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/393126
+ return unless project
+
Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
end
@@ -722,18 +834,19 @@ class Issue < ApplicationRecord
confidential_changed?(from: true, to: false)
end
- override :ensure_metrics
- def ensure_metrics
+ def ensure_metrics!
Issue::Metrics.record!(self)
end
def record_create_action
- Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(author: author, project: project)
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(
+ author: author, namespace: namespace.reset
+ )
end
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
- project.public? && project.feature_available?(:issues, nil) &&
+ resource_parent.public? && resource_parent.feature_available?(:issues, nil) &&
!confidential? && !hidden? && !::Gitlab::ExternalAuthorization.enabled?
end
@@ -749,7 +862,9 @@ class Issue < ApplicationRecord
def ensure_work_item_type
return if work_item_type_id.present? || work_item_type_id_change&.last.present?
- self.work_item_type = WorkItems::Type.default_by_type(issue_type)
+ # 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
+ self.work_item_type = WorkItems::Type.default_by_type(attributes['issue_type'])
end
def allowed_work_item_type_change