Commit 6b2a862f authored by Jarka Košanová's avatar Jarka Košanová

Wrap iterations to iteration_cadences

- add Cadence model and update associated models
- add methods to automatically scope iterations under cadences
- add/change respective specs
parent 7a4f9639
......@@ -34,6 +34,7 @@ class Group < Namespace
has_many :milestones
has_many :iterations
has_many :iteration_cadences, class_name: 'Iteration::Cadence'
has_many :services
has_many :shared_group_links, foreign_key: :shared_with_group_id, class_name: 'GroupGroupLink'
has_many :shared_with_group_links, foreign_key: :shared_group_id, class_name: 'GroupGroupLink'
......
......@@ -16,6 +16,7 @@ class Iteration < ApplicationRecord
belongs_to :project
belongs_to :group
belongs_to :iteration_cadence, class_name: 'Iteration::Cadence', foreign_key: :iteration_cadence_id, inverse_of: :iterations
has_internal_id :iid, scope: :project
has_internal_id :iid, scope: :group
......@@ -26,6 +27,9 @@ class Iteration < ApplicationRecord
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_iteration_cadence
scope :upcoming, -> { with_state(:upcoming) }
scope :started, -> { with_state(:started) }
......@@ -135,6 +139,30 @@ class Iteration < ApplicationRecord
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_iteration_cadence
return if iteration_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.iteration_cadence = group.iteration_cadences.first || create_default_cadence
end
def create_default_cadence
cadence_title = "#{group.name} Iterations"
Iteration::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 iteration_cadence
return if iteration_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')
# frozen_string_literal: true
class Iteration::Cadence < ApplicationRecord
self.table_name = 'iteration_cadences'
belongs_to :group
has_many :iterations, foreign_key: :iteration_cadence_id, inverse_of: :iteration_cadence
validates :title, presence: true
validates :start_date, presence: true
validates :group_id, presence: true
validates :active, presence: true
validates :automatic, presence: true
end
---
title: Add iteration_cadences table and respective model
merge_request: 50707
author:
type: other
......@@ -319,7 +319,9 @@ RSpec.describe Banzai::Filter::IterationReferenceFilter do
project_reference = another_project.to_reference_base(project)
input_text = "See #{project_reference}#{reference}"
iteration.update!(group: another_group)
# we have to update iteration_cadence group first in order to avoid invalid record
iteration.iteration_cadence.update_column(:group_id, another_group.id)
iteration.update_column(:group_id, another_group.id)
doc = reference_filter(input_text)
......@@ -328,7 +330,9 @@ RSpec.describe Banzai::Filter::IterationReferenceFilter do
end
it 'supports parent group references' do
iteration.update!(group: parent_group)
# we have to update iteration_cadence group first in order to avoid invallid record
iteration.iteration_cadence.update_column(:group_id, parent_group.id)
iteration.update_column(:group_id, parent_group.id)
doc = reference_filter("See #{reference}")
......
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop:disable Style/Documentation
class SetDefaultIterationCadences
class Iteration < ApplicationRecord
self.table_name = 'sprints'
end
class IterationCadence < ApplicationRecord
self.table_name = 'iteration_cadences'
include BulkInsertSafe
end
class Group < ApplicationRecord
self.table_name = 'namespaces'
end
def perform(*group_ids)
create_iteration_cadences(group_ids)
assign_iteration_cadences(group_ids)
end
private
def create_iteration_cadences(group_ids)
groups_with_cadence = IterationCadence.select(:group_id)
new_cadences = Group.where(id: group_ids).where.not(id: groups_with_cadence).map do |group|
last_iteration = Iteration.where(group_id: group.id).order(:start_date)&.last
next unless last_iteration
time = Time.now
IterationCadence.new(
group_id: group.id,
title: "#{group.name} Iterations",
start_date: last_iteration.start_date,
last_run_date: last_iteration.start_date,
automatic: false,
created_at: time,
updated_at: time
)
end
IterationCadence.bulk_insert!(new_cadences.compact)
end
def assign_iteration_cadences(group_ids)
IterationCadence.where(group_id: group_ids).each do |cadence|
Iteration.where(iteration_cadence_id: nil).where(group_id: cadence.group_id).update_all(iteration_cadence_id: cadence.id)
end
end
end
end
end
......@@ -34927,6 +34927,9 @@ msgstr ""
msgid "is not in the group enforcing Group Managed Account"
msgstr ""
msgid "is not valid. The iteration group has to match the iteration cadence group."
msgstr ""
msgid "is read only"
msgstr ""
......
# frozen_string_literal: true
FactoryBot.define do
sequence(:cadence_sequential_date) do |n|
n.days.from_now
end
factory :iteration_cadence, class: 'Iteration::Cadence' do
title
group
start_date { generate(:cadence_sequential_date) }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::SetDefaultIterationCadences, schema: 20201231133921 do
let(:namespaces) { table(:namespaces) }
let(:iterations) { table(:sprints) }
let(:iteration_cadences) { table(:iteration_cadences) }
describe '#perform' do
context 'when no iteration cadences exists' do
let!(:group_1) { namespaces.create!(name: 'group 1', path: 'group-1') }
let!(:group_2) { namespaces.create!(name: 'group 2', path: 'group-2') }
let!(:group_3) { namespaces.create!(name: 'group 3', path: 'group-3') }
let!(:iteration_1) { iterations.create!(group_id: group_1.id, iid: 1, title: 'Iteration 1', start_date: 10.days.ago, due_date: 8.days.ago) }
let!(:iteration_2) { iterations.create!(group_id: group_3.id, iid: 1, title: 'Iteration 2', start_date: 10.days.ago, due_date: 8.days.ago) }
let!(:iteration_3) { iterations.create!(group_id: group_3.id, iid: 1, title: 'Iteration 3', start_date: 5.days.ago, due_date: 2.days.ago) }
before do
described_class.new.perform(group_1.id, group_2.id, group_3.id, namespaces.last.id + 1)
end
it 'creates iteration_cadence records for the requested groups' do
expect(iteration_cadences.count).to eq(2)
end
it 'assigns the iteration cadences to the iterations correctly' do
iteration_cadence = iteration_cadences.find_by(group_id: group_1.id)
iteration_records = iterations.where(iteration_cadence_id: iteration_cadence.id)
expect(iteration_cadence.start_date).to eq(iteration_1.start_date)
expect(iteration_cadence.last_run_date).to eq(iteration_1.start_date)
expect(iteration_cadence.title).to eq('group 1 Iterations')
expect(iteration_records.size).to eq(1)
expect(iteration_records.first.id).to eq(iteration_1.id)
iteration_cadence = iteration_cadences.find_by(group_id: group_3.id)
iteration_records = iterations.where(iteration_cadence_id: iteration_cadence.id)
expect(iteration_cadence.start_date).to eq(iteration_3.start_date)
expect(iteration_cadence.last_run_date).to eq(iteration_3.start_date)
expect(iteration_cadence.title).to eq('group 3 Iterations')
expect(iteration_records.size).to eq(2)
expect(iteration_records.first.id).to eq(iteration_2.id)
expect(iteration_records.second.id).to eq(iteration_3.id)
end
end
context 'when an iteration cadence exists for a group' do
let!(:group) { namespaces.create!(name: 'group', path: 'group') }
let!(:iteration_cadence_1) { iteration_cadences.create!(group_id: group.id, start_date: 5.days.ago, title: 'Cadence 1') }
let!(:iteration_cadence_2) { iteration_cadences.create!(group_id: group.id, start_date: 2.days.ago, title: 'Cadence 2') }
let!(:iteration_1) { iterations.create!(group_id: group.id, iid: 1, title: 'Iteration 1', start_date: 10.days.ago, due_date: 8.days.ago) }
let!(:iteration_2) { iterations.create!(group_id: group.id, iteration_cadence_id: iteration_cadence_1.id, iid: 2, title: 'Iteration 2', start_date: 5.days.ago, due_date: 3.days.ago) }
let!(:iteration_3) { iterations.create!(group_id: group.id, iteration_cadence_id: iteration_cadence_2.id, iid: 3, title: 'Iteration 3', start_date: 2.days.ago, due_date: 1.day.ago) }
subject { described_class.new.perform(group.id) }
it 'does not create a new iteration_cadence' do
expect { subject }.not_to change { iteration_cadences.count }
end
it 'assigns iteration cadences to iterations if needed' do
subject
expect(iteration_1.reload.iteration_cadence_id).to eq(iteration_cadence_1.id)
expect(iteration_2.reload.iteration_cadence_id).to eq(iteration_cadence_1.id)
expect(iteration_3.reload.iteration_cadence_id).to eq(iteration_cadence_2.id)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleSetDefaultIterationCadences do
let(:namespaces) { table(:namespaces) }
let(:iterations) { table(:sprints) }
let(:group_1) { namespaces.create!(name: 'test_1', path: 'test_1') }
let!(:group_2) { namespaces.create!(name: 'test_2', path: 'test_2') }
let(:group_3) { namespaces.create!(name: 'test_3', path: 'test_3') }
let(:group_4) { namespaces.create!(name: 'test_4', path: 'test_4') }
let(:group_5) { namespaces.create!(name: 'test_5', path: 'test_5') }
let(:group_6) { namespaces.create!(name: 'test_6', path: 'test_6') }
let(:group_7) { namespaces.create!(name: 'test_7', path: 'test_7') }
let(:group_8) { namespaces.create!(name: 'test_8', path: 'test_8') }
let!(:iteration_1) { iterations.create!(iid: 1, title: 'iteration 1', group_id: group_1.id) }
let!(:iteration_2) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_3.id) }
let!(:iteration_3) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_4.id) }
let!(:iteration_4) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_5.id) }
let!(:iteration_5) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_6.id) }
let!(:iteration_6) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_7.id) }
let!(:iteration_7) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_8.id) }
around do |example|
freeze_time { Sidekiq::Testing.fake! { example.run } }
end
it 'schedules the background jobs', :aggregate_failures do
stub_const("#{described_class.name}::BATCH_SIZE", 3)
migrate!
expect(BackgroundMigrationWorker.jobs.size).to be(3)
expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(2.minutes, group_1.id, group_3.id, group_4.id)
expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(4.minutes, group_5.id, group_6.id, group_7.id)
expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(6.minutes, group_8.id)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Iteration::Cadence do
describe 'associations' do
subject { build(:iteration_cadence) }
it { is_expected.to belong_to(:group) }
it { is_expected.to have_many(:iterations).inverse_of(:iteration_cadence) }
end
describe 'validations' do
subject { build(:iteration_cadence) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:start_date) }
it { is_expected.to validate_presence_of(:group_id) }
it { is_expected.to validate_presence_of(:active) }
it { is_expected.to validate_presence_of(:automatic) }
end
end
......@@ -5,6 +5,13 @@ require 'spec_helper'
RSpec.describe Iteration do
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let(:set_cadence) { nil }
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:iteration_cadence).inverse_of(:iterations) }
end
describe "#iid" do
it "is properly scoped on project and group" do
......@@ -32,6 +39,59 @@ RSpec.describe Iteration do
end
end
describe 'setting iteration cadence' do
let_it_be(:iteration_cadence) { create(:iteration_cadence, group: group, start_date: 10.days.ago) }
let(:iteration) { create(:iteration, group: group, iteration_cadence: set_cadence, start_date: 2.days.from_now) }
context 'when iteration_cadence is set correctly' do
let(:set_cadence) { iteration_cadence}
it 'does not change the iteration_cadence' do
expect(iteration.iteration_cadence).to eq(iteration_cadence)
end
end
context 'when iteration_cadence exists for the group' do
let(:set_cadence) { nil }
it 'sets the iteration_cadence to the existing record' do
expect(iteration.iteration_cadence).to eq(iteration_cadence)
end
end
context 'when iteration_cadence does not exists for the group' do
let_it_be(:group) { create(:group, name: 'Test group')}
let(:iteration) { build(:iteration, group: group, iteration_cadence: set_cadence) }
it 'creates a default iteration_cadence and uses it for the iteration' do
expect { iteration.save! }.to change { Iteration::Cadence.count }.by(1)
end
it 'sets the newly created iteration_cadence to the reecord' do
iteration.save!
expect(iteration.iteration_cadence).to eq(Iteration::Cadence.last)
end
it 'creates the iteration_cadence with the correct attributes' do
iteration.save!
cadence = Iteration::Cadence.last
expect(cadence.reload.start_date).to eq(iteration.start_date)
expect(cadence.title).to eq('Test group Iterations')
end
end
context 'when iteration is a project iteration' do
it 'does not set the iteration_cadence' do
iteration = create(:iteration, iteration_cadence: nil, project: project, skip_project_validation: true)
expect(iteration.reload.iteration_cadence).to be_nil
end
end
end
describe '.filter_by_state' do
let_it_be(:closed_iteration) { create(:iteration, :closed, :skip_future_date_validation, group: group, start_date: 8.days.ago, due_date: 2.days.ago) }
let_it_be(:started_iteration) { create(:iteration, :started, :skip_future_date_validation, group: group, start_date: 1.day.ago, due_date: 6.days.from_now) }
......@@ -307,6 +367,43 @@ RSpec.describe Iteration do
end
end
describe '#validate_group' do
let_it_be(:iteration_cadence) { create(:iteration_cadence, group: group) }
context 'when the iteration and iteration cadence groups are same' do
it 'is valid' do
iteration = build(:iteration, group: group, iteration_cadence: iteration_cadence)
expect(iteration).to be_valid
end
end
context 'when the iteration and iteration cadence groups are different' do
it 'is invalid' do
other_group = create(:group)
iteration = build(:iteration, group: other_group, iteration_cadence: iteration_cadence)
expect(iteration).not_to be_valid
end
end
context 'when the iteration belongs to a project and the iteration cadence is set' do
it 'is invalid' do
iteration = build(:iteration, project: project, iteration_cadence: iteration_cadence, skip_project_validation: true)
expect(iteration).to be_invalid
end
end
context 'when the iteration belongs to a project and the iteration cadence is not set' do
it 'is valid' do
iteration = build(:iteration, project: project, skip_project_validation: true)
expect(iteration).to be_valid
end
end
end
describe '.within_timeframe' do
let_it_be(:now) { Time.current }
let_it_be(:project) { create(:project, :empty_repo) }
......
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