2020-04-23 21:09:31 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-05-14 12:08:21 +00:00
|
|
|
class Iteration < ApplicationRecord
|
|
|
|
self.table_name = 'sprints'
|
|
|
|
|
2020-05-15 09:07:59 +00:00
|
|
|
attr_accessor :skip_future_date_validation
|
2020-08-07 03:10:05 +00:00
|
|
|
attr_accessor :skip_project_validation
|
2020-05-15 09:07:59 +00:00
|
|
|
|
|
|
|
STATE_ENUM_MAP = {
|
|
|
|
upcoming: 1,
|
|
|
|
started: 2,
|
|
|
|
closed: 3
|
2020-04-27 09:09:51 +00:00
|
|
|
}.with_indifferent_access.freeze
|
|
|
|
|
|
|
|
include AtomicInternalId
|
|
|
|
|
2020-04-23 21:09:31 +00:00
|
|
|
belongs_to :project
|
|
|
|
belongs_to :group
|
2020-04-27 09:09:51 +00:00
|
|
|
|
2020-05-14 12:08:21 +00:00
|
|
|
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.iterations&.maximum(:iid) }
|
|
|
|
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.iterations&.maximum(:iid) }
|
2020-05-01 00:09:59 +00:00
|
|
|
|
2020-05-15 09:07:59 +00:00
|
|
|
validates :start_date, presence: true
|
|
|
|
validates :due_date, presence: true
|
|
|
|
|
|
|
|
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-07 03:10:05 +00:00
|
|
|
validate :no_project, unless: :skip_project_validation
|
2020-05-15 09:07:59 +00:00
|
|
|
|
|
|
|
scope :upcoming, -> { with_state(:upcoming) }
|
|
|
|
scope :started, -> { with_state(:started) }
|
|
|
|
|
2020-05-27 00:08:11 +00:00
|
|
|
scope :within_timeframe, -> (start_date, end_date) do
|
|
|
|
where('start_date is not NULL or due_date is not NULL')
|
|
|
|
.where('start_date is NULL or start_date <= ?', end_date)
|
|
|
|
.where('due_date is NULL or due_date >= ?', start_date)
|
|
|
|
end
|
|
|
|
|
2020-09-02 06:10:47 +00: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 06:09:35 +00:00
|
|
|
|
2020-05-15 09:07:59 +00:00
|
|
|
state_machine :state_enum, initial: :upcoming do
|
|
|
|
event :start do
|
|
|
|
transition upcoming: :started
|
|
|
|
end
|
|
|
|
|
2020-05-01 00:09:59 +00:00
|
|
|
event :close do
|
2020-05-15 09:07:59 +00:00
|
|
|
transition [:upcoming, :started] => :closed
|
2020-05-01 00:09:59 +00:00
|
|
|
end
|
|
|
|
|
2020-05-15 09:07:59 +00: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
|
|
|
|
when 'opened' then iterations.started.or(iterations.upcoming)
|
|
|
|
when 'all' then iterations
|
|
|
|
else iterations.upcoming
|
|
|
|
end
|
2020-05-01 00:09:59 +00:00
|
|
|
end
|
2020-05-26 21:07:45 +00:00
|
|
|
|
|
|
|
def reference_prefix
|
|
|
|
'*iteration:'
|
|
|
|
end
|
|
|
|
|
|
|
|
def reference_pattern
|
|
|
|
nil
|
|
|
|
end
|
2020-05-15 09:07:59 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def state
|
|
|
|
STATE_ENUM_MAP.key(state_enum)
|
|
|
|
end
|
2020-05-01 00:09:59 +00:00
|
|
|
|
2020-05-15 09:07:59 +00:00
|
|
|
def state=(value)
|
|
|
|
self.state_enum = STATE_ENUM_MAP[value]
|
|
|
|
end
|
|
|
|
|
2020-05-27 00:08:11 +00:00
|
|
|
def resource_parent
|
|
|
|
group || project
|
|
|
|
end
|
|
|
|
|
2020-05-15 09:07:59 +00:00
|
|
|
private
|
|
|
|
|
|
|
|
def start_or_due_dates_changed?
|
|
|
|
start_date_changed? || due_date_changed?
|
|
|
|
end
|
|
|
|
|
|
|
|
# ensure dates do not overlap with other Iterations in the same group/project
|
|
|
|
def dates_do_not_overlap
|
2020-06-26 12:08:51 +00:00
|
|
|
return unless resource_parent.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
|
2020-05-15 09:07:59 +00:00
|
|
|
|
|
|
|
errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations"))
|
|
|
|
end
|
|
|
|
|
|
|
|
# ensure dates are in the future
|
|
|
|
def future_date
|
|
|
|
if start_date_changed?
|
2020-05-19 06:08:03 +00:00
|
|
|
errors.add(:start_date, s_("Iteration|cannot be in the past")) if start_date < Date.current
|
2020-05-15 09:07:59 +00:00
|
|
|
errors.add(:start_date, s_("Iteration|cannot be more than 500 years in the future")) if start_date > 500.years.from_now
|
|
|
|
end
|
|
|
|
|
|
|
|
if due_date_changed?
|
2020-05-19 06:08:03 +00:00
|
|
|
errors.add(:due_date, s_("Iteration|cannot be in the past")) if due_date < Date.current
|
2020-05-15 09:07:59 +00:00
|
|
|
errors.add(:due_date, s_("Iteration|cannot be more than 500 years in the future")) if due_date > 500.years.from_now
|
|
|
|
end
|
2020-05-01 00:09:59 +00:00
|
|
|
end
|
2020-08-07 03:10:05 +00: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
|
2020-04-23 21:09:31 +00:00
|
|
|
end
|
2020-05-26 21:07:45 +00:00
|
|
|
|
|
|
|
Iteration.prepend_if_ee('EE::Iteration')
|