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

iteration.rb « models « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 012a062712f8363550f8dbcc0ca1babb83f2ac2d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# frozen_string_literal: true

class Iteration < ApplicationRecord
  self.table_name = 'sprints'

  attr_accessor :skip_future_date_validation
  attr_accessor :skip_project_validation

  STATE_ENUM_MAP = {
      upcoming: 1,
      started: 2,
      closed: 3
  }.with_indifferent_access.freeze

  include AtomicInternalId

  belongs_to :project
  belongs_to :group
  belongs_to :iterations_cadence, class_name: 'Iterations::Cadence', foreign_key: :iterations_cadence_id, inverse_of: :iterations

  has_internal_id :iid, scope: :project
  has_internal_id :iid, scope: :group

  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
  validate :no_project, unless: :skip_project_validation
  validate :validate_group

  before_create :set_iterations_cadence

  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 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

  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

  # 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 '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

    def reference_prefix
      '*iteration:'
    end

    def reference_pattern
      nil
    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

  private

  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 group/project tree
  def dates_do_not_overlap
    iterations = if parent_group.present? && resource_parent.is_a?(Project)
                   Iteration.where(group: parent_group.self_and_ancestors).or(project.iterations)
                 elsif parent_group.present?
                   Iteration.where(group: parent_group.self_and_ancestors)
                 else
                   project.iterations
                 end

    return unless iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?

    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?
      errors.add(:start_date, s_("Iteration|cannot be in the past")) if start_date < Date.current
      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?
      errors.add(:due_date, s_("Iteration|cannot be in the past")) if due_date < Date.current
      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

  # 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

    self.iterations_cadence = group.iterations_cadences.first || create_default_cadence
  end

  def create_default_cadence
    cadence_title = "#{group.name} Iterations"

    Iterations::Cadence.create!(group: group, title: cadence_title, start_date: start_date)
  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
end

Iteration.prepend_if_ee('EE::Iteration')