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
|
|
|
|
|
2020-04-23 17:09:31 -04:00
|
|
|
belongs_to :project
|
|
|
|
belongs_to :group
|
2020-04-27 05:09:51 -04:00
|
|
|
|
2020-05-14 08:08:21 -04: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-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
|
|
|
|
|
|
|
|
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
|
2020-05-15 05:07:59 -04:00
|
|
|
|
|
|
|
scope :upcoming, -> { with_state(:upcoming) }
|
|
|
|
scope :started, -> { with_state(:started) }
|
|
|
|
|
2020-05-26 20:08:11 -04: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 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
|
|
|
|
when 'opened' then iterations.started.or(iterations.upcoming)
|
|
|
|
when 'all' then iterations
|
|
|
|
else iterations.upcoming
|
|
|
|
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
|
|
|
|
|
|
|
|
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 08:08:51 -04:00
|
|
|
return unless resource_parent.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
|
2020-05-15 05:07:59 -04: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 02:08:03 -04:00
|
|
|
errors.add(:start_date, s_("Iteration|cannot be in the past")) if start_date < Date.current
|
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
|
|
|
|
end
|
|
|
|
|
|
|
|
if due_date_changed?
|
2020-05-19 02:08:03 -04:00
|
|
|
errors.add(:due_date, s_("Iteration|cannot be in the past")) if due_date < Date.current
|
2020-05-15 05:07:59 -04: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-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
|
2020-04-23 17:09:31 -04:00
|
|
|
end
|
2020-05-26 17:07:45 -04:00
|
|
|
|
|
|
|
Iteration.prepend_if_ee('EE::Iteration')
|