Commit 86bcbb6e authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch 'move-iteration-as-ee-model' into 'master'

Remove FOSS Iteration model placeholder

See merge request gitlab-org/gitlab!59172

https://gitlab.com/gitlab-org/gitlab/-/issues/322849
parents 11c635a8 86f190a2
......@@ -1884,7 +1884,6 @@ Gitlab/NamespacedClass:
- 'app/models/issue_email_participant.rb'
- 'app/models/issue_link.rb'
- 'app/models/issue_user_mention.rb'
- 'app/models/iteration.rb'
- 'app/models/jira_connect_installation.rb'
- 'app/models/jira_connect_subscription.rb'
- 'app/models/jira_import_state.rb'
......@@ -2696,6 +2695,7 @@ Gitlab/NamespacedClass:
- 'ee/app/models/issuable_metric_image.rb'
- 'ee/app/models/issuable_sla.rb'
- 'ee/app/models/issuables_analytics.rb'
- 'ee/app/models/iteration.rb'
- 'ee/app/models/iteration_note.rb'
- 'ee/app/models/ldap_group_link.rb'
- 'ee/app/models/ldap_key.rb'
......
# frozen_string_literal: true
# Placeholder class for model that is implemented in EE
class Iteration < ApplicationRecord
self.table_name = 'sprints'
def self.reference_prefix
'*iteration:'
end
def self.reference_pattern
nil
end
end
Iteration.prepend_if_ee('::EE::Iteration')
# frozen_string_literal: true
module EE
module Iteration
extend ActiveSupport::Concern
STATE_ENUM_MAP = {
upcoming: 1,
started: 2,
closed: 3
}.with_indifferent_access.freeze
# For Iteration
class Predefined
None = ::Timebox::TimeboxStruct.new('None', 'none', ::Timebox::None.id).freeze
Any = ::Timebox::TimeboxStruct.new('Any', 'any', ::Timebox::Any.id).freeze
Current = ::Timebox::TimeboxStruct.new('Current', 'current', -4).freeze
ALL = [None, Any, Current].freeze
end
prepended do
include AtomicInternalId
include Timebox
attr_accessor :skip_future_date_validation
attr_accessor :skip_project_validation
belongs_to :project
belongs_to :group
belongs_to :iterations_cadence, class_name: '::Iterations::Cadence', foreign_key: :iterations_cadence_id, inverse_of: :iterations
has_many :issues, foreign_key: 'sprint_id'
has_many :merge_requests, foreign_key: 'sprint_id'
has_internal_id :iid, scope: :project
has_internal_id :iid, scope: :group
validates :start_date, presence: true
validates :due_date, presence: true
validates :iterations_cadence, presence: true, unless: -> { project_id.present? }
validate :dates_do_not_overlap, if: :start_or_due_dates_changed?
validate :future_date, if: :start_or_due_dates_changed?, unless: :skip_future_date_validation
validate :no_project, unless: :skip_project_validation
validate :validate_group
before_validation :set_iterations_cadence, unless: -> { project_id.present? }
before_create :set_past_iteration_state
scope :upcoming, -> { with_state(:upcoming) }
scope :started, -> { with_state(:started) }
scope :closed, -> { with_state(:closed) }
scope :within_timeframe, -> (start_date, end_date) do
where('start_date <= ?', end_date).where('due_date >= ?', start_date)
end
scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) }
scope :due_date_passed, -> { where('due_date < ?', Date.current) }
state_machine :state_enum, initial: :upcoming do
event :start do
transition upcoming: :started
end
event :close do
transition [:upcoming, :started] => :closed
end
state :upcoming, value: Iteration::STATE_ENUM_MAP[:upcoming]
state :started, value: Iteration::STATE_ENUM_MAP[:started]
state :closed, value: Iteration::STATE_ENUM_MAP[:closed]
end
class << self
alias_method :with_state, :with_state_enum
alias_method :with_states, :with_state_enums
end
end
class_methods do
def reference_pattern
# NOTE: The id pattern only matches when all characters on the expression
# are digits, so it will match *iteration:2 but not *iteration:2.1 because that's probably a
# iteration name and we want it to be matched as such.
@reference_pattern ||= %r{
(#{::Project.reference_pattern})?
#{::Regexp.escape(reference_prefix)}
(?:
(?<iteration_id>
\d+(?!\S\w)\b # Integer-based iteration id, or
) |
(?<iteration_name>
[^"\s]+\b | # String-based single-word iteration title, or
"[^"]+" # String-based multi-word iteration surrounded in quotes
)
)
}x.freeze
end
def link_reference_pattern
@link_reference_pattern ||= super("iterations", /(?<iteration>\d+)/)
end
def filter_by_state(iterations, state)
case state
when 'closed' then iterations.closed
when 'started' then iterations.started
when 'upcoming' then iterations.upcoming
when 'opened' then iterations.started.or(iterations.upcoming)
when 'all' then iterations
else raise ArgumentError, "Unknown state filter: #{state}"
end
end
end
def state
STATE_ENUM_MAP.key(state_enum)
end
def state=(value)
self.state_enum = STATE_ENUM_MAP[value]
end
def resource_parent
group || project
end
# Show just the title when we manage to find an iteration, without the reference pattern,
# since it's long and unsightly.
def reference_link_text(from = nil)
self.title
end
def supports_timebox_charts?
resource_parent&.feature_available?(:iterations) && weight_available?
end
private
def timebox_format_reference(format = :id)
raise ::ArgumentError, _('Unknown format') unless [:id, :name].include?(format)
if format == :name
super
else
id
end
end
def parent_group
group || project.group
end
def start_or_due_dates_changed?
start_date_changed? || due_date_changed?
end
# ensure dates do not overlap with other Iterations in the same cadence tree
def dates_do_not_overlap
return unless iterations_cadence
return unless iterations_cadence.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
# for now we only have a single default cadence within a group just to wrap the iterations into a set.
# once we introduce multiple cadences per group we need to change this message.
# related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/299312
errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations within this group"))
end
def future_date
if start_or_due_dates_changed?
errors.add(:start_date, s_("Iteration|cannot be more than 500 years in the future")) if start_date > 500.years.from_now
errors.add(:due_date, s_("Iteration|cannot be more than 500 years in the future")) if due_date > 500.years.from_now
end
end
def no_project
return unless project_id.present?
errors.add(:project_id, s_("is not allowed. We do not currently support project-level iterations"))
end
def set_past_iteration_state
# if we create an iteration in the past, we set the state to closed right away,
# no need to wait for IterationsUpdateStatusWorker to do so.
self.state = :closed if due_date < Date.current
end
# TODO: this method should be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296099
def set_iterations_cadence
return if iterations_cadence
# For now we support only group iterations
# issue to clarify project iterations: https://gitlab.com/gitlab-org/gitlab/-/issues/299864
return unless group
# we need this as we use the cadence to validate the dates overlap for this iteration,
# so in the case this runs before background migration we need to first set all iterations
# in this group to a cadence before we can validate the dates overlap.
default_cadence = find_or_create_default_cadence
group.iterations.where(iterations_cadence_id: nil).update_all(iterations_cadence_id: default_cadence.id)
self.iterations_cadence = default_cadence
end
# TODO: this method should be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296099
def find_or_create_default_cadence
cadence_title = "#{group.name} Iterations"
start_date = self.start_date || Date.today
::Iterations::Cadence.create_with(
title: cadence_title,
start_date: start_date,
automatic: false,
# set to 0, i.e. unspecified when creating default iterations as we do validate for presence.
iterations_in_advance: 0,
duration_in_weeks: 0
).safe_find_or_create_by!(group: group)
end
# TODO: remove this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296100
def validate_group
return if iterations_cadence&.group_id == group_id
return unless iterations_cadence
errors.add(:group, s_('is not valid. The iteration group has to match the iteration cadence group.'))
end
end
end
# frozen_string_literal: true
class Iteration < ApplicationRecord
include AtomicInternalId
include Timebox
self.table_name = 'sprints'
STATE_ENUM_MAP = {
upcoming: 1,
started: 2,
closed: 3
}.with_indifferent_access.freeze
attr_accessor :skip_future_date_validation
attr_accessor :skip_project_validation
belongs_to :project
belongs_to :group
belongs_to :iterations_cadence, class_name: '::Iterations::Cadence', foreign_key: :iterations_cadence_id, inverse_of: :iterations
has_many :issues, foreign_key: 'sprint_id'
has_many :merge_requests, foreign_key: 'sprint_id'
has_internal_id :iid, scope: :project
has_internal_id :iid, scope: :group
validates :start_date, presence: true
validates :due_date, presence: true
validates :iterations_cadence, presence: true, unless: -> { project_id.present? }
validate :dates_do_not_overlap, if: :start_or_due_dates_changed?
validate :future_date, if: :start_or_due_dates_changed?, unless: :skip_future_date_validation
validate :no_project, unless: :skip_project_validation
validate :validate_group
before_validation :set_iterations_cadence, unless: -> { project_id.present? }
before_create :set_past_iteration_state
scope :upcoming, -> { with_state(:upcoming) }
scope :started, -> { with_state(:started) }
scope :closed, -> { with_state(:closed) }
scope :within_timeframe, -> (start_date, end_date) do
where('start_date <= ?', end_date).where('due_date >= ?', start_date)
end
scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) }
scope :due_date_passed, -> { where('due_date < ?', Date.current) }
state_machine :state_enum, initial: :upcoming do
event :start do
transition upcoming: :started
end
event :close do
transition [:upcoming, :started] => :closed
end
state :upcoming, value: Iteration::STATE_ENUM_MAP[:upcoming]
state :started, value: Iteration::STATE_ENUM_MAP[:started]
state :closed, value: Iteration::STATE_ENUM_MAP[:closed]
end
class << self
alias_method :with_state, :with_state_enum
alias_method :with_states, :with_state_enums
def reference_prefix
'*iteration:'
end
def reference_pattern
# NOTE: The id pattern only matches when all characters on the expression
# are digits, so it will match *iteration:2 but not *iteration:2.1 because that's probably a
# iteration name and we want it to be matched as such.
@reference_pattern ||= %r{
(#{::Project.reference_pattern})?
#{::Regexp.escape(reference_prefix)}
(?:
(?<iteration_id>
\d+(?!\S\w)\b # Integer-based iteration id, or
) |
(?<iteration_name>
[^"\s]+\b | # String-based single-word iteration title, or
"[^"]+" # String-based multi-word iteration surrounded in quotes
)
)
}x.freeze
end
def link_reference_pattern
@link_reference_pattern ||= super("iterations", /(?<iteration>\d+)/)
end
def filter_by_state(iterations, state)
case state
when 'closed' then iterations.closed
when 'started' then iterations.started
when 'upcoming' then iterations.upcoming
when 'opened' then iterations.started.or(iterations.upcoming)
when 'all' then iterations
else raise ArgumentError, "Unknown state filter: #{state}"
end
end
end
# For Iteration
class Predefined
None = ::Timebox::TimeboxStruct.new('None', 'none', ::Timebox::None.id).freeze
Any = ::Timebox::TimeboxStruct.new('Any', 'any', ::Timebox::Any.id).freeze
Current = ::Timebox::TimeboxStruct.new('Current', 'current', -4).freeze
ALL = [None, Any, Current].freeze
end
def state
STATE_ENUM_MAP.key(state_enum)
end
def state=(value)
self.state_enum = STATE_ENUM_MAP[value]
end
def resource_parent
group || project
end
# Show just the title when we manage to find an iteration, without the reference pattern,
# since it's long and unsightly.
def reference_link_text(from = nil)
self.title
end
def supports_timebox_charts?
resource_parent&.feature_available?(:iterations) && weight_available?
end
private
def timebox_format_reference(format = :id)
raise ::ArgumentError, _('Unknown format') unless [:id, :name].include?(format)
if format == :name
super
else
id
end
end
def parent_group
group || project.group
end
def start_or_due_dates_changed?
start_date_changed? || due_date_changed?
end
# ensure dates do not overlap with other Iterations in the same cadence tree
def dates_do_not_overlap
return unless iterations_cadence
return unless iterations_cadence.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
# for now we only have a single default cadence within a group just to wrap the iterations into a set.
# once we introduce multiple cadences per group we need to change this message.
# related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/299312
errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations within this group"))
end
def future_date
if start_or_due_dates_changed?
errors.add(:start_date, s_("Iteration|cannot be more than 500 years in the future")) if start_date > 500.years.from_now
errors.add(:due_date, s_("Iteration|cannot be more than 500 years in the future")) if due_date > 500.years.from_now
end
end
def no_project
return unless project_id.present?
errors.add(:project_id, s_("is not allowed. We do not currently support project-level iterations"))
end
def set_past_iteration_state
# if we create an iteration in the past, we set the state to closed right away,
# no need to wait for IterationsUpdateStatusWorker to do so.
self.state = :closed if due_date < Date.current
end
# TODO: this method should be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296099
def set_iterations_cadence
return if iterations_cadence
# For now we support only group iterations
# issue to clarify project iterations: https://gitlab.com/gitlab-org/gitlab/-/issues/299864
return unless group
# we need this as we use the cadence to validate the dates overlap for this iteration,
# so in the case this runs before background migration we need to first set all iterations
# in this group to a cadence before we can validate the dates overlap.
default_cadence = find_or_create_default_cadence
group.iterations.where(iterations_cadence_id: nil).update_all(iterations_cadence_id: default_cadence.id)
self.iterations_cadence = default_cadence
end
# TODO: this method should be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296099
def find_or_create_default_cadence
cadence_title = "#{group.name} Iterations"
start_date = self.start_date || Date.today
::Iterations::Cadence.create_with(
title: cadence_title,
start_date: start_date,
automatic: false,
# set to 0, i.e. unspecified when creating default iterations as we do validate for presence.
iterations_in_advance: 0,
duration_in_weeks: 0
).safe_find_or_create_by!(group: group)
end
# TODO: remove this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296100
def validate_group
return if iterations_cadence&.group_id == group_id
return unless iterations_cadence
errors.add(:group, s_('is not valid. The iteration group has to match the iteration cadence group.'))
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment