Commit f810b383 authored by Andreas Brandl's avatar Andreas Brandl

Merge branch 'scope-board-to-iteration-cadence' into 'master'

Scope board to iteration cadence

See merge request gitlab-org/gitlab!69030
parents 086fb4d2 3015a023
# frozen_string_literal: true
class AddIterationCadenceIdToIssueBoards < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def change
add_column :boards, :iteration_cadence_id, :bigint
end
end
# frozen_string_literal: true
class AddFkToIterationCadenceIdOnBoards < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'index_boards_on_iteration_cadence_id'
def up
add_concurrent_index :boards, :iteration_cadence_id, name: INDEX_NAME
add_concurrent_foreign_key :boards, :iterations_cadences, column: :iteration_cadence_id
end
def down
with_lock_retries do
remove_foreign_key_if_exists :boards, column: :iteration_cadence_id
end
remove_concurrent_index_by_name :boards, INDEX_NAME
end
end
# frozen_string_literal: true
class BackfillCadenceIdForBoardsScopedToIteration < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
BATCH_SIZE = 1000
DELAY = 2.minutes.to_i
MIGRATION = 'BackfillIterationCadenceIdForBoards'
class MigrationBoard < ApplicationRecord
include EachBatch
self.table_name = 'boards'
end
def up
schedule_backfill_group_boards
schedule_backfill_project_boards
end
def down
MigrationBoard.where.not(iteration_cadence_id: nil).each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first
delay = index * DELAY
migrate_in(delay, MIGRATION, ['none', 'down', *range])
end
end
private
def schedule_backfill_project_boards
MigrationBoard.where(iteration_id: -4).where.not(project_id: nil).where(iteration_cadence_id: nil).each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first
delay = index * DELAY
migrate_in(delay, MIGRATION, ['project', 'up', *range])
end
end
def schedule_backfill_group_boards
MigrationBoard.where(iteration_id: -4).where.not(group_id: nil).where(iteration_cadence_id: nil).each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first
delay = index * DELAY
migrate_in(delay, MIGRATION, ['group', 'up', *range])
end
end
end
d9c7cc7721b28cbd442bf40255ecfbd20d0abf4cd31631c150ebdc05c76062be
\ No newline at end of file
b97b77aef61db2e51106ac090f5511a67fa85be8f3741f618fe03c8c03ecd88c
\ No newline at end of file
fd7aef11635bc4c5d6b9346dbed90f6c114da7b7a33744083e8610f3850e4736
\ No newline at end of file
......@@ -10894,7 +10894,8 @@ CREATE TABLE boards (
weight integer,
hide_backlog_list boolean DEFAULT false NOT NULL,
hide_closed_list boolean DEFAULT false NOT NULL,
iteration_id bigint
iteration_id bigint,
iteration_cadence_id bigint
);
CREATE TABLE boards_epic_board_labels (
......@@ -24405,6 +24406,8 @@ CREATE INDEX index_boards_epic_user_preferences_on_user_id ON boards_epic_user_p
CREATE INDEX index_boards_on_group_id ON boards USING btree (group_id);
CREATE INDEX index_boards_on_iteration_cadence_id ON boards USING btree (iteration_cadence_id);
CREATE INDEX index_boards_on_iteration_id ON boards USING btree (iteration_id);
CREATE INDEX index_boards_on_milestone_id ON boards USING btree (milestone_id);
......@@ -27911,6 +27914,9 @@ ALTER TABLE ONLY alert_management_alerts
ALTER TABLE ONLY identities
ADD CONSTRAINT fk_aade90f0fc FOREIGN KEY (saml_provider_id) REFERENCES saml_providers(id) ON DELETE CASCADE;
ALTER TABLE ONLY boards
ADD CONSTRAINT fk_ab0a250ff6 FOREIGN KEY (iteration_cadence_id) REFERENCES iterations_cadences(id) ON DELETE CASCADE;
ALTER TABLE ONLY dep_ci_build_trace_sections
ADD CONSTRAINT fk_ab7c104e26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......@@ -1075,6 +1075,7 @@ Input type: `CreateBoardInput`
| <a id="mutationcreateboardgrouppath"></a>`groupPath` | [`ID`](#id) | Full path of the group with which the resource is associated. |
| <a id="mutationcreateboardhidebackloglist"></a>`hideBacklogList` | [`Boolean`](#boolean) | Whether or not backlog list is hidden. |
| <a id="mutationcreateboardhideclosedlist"></a>`hideClosedList` | [`Boolean`](#boolean) | Whether or not closed list is hidden. |
| <a id="mutationcreateboarditerationcadenceid"></a>`iterationCadenceId` | [`IterationsCadenceID`](#iterationscadenceid) | ID of iteration cadence to be assigned to the board. |
| <a id="mutationcreateboarditerationid"></a>`iterationId` | [`IterationID`](#iterationid) | ID of iteration to be assigned to the board. |
| <a id="mutationcreateboardlabelids"></a>`labelIds` | [`[LabelID!]`](#labelid) | IDs of labels to be added to the board. |
| <a id="mutationcreateboardlabels"></a>`labels` | [`[String!]`](#string) | Labels of the issue. |
......@@ -4122,6 +4123,7 @@ Input type: `UpdateBoardInput`
| <a id="mutationupdateboardhidebackloglist"></a>`hideBacklogList` | [`Boolean`](#boolean) | Whether or not backlog list is hidden. |
| <a id="mutationupdateboardhideclosedlist"></a>`hideClosedList` | [`Boolean`](#boolean) | Whether or not closed list is hidden. |
| <a id="mutationupdateboardid"></a>`id` | [`BoardID!`](#boardid) | Board global ID. |
| <a id="mutationupdateboarditerationcadenceid"></a>`iterationCadenceId` | [`IterationsCadenceID`](#iterationscadenceid) | ID of iteration cadence to be assigned to the board. |
| <a id="mutationupdateboarditerationid"></a>`iterationId` | [`IterationID`](#iterationid) | ID of iteration to be assigned to the board. |
| <a id="mutationupdateboardlabelids"></a>`labelIds` | [`[LabelID!]`](#labelid) | IDs of labels to be added to the board. |
| <a id="mutationupdateboardlabels"></a>`labels` | [`[String!]`](#string) | Labels of the issue. |
......@@ -7897,6 +7899,7 @@ Represents a project or group issue board.
| <a id="boardhideclosedlist"></a>`hideClosedList` | [`Boolean`](#boolean) | Whether or not closed list is hidden. |
| <a id="boardid"></a>`id` | [`ID!`](#id) | ID (global ID) of the board. |
| <a id="boarditeration"></a>`iteration` | [`Iteration`](#iteration) | Board iteration. |
| <a id="boarditerationcadence"></a>`iterationCadence` | [`IterationCadence`](#iterationcadence) | Board iteration cadence. |
| <a id="boardlabels"></a>`labels` | [`LabelConnection`](#labelconnection) | Labels of the board. (see [Connections](#connections)) |
| <a id="boardmilestone"></a>`milestone` | [`Milestone`](#milestone) | Board milestone. |
| <a id="boardname"></a>`name` | [`String`](#string) | Name of the board. |
......
......@@ -13,6 +13,7 @@ module Iterations
end
def execute
raise ArgumentError, 'group argument is missing' unless group.present?
return Iterations::Cadence.none unless group.iteration_cadences_feature_flag_enabled?
items = Iterations::Cadence.all
......
......@@ -23,6 +23,9 @@ module EE
field :iteration, type: ::Types::IterationType, null: true,
description: 'Board iteration.'
field :iteration_cadence, type: ::Types::Iterations::CadenceType, null: true,
description: 'Board iteration cadence.'
field :weight, type: GraphQL::Types::Int, null: true,
description: 'Weight of the board.'
end
......
......@@ -25,6 +25,11 @@ module Mutations
required: false,
description: 'ID of iteration to be assigned to the board.'
argument :iteration_cadence_id,
::Types::GlobalIDType[::Iterations::Cadence],
required: false,
description: 'ID of iteration cadence to be assigned to the board.'
argument :weight,
GraphQL::Types::Int,
required: false,
......
......@@ -55,7 +55,8 @@ module Resolvers
raise raise_resource_not_available_error!('The project does not have a parent group. Iteration cadences are only supported only at group level.') if @parent.group.blank?
@parent.group
else raise "Unexpected parent type: #{@parent.class}"
else
raise "Unexpected parent type: #{@parent.class}"
end
end
......
......@@ -11,6 +11,7 @@ module EE
prepended do
belongs_to :milestone
belongs_to :iteration
belongs_to :iteration_cadence, class_name: 'Iterations::Cadence'
has_many :board_labels
has_many :user_preferences, class_name: 'BoardUserPreference', inverse_of: :board
......@@ -26,6 +27,7 @@ module EE
scope :with_associations, -> { preload(:destroyable_lists, :labels, :assignee) }
scope :in_iterations, ->(iterations) { where(iteration: iterations) }
scope :in_iteration_cadences, ->(cadences) { where(iteration_cadence: cadences) }
end
override :scoped?
......
......@@ -17,6 +17,10 @@ module EE
Current = ::Timebox::TimeboxStruct.new('Current', 'current', -4).freeze
ALL = [None, Any, Current].freeze
def self.by_id(id)
::Iteration::Predefined::ALL.index_by(&:id)[id]
end
end
prepended do
......
......@@ -17,13 +17,8 @@ module EE
return if params[:milestone_id].blank?
return if ::Milestone::Predefined::ALL.map(&:id).include?(params[:milestone_id].to_i)
finder_params =
case parent
when Group
{ group_ids: parent.self_and_ancestors }
when Project
{ project_ids: [parent.id], group_ids: parent.group&.self_and_ancestors }
end
finder_params = { group_ids: group&.self_and_ancestors }
finder_params[:project_ids] = [parent.id] if parent.is_a?(Project)
milestone = ::MilestonesFinder.new(finder_params).find_by(id: params[:milestone_id])
......@@ -31,13 +26,16 @@ module EE
end
# rubocop: enable CodeReuse/ActiveRecord
def filter_iteration
return if params[:iteration_id].blank?
return if ::Iteration::Predefined::ALL.map(&:id).include?(params[:iteration_id].to_i)
def filter_iteration_and_iteration_cadence
return if params[:iteration_id].blank? && params[:iteration_cadence_id].blank?
iteration = IterationsFinder.new(current_user, iterations_finder_params).find_by(id: params[:iteration_id]) # rubocop: disable CodeReuse/ActiveRecord
if params[:iteration_id].present? && !wildcard_iteration_id?
filter_iteration
else
filter_iteration_cadence
end
params.delete(:iteration_id) unless iteration
ensure_iteration_cadence if wildcard_iteration_id?
end
def filter_labels
......@@ -52,8 +50,55 @@ module EE
@labels_service ||= ::Labels::AvailableLabelsService.new(current_user, parent, params)
end
private
def filter_iteration
iteration = IterationsFinder.new(current_user, iterations_finder_params).execute.first
if !iteration
params.delete(:iteration_id)
params.delete(:iteration_cadence_id)
else
params[:iteration_cadence_id] = iteration.iterations_cadence_id
end
end
def filter_iteration_cadence
return if params[:iteration_cadence_id].blank?
cadence = Iterations::CadencesFinder.new(current_user, group, { include_ancestor_groups: true, id: params[:iteration_cadence_id] }).execute.first
params.delete(:iteration_cadence_id) unless cadence
end
# todo: enforce iteration_cadence_id before we make multiple iteration cadences GA
# https://gitlab.com/gitlab-org/gitlab/-/issues/323653
def ensure_iteration_cadence
return if params[:iteration_cadence_id].present?
cadence = Iterations::CadencesFinder.new(current_user, group, { include_ancestor_groups: true }).execute.first
wildcard_iteration_title = ::Iteration::Predefined.by_id(params[:iteration_id].to_i)&.name&.upcase
raise ArgumentError, "No cadence could be found to scope board to #{wildcard_iteration_title} iteration." unless cadence
end
def wildcard_iteration_id?
return false if params[:iteration_id].blank?
::Iteration::Predefined::ALL.map(&:id).include?(params[:iteration_id].to_i)
end
def group
case parent
when Group
parent
when Project
parent.group
end
end
def iterations_finder_params
{ parent: parent, include_ancestors: true, state: 'all' }
{ parent: parent, include_ancestors: true, state: 'all', id: params[:iteration_id] }
end
end
end
......
......@@ -10,7 +10,7 @@ module EE
filter_assignee
filter_labels
filter_milestone
filter_iteration
filter_iteration_and_iteration_cadence
super
end
......
......@@ -12,7 +12,7 @@ module EE
filter_assignee
filter_labels
filter_milestone
filter_iteration
filter_iteration_and_iteration_cadence
end
override :permitted_params
......@@ -20,7 +20,7 @@ module EE
permitted = super
if parent.feature_available?(:scoped_issue_board)
permitted += %i(milestone_id iteration_id assignee_id weight labels label_ids)
permitted += %i(milestone_id iteration_id iteration_cadence_id assignee_id weight labels label_ids)
end
permitted
......
......@@ -157,7 +157,7 @@ module Iterations
def can_create_iterations_in_cadence?
cadence && user && cadence.group.iteration_cadences_feature_flag_enabled? &&
(user.automation_bot? || user.can?(:create_iteration_cadence, cadence))
(user.automation_bot? || user.can?(:create_iteration, cadence))
end
end
end
......
......@@ -34,7 +34,11 @@ module Iterations
def destroy_and_remove_references
ApplicationRecord.transaction do
Board.in_iterations(iteration_cadence.iterations).update_all(iteration_id: nil) && iteration_cadence.destroy
Board.in_iteration_cadences(iteration_cadence).update_all(iteration_id: nil, iteration_cadence_id: nil)
# it may be that a board is scoped to a specific iteration but missing the cadence_id, so we cleanup that one as well
Board.in_iterations(iteration_cadence.iterations).update_all(iteration_id: nil)
iteration_cadence.destroy
end
end
end
......
# frozen_string_literal: true
module EE
module Gitlab
module BackgroundMigration
# class that will populate issue boards with iteration cadence id for boards scopped to current iteration
module BackfillIterationCadenceIdForBoards
BATCH_SIZE = 100
class MigrationBoard < ApplicationRecord
include EachBatch
self.table_name = 'boards'
end
class MigrationGroup < ActiveRecord::Base
self.inheritance_column = :_type_disabled
self.table_name = 'namespaces'
end
class MigrationProject < ActiveRecord::Base
self.table_name = 'projects'
end
class MigrationCadence < ApplicationRecord
self.table_name = 'iterations_cadences'
end
def perform(board_type, method, start_id, end_id)
if method == "up"
back_fill_group_boards(start_id, end_id) if board_type == 'group'
back_fill_project_boards(start_id, end_id) if board_type == 'project'
else
MigrationBoard.where.not(iteration_cadence_id: nil).where(id: start_id..end_id).each_batch(of: BATCH_SIZE) do |batch|
batch.update_all(iteration_cadence_id: nil)
end
end
end
private
def bulk_update(cadences_sql)
MigrationBoard.connection.exec_query(<<~SQL)
UPDATE boards SET
iteration_id = CASE
WHEN boards_cadences.first_cadence_id IS NULL THEN NULL
ELSE boards.iteration_id
END,
iteration_cadence_id = boards_cadences.first_cadence_id
FROM #{cadences_sql}
WHERE boards.id = boards_cadences.board_id
SQL
end
def back_fill_group_boards(start_id, end_id)
boards_relation(start_id, end_id).where.not(group_id: nil).each_batch(of: BATCH_SIZE) do |batch|
range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first
sql = <<~SQL
(
SELECT
boards.id AS board_id,
(SELECT id FROM iterations_cadences WHERE group_id = ANY(traversal_ids) ORDER BY iterations_cadences.id LIMIT 1) AS first_cadence_id
FROM boards
INNER JOIN namespaces ON boards.group_id = namespaces.id
WHERE boards.id BETWEEN #{range.first} AND #{range.last} AND boards.group_id IS NOT NULL AND iteration_id = -4
ORDER BY first_cadence_id NULLS FIRST
) AS boards_cadences
SQL
bulk_update(sql)
end
end
def back_fill_project_boards(start_id, end_id)
boards_relation(start_id, end_id).where.not(project_id: nil).each_batch(of: BATCH_SIZE) do |batch|
range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first
sql = <<~SQL
(
SELECT
boards.id AS board_id,
(SELECT id FROM iterations_cadences WHERE group_id = ANY(traversal_ids) ORDER BY iterations_cadences.id LIMIT 1) AS first_cadence_id
FROM boards
INNER JOIN projects ON boards.project_id = projects.id
INNER JOIN namespaces ON projects.namespace_id = namespaces.id
WHERE boards.id BETWEEN #{range.first} AND #{range.last} AND boards.project_id IS NOT NULL AND iteration_id = -4
ORDER BY first_cadence_id NULLS FIRST
) AS boards_cadences
SQL
bulk_update(sql)
end
end
def build_board_cadence_data(group_board_pairs)
board_cadence_data = []
group_board_pairs.each do |pair|
cadence = MigrationCadence.where(group_id: MigrationGroup.where(id: pair.last).select('unnest(namespaces.traversal_ids) AS ids')).first
board_cadence_data << if cadence.present?
[pair.first, cadence.id, -4]
else
[pair.first, Arel::Nodes::SqlLiteral.new("NULL"), Arel::Nodes::SqlLiteral.new("NULL")]
end
end
board_cadence_data
end
def boards_relation(start_id, end_id)
MigrationBoard.where(iteration_id: -4).where(iteration_cadence_id: nil).where(id: start_id..end_id)
end
end
end
end
end
......@@ -275,25 +275,48 @@ RSpec.describe 'Scoped issue boards', :js do
end
context 'iteration' do
context 'board not scoped to iteration' do
it 'sets board to current iteration' do
expect(page).to have_selector('.board-card', count: 3)
context 'group with iterations' do
let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:iteration) { create(:iteration, group: group, iterations_cadence: cadence) }
update_board_scope('current_iteration', true)
context 'board not scoped to iteration' do
it 'sets board to current iteration' do
expect(page).to have_selector('.board-card', count: 3)
expect(page).to have_selector('.board-card', count: 0)
update_board_scope('current_iteration', true)
expect(page).to have_selector('.board-card', count: 0)
expect(page).not_to have_selector('.gl-alert-body')
end
end
end
context 'board scoped to current iteration' do
it 'removes current iteration from board' do
create_board_scope('current_iteration', true)
context 'board scoped to current iteration' do
it 'removes current iteration from board' do
create_board_scope('current_iteration', true)
expect(page).to have_selector('.board-card', count: 0)
expect(page).to have_selector('.board-card', count: 0)
update_board_scope('current_iteration', false)
update_board_scope('current_iteration', false)
expect(page).to have_selector('.board-card', count: 3)
expect(page).not_to have_selector('.gl-alert-body')
end
end
end
context 'group without iterations' do
it 'sets board to current iteration' do
expect(page).to have_selector('.board-card', count: 3)
edit_board.click
click_value('current_iteration', true)
click_on_board_modal
click_button 'Save changes'
expect(page).to have_selector('.gl-alert-body')
end
end
end
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Board'] do
it 'includes the ee specific fields' do
expect(described_class).to have_graphql_fields(
:assignee, :epics, :hide_backlog_list, :hide_closed_list, :labels, :milestone, :iteration, :weight
:assignee, :epics, :hide_backlog_list, :hide_closed_list, :labels, :milestone, :iteration, :iteration_cadence, :weight
).at_least
end
end
# frozen_string_literal: true
require 'spec_helper'
# rubocop:disable RSpec/FactoriesInMigrationSpecs
RSpec.describe Gitlab::BackgroundMigration::BackfillIterationCadenceIdForBoards do
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
let!(:project_board1) { create(:board, name: 'Project Dev1', project: project) }
let!(:project_board2) { create(:board, name: 'Project Dev2', project: project, iteration_id: -4) }
let!(:project_board3) { create(:board, name: 'Project Dev3', project: project, iteration_id: -4) }
let!(:project_board4) { create(:board, name: 'Project Dev4', project: project, iteration_id: -4) }
let!(:group_board1) { create(:board, name: 'Group Dev1', group: group) }
let!(:group_board2) { create(:board, name: 'Group Dev2', group: group, iteration_id: -4) }
let!(:group_board3) { create(:board, name: 'Group Dev3', group: group, iteration_id: -4) }
let!(:group_board4) { create(:board, name: 'Group Dev4', group: group, iteration_id: -4) }
let(:migration) { described_class.new }
subject { migration.perform(board_type, direction, start_id, end_id) }
context 'up' do
let(:direction) { 'up' }
shared_examples 'resets iteration_id to nil' do
it 'resets iteration_id to nil' do
subject
expect(boards.map(&:iteration_cadence)).to eq([nil, nil, nil])
expect(boards.map(&:iteration)).to eq([nil, nil, nil])
end
end
context 'when group does not have cadences' do
context 'back-fill project boards' do
let(:board_type) { 'project' }
let(:start_id) { project_board2.id }
let(:end_id) { project_board4.id }
let(:boards) { [project_board2.reload, project_board3.reload, project_board4.reload] }
it_behaves_like 'resets iteration_id to nil'
context 'with pagination' do
before do
stub_const('::EE::Gitlab::BackgroundMigration::BackfillIterationCadenceIdForBoards::BATCH_SIZE', 2)
end
it 'expect batched updates' do
expect(migration).to receive(:bulk_update).twice.and_call_original
subject
end
it_behaves_like 'resets iteration_id to nil'
end
end
context 'back-fill group boards' do
let(:board_type) { 'group' }
let(:start_id) { group_board2.id }
let(:end_id) { group_board4.id }
let(:boards) { [group_board2.reload, group_board3.reload, group_board4.reload] }
it_behaves_like 'resets iteration_id to nil'
context 'with pagination' do
before do
stub_const('::EE::Gitlab::BackgroundMigration::BackfillIterationCadenceIdForBoards::BATCH_SIZE', 2)
end
it 'expect batched updates' do
expect(migration).to receive(:bulk_update).twice.and_call_original
subject
end
it_behaves_like 'resets iteration_id to nil'
end
end
end
context 'when group has cadences' do
let!(:cadence) { create(:iterations_cadence, group: group) }
shared_examples 'sets the correct cadence id' do
it 'sets correct cadence id' do
subject
expect(boards.map(&:iteration_cadence_id)).to eq([cadence.id, cadence.id, cadence.id])
expect(boards.map(&:iteration_id)).to eq([-4, -4, -4])
end
end
context 'when group does not have cadences' do
context 'back-fill project boards' do
let(:board_type) { 'project' }
let(:start_id) { project_board2.id }
let(:end_id) { project_board4.id }
let(:boards) { [project_board2.reload, project_board3.reload, project_board4.reload] }
it_behaves_like 'sets the correct cadence id'
context 'with pagination' do
before do
stub_const('::EE::Gitlab::BackgroundMigration::BackfillIterationCadenceIdForBoards::BATCH_SIZE', 2)
end
it 'expect batched updates' do
expect(migration).to receive(:bulk_update).twice.and_call_original
subject
end
it_behaves_like 'sets the correct cadence id'
end
end
context 'back-fill group boards' do
let(:board_type) { 'group' }
let(:start_id) { group_board2.id }
let(:end_id) { group_board4.id }
let(:boards) { [group_board2.reload, group_board3.reload, group_board4.reload] }
it_behaves_like 'sets the correct cadence id'
context 'with pagination' do
before do
stub_const('::EE::Gitlab::BackgroundMigration::BackfillIterationCadenceIdForBoards::BATCH_SIZE', 2)
end
it 'expect batched updates' do
expect(migration).to receive(:bulk_update).twice.and_call_original
subject
end
it_behaves_like 'sets the correct cadence id'
end
end
end
end
end
context 'down' do
let!(:cadence) { create(:iterations_cadence, group: group) }
let!(:project_board1) { create(:board, name: 'Project Dev1', project: project) }
let!(:project_board2) { create(:board, name: 'Project Dev2', project: project, iteration_cadence: cadence) }
let!(:project_board3) { create(:board, name: 'Project Dev3', project: project, iteration_id: -4, iteration_cadence: cadence) }
let!(:project_board4) { create(:board, name: 'Project Dev4', project: project, iteration_id: -4, iteration_cadence: cadence) }
let!(:group_board1) { create(:board, name: 'Group Dev1', group: group) }
let!(:group_board2) { create(:board, name: 'Group Dev2', group: group, iteration_cadence: cadence) }
let!(:group_board3) { create(:board, name: 'Group Dev3', group: group, iteration_id: -4, iteration_cadence: cadence) }
let!(:group_board4) { create(:board, name: 'Group Dev4', group: group, iteration_id: -4, iteration_cadence: cadence) }
let(:direction) { 'down' }
let(:board_type) { 'none' }
let(:start_id) { project_board2.id }
let(:end_id) { group_board4.id }
let(:boards) { [project_board2.reload, project_board3.reload, project_board4.reload, group_board2.reload, group_board4.reload, group_board4.reload] }
it 'resets cadence id to nil' do
subject
expect(boards.map(&:iteration_cadence_id)).to eq([nil, nil, nil, nil, nil, nil])
expect(boards.map(&:iteration_id)).to eq([nil, -4, -4, nil, -4, -4])
end
context 'batched' do
before do
stub_const('::EE::Gitlab::BackgroundMigration::BackfillIterationCadenceIdForBoards::BATCH_SIZE', 2)
end
it 'resets cadence id to nil' do
subject
expect(boards.map(&:iteration_cadence_id)).to eq([nil, nil, nil, nil, nil, nil])
expect(boards.map(&:iteration_id)).to eq([nil, -4, -4, nil, -4, -4])
end
end
end
end
# rubocop:enable RSpec/FactoriesInMigrationSpecs
......@@ -35,22 +35,20 @@ RSpec.describe Boards::CreateService, services: true do
end
context 'when setting a timebox' do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
parent.add_reporter(user)
end
it_behaves_like 'setting a milestone scope' do
before do
parent.add_reporter(user)
end
subject { described_class.new(parent, user, args).execute.payload }
subject { described_class.new(parent, user, milestone_id: milestone.id).execute.payload }
it_behaves_like 'setting a milestone scope' do
let(:args) { { milestone_id: milestone.id } }
end
it_behaves_like 'setting an iteration scope' do
subject { described_class.new(parent, user, iteration_id: iteration.id).execute.payload }
let(:args) { { iteration_id: iteration.id } }
end
end
end
......
......@@ -51,26 +51,23 @@ RSpec.describe Boards::UpdateService, services: true do
end
context 'when setting a timebox' do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
parent.add_reporter(user)
end
it_behaves_like 'setting a milestone scope' do
subject { board.reload }
subject do
described_class.new(parent, user, args).execute(board)
board.reload
end
before do
described_class.new(parent, user, milestone_id: milestone.id).execute(board)
end
it_behaves_like 'setting a milestone scope' do
let(:args) { { milestone_id: milestone.id } }
end
it_behaves_like 'setting an iteration scope' do
subject { board.reload }
before do
described_class.new(parent, user, iteration_id: iteration.id).execute(board)
end
let(:args) { { iteration_id: iteration.id } }
end
end
end
......
......@@ -12,7 +12,8 @@ RSpec.describe Iterations::Cadences::DestroyService do
let_it_be(:iteration) { create(:current_iteration, group: group, iterations_cadence: iteration_cadence, start_date: 2.days.ago, due_date: 5.days.from_now) }
let_it_be(:iteration_list, refind: true) { create(:iteration_list, iteration: iteration) }
let_it_be(:iteration_event, refind: true) { create(:resource_iteration_event, iteration: iteration) }
let_it_be(:board) { create(:board, iteration: iteration, group: group) }
let_it_be(:board, refind: true) { create(:board, iteration: iteration, iteration_cadence: iteration_cadence, group: group) }
let_it_be(:board2, refind: true) { create(:board, iteration: iteration, group: group) }
let_it_be(:issue) { create(:issue, namespace: group, iteration: iteration) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, iteration: iteration) }
......@@ -41,6 +42,7 @@ RSpec.describe Iterations::Cadences::DestroyService do
expect do
results
board.reload
board2.reload
issue.reload
merge_request.reload
end.to change(Iterations::Cadence, :count).by(-1).and(
......@@ -51,6 +53,10 @@ RSpec.describe Iterations::Cadences::DestroyService do
change(Iteration, :count).by(-1)
).and(
change(board, :iteration_id).from(iteration.id).to(nil)
).and(
change(board2, :iteration_id).from(iteration.id).to(nil)
).and(
change(board, :iteration_cadence_id).from(iteration_cadence.id).to(nil)
).and(
change(issue, :iteration).from(iteration).to(nil)
).and(
......
......@@ -27,11 +27,12 @@ RSpec.shared_examples 'setting a timebox scope' do |timebox_type|
end
end
let(:ancestor_group) { create(:group) }
let(:group) { create(:group, parent: ancestor_group) }
let_it_be(:ancestor_group) { create(:group) }
let_it_be(:group) { create(:group, parent: ancestor_group) }
let_it_be(:project) { create(:project, :private, group: group) }
context 'for a group board' do
let(:parent) { group }
let(:parent) { group.reload }
it_behaves_like "an invalid #{timebox_type}"
it_behaves_like "a predefined #{timebox_type}"
......@@ -39,8 +40,7 @@ RSpec.shared_examples 'setting a timebox scope' do |timebox_type|
end
context 'for a project board' do
let(:project) { create(:project, :private, group: group) }
let(:parent) { project }
let(:parent) { project.reload }
it_behaves_like "an invalid #{timebox_type}"
it_behaves_like "a predefined #{timebox_type}"
......@@ -88,22 +88,49 @@ end
RSpec.shared_examples 'setting an iteration scope' do
shared_examples 'a predefined iteration' do
context 'None' do
let(:iteration) { ::Iteration::Predefined::None }
context 'without iteration cadence' do
let(:args) { { iteration_id: iteration.id }}
it { expect(subject.iteration).to eq(iteration) }
end
context 'None' do
let(:iteration) { ::Iteration::Predefined::None }
context 'Any' do
let(:iteration) { ::Iteration::Predefined::Any }
it { expect { subject }.to raise_error ArgumentError, "No cadence could be found to scope board to NONE iteration." }
end
context 'Any' do
let(:iteration) { ::Iteration::Predefined::Any }
it { expect(subject.iteration).to eq(iteration) }
it { expect { subject }.to raise_error ArgumentError, "No cadence could be found to scope board to ANY iteration." }
end
context 'Current' do
let(:iteration) { ::Iteration::Predefined::Current }
it { expect { subject }.to raise_error ArgumentError, "No cadence could be found to scope board to CURRENT iteration." }
end
end
context 'Current' do
let(:iteration) { ::Iteration::Predefined::Current }
context 'with iteration cadence' do
let(:iteration_cadence) { create(:iterations_cadence, group: group) }
let(:args) { { iteration_id: iteration.id, iteration_cadence_id: iteration_cadence.id } }
it { expect(subject.iteration).to eq(iteration) }
context 'None' do
let(:iteration) { ::Iteration::Predefined::None }
it { expect(subject.iteration).to eq(iteration) }
end
context 'Any' do
let(:iteration) { ::Iteration::Predefined::Any }
it { expect(subject.iteration).to eq(iteration) }
end
context 'Current' do
let(:iteration) { ::Iteration::Predefined::Current }
it { expect(subject.iteration).to eq(iteration) }
end
end
end
......
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop: disable Style/Documentation
class BackfillIterationCadenceIdForBoards
def perform(*args)
end
end
end
end
Gitlab::BackgroundMigration::BackfillIterationCadenceIdForBoards.prepend_mod_with('Gitlab::BackgroundMigration::BackfillIterationCadenceIdForBoards')
......@@ -400,6 +400,7 @@ excluded_attributes:
boards:
- :milestone_id
- :iteration_id
- :iteration_cadence_id
lists:
- :board_id
- :label_id
......
......@@ -683,6 +683,7 @@ boards:
- destroyable_lists
- milestone
- iteration
- iteration_cadence
- board_labels
- board_assignee
- assignee
......
......@@ -762,6 +762,7 @@ Board:
- group_id
- milestone_id
- iteration_id
- iteration_cadence_id
- weight
- name
- hide_backlog_list
......
# frozen_string_literal: true
require 'spec_helper'
require_migration!
# require Rails.root.join('db', 'post_migrate', '20210825193652_backfill_candence_id_for_boards_scoped_to_iteration.rb')
RSpec.describe BackfillCadenceIdForBoardsScopedToIteration, :migration do
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:iterations_cadences) { table(:iterations_cadences) }
let(:boards) { table(:boards) }
let!(:group) { namespaces.create!(name: 'group1', path: 'group1', type: 'Group') }
let!(:cadence) { iterations_cadences.create!(title: 'group cadence', group_id: group.id, start_date: Time.current) }
let!(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) }
let!(:project_board1) { boards.create!(name: 'Project Dev1', project_id: project.id) }
let!(:project_board2) { boards.create!(name: 'Project Dev2', project_id: project.id, iteration_id: -4) }
let!(:project_board3) { boards.create!(name: 'Project Dev3', project_id: project.id, iteration_id: -4) }
let!(:project_board4) { boards.create!(name: 'Project Dev4', project_id: project.id, iteration_id: -4) }
let!(:group_board1) { boards.create!(name: 'Group Dev1', group_id: group.id) }
let!(:group_board2) { boards.create!(name: 'Group Dev2', group_id: group.id, iteration_id: -4) }
let!(:group_board3) { boards.create!(name: 'Group Dev3', group_id: group.id, iteration_id: -4) }
let!(:group_board4) { boards.create!(name: 'Group Dev4', group_id: group.id, iteration_id: -4) }
describe '#up' do
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
freeze_time do
described_class.new.up
migration = described_class::MIGRATION
expect(migration).to be_scheduled_delayed_migration(2.minutes, 'group', 'up', group_board2.id, group_board4.id)
expect(migration).to be_scheduled_delayed_migration(2.minutes, 'project', 'up', project_board2.id, project_board4.id)
expect(BackgroundMigrationWorker.jobs.size).to eq 2
end
end
end
context 'in batches' do
before do
stub_const('BackfillCadenceIdForBoardsScopedToIteration::BATCH_SIZE', 2)
end
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
freeze_time do
described_class.new.up
migration = described_class::MIGRATION
expect(migration).to be_scheduled_delayed_migration(2.minutes, 'group', 'up', group_board2.id, group_board3.id)
expect(migration).to be_scheduled_delayed_migration(4.minutes, 'group', 'up', group_board4.id, group_board4.id)
expect(migration).to be_scheduled_delayed_migration(2.minutes, 'project', 'up', project_board2.id, project_board3.id)
expect(migration).to be_scheduled_delayed_migration(4.minutes, 'project', 'up', project_board4.id, project_board4.id)
expect(BackgroundMigrationWorker.jobs.size).to eq 4
end
end
end
end
end
describe '#down' do
let!(:project_board1) { boards.create!(name: 'Project Dev1', project_id: project.id) }
let!(:project_board2) { boards.create!(name: 'Project Dev2', project_id: project.id, iteration_cadence_id: cadence.id) }
let!(:project_board3) { boards.create!(name: 'Project Dev3', project_id: project.id, iteration_id: -4, iteration_cadence_id: cadence.id) }
let!(:project_board4) { boards.create!(name: 'Project Dev4', project_id: project.id, iteration_id: -4, iteration_cadence_id: cadence.id) }
let!(:group_board1) { boards.create!(name: 'Group Dev1', group_id: group.id) }
let!(:group_board2) { boards.create!(name: 'Group Dev2', group_id: group.id, iteration_cadence_id: cadence.id) }
let!(:group_board3) { boards.create!(name: 'Group Dev3', group_id: group.id, iteration_id: -4, iteration_cadence_id: cadence.id) }
let!(:group_board4) { boards.create!(name: 'Group Dev4', group_id: group.id, iteration_id: -4, iteration_cadence_id: cadence.id) }
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
freeze_time do
described_class.new.down
migration = described_class::MIGRATION
expect(migration).to be_scheduled_delayed_migration(2.minutes, 'none', 'down', project_board2.id, group_board4.id)
expect(BackgroundMigrationWorker.jobs.size).to eq 1
end
end
end
context 'in batches' do
before do
stub_const('BackfillCadenceIdForBoardsScopedToIteration::BATCH_SIZE', 2)
end
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
freeze_time do
described_class.new.down
migration = described_class::MIGRATION
expect(migration).to be_scheduled_delayed_migration(2.minutes, 'none', 'down', project_board2.id, project_board3.id)
expect(migration).to be_scheduled_delayed_migration(4.minutes, 'none', 'down', project_board4.id, group_board2.id)
expect(migration).to be_scheduled_delayed_migration(6.minutes, 'none', 'down', group_board3.id, group_board4.id)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
end
end
end
end
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