2020-04-23 17:09:31 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-05-14 08:08:21 -04:00
|
|
|
class Iteration < ApplicationRecord
|
|
|
|
self.table_name = 'sprints'
|
|
|
|
|
2020-05-15 05:07:59 -04:00
|
|
|
attr_accessor :skip_future_date_validation
|
2020-08-06 23:10:05 -04:00
|
|
|
attr_accessor :skip_project_validation
|
2020-05-15 05:07:59 -04:00
|
|
|
|
|
|
|
STATE_ENUM_MAP = {
|
|
|
|
upcoming: 1,
|
|
|
|
started: 2,
|
|
|
|
closed: 3
|
2020-04-27 05:09:51 -04:00
|
|
|
}.with_indifferent_access.freeze
|
|
|
|
|
|
|
|
include AtomicInternalId
|
2021-02-23 16:10:44 -05:00
|
|
|
include Timebox
|
2020-04-27 05:09:51 -04:00
|
|
|
|
2020-04-23 17:09:31 -04:00
|
|
|
belongs_to :project
|
|
|
|
belongs_to :group
|
2021-02-17 10:09:21 -05:00
|
|
|
belongs_to :iterations_cadence, class_name: 'Iterations::Cadence', foreign_key: :iterations_cadence_id, inverse_of: :iterations
|
2020-04-27 05:09:51 -04:00
|
|
|
|
2020-11-10 19:08:58 -05:00
|
|
|
has_internal_id :iid, scope: :project
|
|
|
|
has_internal_id :iid, scope: :group
|
2020-04-30 20:09:59 -04:00
|
|
|
|
2020-05-15 05:07:59 -04:00
|
|
|
validates :start_date, presence: true
|
|
|
|
validates :due_date, presence: true
|
2021-02-23 16:10:44 -05:00
|
|
|
validates :iterations_cadence, presence: true, unless: -> { project_id.present? }
|
2020-05-15 05:07:59 -04:00
|
|
|
|
|
|
|
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
|
2020-08-06 23:10:05 -04:00
|
|
|
validate :no_project, unless: :skip_project_validation
|
2021-02-17 10:09:21 -05:00
|
|
|
validate :validate_group
|
|
|
|
|
2021-02-23 16:10:44 -05:00
|
|
|
before_validation :set_iterations_cadence, unless: -> { project_id.present? }
|
|
|
|
before_create :set_past_iteration_state
|
2020-05-15 05:07:59 -04:00
|
|
|
|
|
|
|
scope :upcoming, -> { with_state(:upcoming) }
|
|
|
|
scope :started, -> { with_state(:started) }
|
2020-09-17 17:09:39 -04:00
|
|
|
scope :closed, -> { with_state(:closed) }
|
2020-05-15 05:07:59 -04:00
|
|
|
|
2020-05-26 20:08:11 -04:00
|
|
|
scope :within_timeframe, -> (start_date, end_date) do
|
2021-02-23 16:10:44 -05:00
|
|
|
where('start_date <= ?', end_date).where('due_date >= ?', start_date)
|
2020-05-26 20:08:11 -04:00
|
|
|
end
|
|
|
|
|
2020-09-02 02:10:47 -04:00
|
|
|
scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) }
|
|
|
|
scope :due_date_passed, -> { where('due_date < ?', Date.current) }
|
2020-07-15 02:09:35 -04:00
|
|
|
|
2020-05-15 05:07:59 -04:00
|
|
|
state_machine :state_enum, initial: :upcoming do
|
|
|
|
event :start do
|
|
|
|
transition upcoming: :started
|
|
|
|
end
|
|
|
|
|
2020-04-30 20:09:59 -04:00
|
|
|
event :close do
|
2020-05-15 05:07:59 -04:00
|
|
|
transition [:upcoming, :started] => :closed
|
2020-04-30 20:09:59 -04:00
|
|
|
end
|
|
|
|
|
2020-05-15 05:07:59 -04:00
|
|
|
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
|
|
|
|
|
|
|
|
# Alias to state machine .with_state_enum method
|
|
|
|
# This needs to be defined after the state machine block to avoid errors
|
|
|
|
class << self
|
|
|
|
alias_method :with_state, :with_state_enum
|
|
|
|
alias_method :with_states, :with_state_enums
|
|
|
|
|
|
|
|
def filter_by_state(iterations, state)
|
|
|
|
case state
|
|
|
|
when 'closed' then iterations.closed
|
|
|
|
when 'started' then iterations.started
|
2020-09-17 17:09:39 -04:00
|
|
|
when 'upcoming' then iterations.upcoming
|
2020-05-15 05:07:59 -04:00
|
|
|
when 'opened' then iterations.started.or(iterations.upcoming)
|
|
|
|
when 'all' then iterations
|
2020-09-17 17:09:39 -04:00
|
|
|
else raise ArgumentError, "Unknown state filter: #{state}"
|
2020-05-15 05:07:59 -04:00
|
|
|
end
|
2020-04-30 20:09:59 -04:00
|
|
|
end
|
2020-05-26 17:07:45 -04:00
|
|
|
|
|
|
|
def reference_prefix
|
|
|
|
'*iteration:'
|
|
|
|
end
|
|
|
|
|
|
|
|
def reference_pattern
|
|
|
|
nil
|
|
|
|
end
|
2020-05-15 05:07:59 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def state
|
|
|
|
STATE_ENUM_MAP.key(state_enum)
|
|
|
|
end
|
2020-04-30 20:09:59 -04:00
|
|
|
|
2020-05-15 05:07:59 -04:00
|
|
|
def state=(value)
|
|
|
|
self.state_enum = STATE_ENUM_MAP[value]
|
|
|
|
end
|
|
|
|
|
2020-05-26 20:08:11 -04:00
|
|
|
def resource_parent
|
|
|
|
group || project
|
|
|
|
end
|
|
|
|
|
2020-05-15 05:07:59 -04:00
|
|
|
private
|
|
|
|
|
2020-09-28 11:09:44 -04:00
|
|
|
def parent_group
|
|
|
|
group || project.group
|
|
|
|
end
|
|
|
|
|
2020-05-15 05:07:59 -04:00
|
|
|
def start_or_due_dates_changed?
|
|
|
|
start_date_changed? || due_date_changed?
|
|
|
|
end
|
|
|
|
|
2021-02-23 16:10:44 -05:00
|
|
|
# ensure dates do not overlap with other Iterations in the same cadence tree
|
2020-05-15 05:07:59 -04:00
|
|
|
def dates_do_not_overlap
|
2021-02-23 16:10:44 -05:00
|
|
|
return unless iterations_cadence
|
|
|
|
return unless iterations_cadence.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
|
2020-05-15 05:07:59 -04:00
|
|
|
|
2021-02-23 16:10:44 -05:00
|
|
|
# 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"))
|
2020-05-15 05:07:59 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def future_date
|
2021-02-23 16:10:44 -05:00
|
|
|
if start_or_due_dates_changed?
|
2020-05-15 05:07:59 -04:00
|
|
|
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
|
2020-04-30 20:09:59 -04:00
|
|
|
end
|
2020-08-06 23:10:05 -04:00
|
|
|
|
|
|
|
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
|
2021-02-17 10:09:21 -05:00
|
|
|
|
2021-02-23 16:10:44 -05:00
|
|
|
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
|
|
|
|
|
2021-02-17 10:09:21 -05:00
|
|
|
# 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
|
|
|
|
|
2021-02-23 16:10:44 -05:00
|
|
|
# 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
|
2021-02-17 10:09:21 -05:00
|
|
|
end
|
|
|
|
|
2021-02-23 16:10:44 -05:00
|
|
|
def find_or_create_default_cadence
|
2021-02-17 10:09:21 -05:00
|
|
|
cadence_title = "#{group.name} Iterations"
|
2021-02-23 16:10:44 -05:00
|
|
|
start_date = self.start_date || Date.today
|
2021-02-17 10:09:21 -05:00
|
|
|
|
2021-02-23 16:10:44 -05:00
|
|
|
::Iterations::Cadence.create_with(title: cadence_title, start_date: start_date).safe_find_or_create_by!(group: group)
|
2021-02-17 10:09:21 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# TODO: remove this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296100
|
|
|
|
def validate_group
|
|
|
|
return unless iterations_cadence
|
|
|
|
return if iterations_cadence.group_id == group_id
|
|
|
|
|
|
|
|
errors.add(:group, s_('is not valid. The iteration group has to match the iteration cadence group.'))
|
|
|
|
end
|
2020-04-23 17:09:31 -04:00
|
|
|
end
|
2020-05-26 17:07:45 -04:00
|
|
|
|
|
|
|
Iteration.prepend_if_ee('EE::Iteration')
|