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 ( ...@@ -10894,7 +10894,8 @@ CREATE TABLE boards (
weight integer, weight integer,
hide_backlog_list boolean DEFAULT false NOT NULL, hide_backlog_list boolean DEFAULT false NOT NULL,
hide_closed_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 ( 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 ...@@ -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_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_iteration_id ON boards USING btree (iteration_id);
CREATE INDEX index_boards_on_milestone_id ON boards USING btree (milestone_id); CREATE INDEX index_boards_on_milestone_id ON boards USING btree (milestone_id);
...@@ -27911,6 +27914,9 @@ ALTER TABLE ONLY alert_management_alerts ...@@ -27911,6 +27914,9 @@ ALTER TABLE ONLY alert_management_alerts
ALTER TABLE ONLY identities ALTER TABLE ONLY identities
ADD CONSTRAINT fk_aade90f0fc FOREIGN KEY (saml_provider_id) REFERENCES saml_providers(id) ON DELETE CASCADE; 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 ALTER TABLE ONLY dep_ci_build_trace_sections
ADD CONSTRAINT fk_ab7c104e26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_ab7c104e26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
...@@ -1075,6 +1075,7 @@ Input type: `CreateBoardInput` ...@@ -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="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="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="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="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="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. | | <a id="mutationcreateboardlabels"></a>`labels` | [`[String!]`](#string) | Labels of the issue. |
...@@ -4122,6 +4123,7 @@ Input type: `UpdateBoardInput` ...@@ -4122,6 +4123,7 @@ Input type: `UpdateBoardInput`
| <a id="mutationupdateboardhidebackloglist"></a>`hideBacklogList` | [`Boolean`](#boolean) | Whether or not backlog list is hidden. | | <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="mutationupdateboardhideclosedlist"></a>`hideClosedList` | [`Boolean`](#boolean) | Whether or not closed list is hidden. |
| <a id="mutationupdateboardid"></a>`id` | [`BoardID!`](#boardid) | Board global ID. | | <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="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="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. | | <a id="mutationupdateboardlabels"></a>`labels` | [`[String!]`](#string) | Labels of the issue. |
...@@ -7897,6 +7899,7 @@ Represents a project or group issue board. ...@@ -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="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="boardid"></a>`id` | [`ID!`](#id) | ID (global ID) of the board. |
| <a id="boarditeration"></a>`iteration` | [`Iteration`](#iteration) | Board iteration. | | <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="boardlabels"></a>`labels` | [`LabelConnection`](#labelconnection) | Labels of the board. (see [Connections](#connections)) |
| <a id="boardmilestone"></a>`milestone` | [`Milestone`](#milestone) | Board milestone. | | <a id="boardmilestone"></a>`milestone` | [`Milestone`](#milestone) | Board milestone. |
| <a id="boardname"></a>`name` | [`String`](#string) | Name of the board. | | <a id="boardname"></a>`name` | [`String`](#string) | Name of the board. |
......
...@@ -13,6 +13,7 @@ module Iterations ...@@ -13,6 +13,7 @@ module Iterations
end end
def execute def execute
raise ArgumentError, 'group argument is missing' unless group.present?
return Iterations::Cadence.none unless group.iteration_cadences_feature_flag_enabled? return Iterations::Cadence.none unless group.iteration_cadences_feature_flag_enabled?
items = Iterations::Cadence.all items = Iterations::Cadence.all
......
...@@ -23,6 +23,9 @@ module EE ...@@ -23,6 +23,9 @@ module EE
field :iteration, type: ::Types::IterationType, null: true, field :iteration, type: ::Types::IterationType, null: true,
description: 'Board iteration.' description: 'Board iteration.'
field :iteration_cadence, type: ::Types::Iterations::CadenceType, null: true,
description: 'Board iteration cadence.'
field :weight, type: GraphQL::Types::Int, null: true, field :weight, type: GraphQL::Types::Int, null: true,
description: 'Weight of the board.' description: 'Weight of the board.'
end end
......
...@@ -25,6 +25,11 @@ module Mutations ...@@ -25,6 +25,11 @@ module Mutations
required: false, required: false,
description: 'ID of iteration to be assigned to the board.' 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, argument :weight,
GraphQL::Types::Int, GraphQL::Types::Int,
required: false, required: false,
......
...@@ -55,7 +55,8 @@ module Resolvers ...@@ -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? 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 @parent.group
else raise "Unexpected parent type: #{@parent.class}" else
raise "Unexpected parent type: #{@parent.class}"
end end
end end
......
...@@ -11,6 +11,7 @@ module EE ...@@ -11,6 +11,7 @@ module EE
prepended do prepended do
belongs_to :milestone belongs_to :milestone
belongs_to :iteration belongs_to :iteration
belongs_to :iteration_cadence, class_name: 'Iterations::Cadence'
has_many :board_labels has_many :board_labels
has_many :user_preferences, class_name: 'BoardUserPreference', inverse_of: :board has_many :user_preferences, class_name: 'BoardUserPreference', inverse_of: :board
...@@ -26,6 +27,7 @@ module EE ...@@ -26,6 +27,7 @@ module EE
scope :with_associations, -> { preload(:destroyable_lists, :labels, :assignee) } scope :with_associations, -> { preload(:destroyable_lists, :labels, :assignee) }
scope :in_iterations, ->(iterations) { where(iteration: iterations) } scope :in_iterations, ->(iterations) { where(iteration: iterations) }
scope :in_iteration_cadences, ->(cadences) { where(iteration_cadence: cadences) }
end end
override :scoped? override :scoped?
......
...@@ -17,6 +17,10 @@ module EE ...@@ -17,6 +17,10 @@ module EE
Current = ::Timebox::TimeboxStruct.new('Current', 'current', -4).freeze Current = ::Timebox::TimeboxStruct.new('Current', 'current', -4).freeze
ALL = [None, Any, Current].freeze ALL = [None, Any, Current].freeze
def self.by_id(id)
::Iteration::Predefined::ALL.index_by(&:id)[id]
end
end end
prepended do prepended do
......
...@@ -17,13 +17,8 @@ module EE ...@@ -17,13 +17,8 @@ module EE
return if params[:milestone_id].blank? return if params[:milestone_id].blank?
return if ::Milestone::Predefined::ALL.map(&:id).include?(params[:milestone_id].to_i) return if ::Milestone::Predefined::ALL.map(&:id).include?(params[:milestone_id].to_i)
finder_params = finder_params = { group_ids: group&.self_and_ancestors }
case parent finder_params[:project_ids] = [parent.id] if parent.is_a?(Project)
when Group
{ group_ids: parent.self_and_ancestors }
when Project
{ project_ids: [parent.id], group_ids: parent.group&.self_and_ancestors }
end
milestone = ::MilestonesFinder.new(finder_params).find_by(id: params[:milestone_id]) milestone = ::MilestonesFinder.new(finder_params).find_by(id: params[:milestone_id])
...@@ -31,13 +26,16 @@ module EE ...@@ -31,13 +26,16 @@ module EE
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def filter_iteration def filter_iteration_and_iteration_cadence
return if params[:iteration_id].blank? return if params[:iteration_id].blank? && params[:iteration_cadence_id].blank?
return if ::Iteration::Predefined::ALL.map(&:id).include?(params[:iteration_id].to_i)
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 end
def filter_labels def filter_labels
...@@ -52,8 +50,55 @@ module EE ...@@ -52,8 +50,55 @@ module EE
@labels_service ||= ::Labels::AvailableLabelsService.new(current_user, parent, params) @labels_service ||= ::Labels::AvailableLabelsService.new(current_user, parent, params)
end 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 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 end
end end
......
...@@ -10,7 +10,7 @@ module EE ...@@ -10,7 +10,7 @@ module EE
filter_assignee filter_assignee
filter_labels filter_labels
filter_milestone filter_milestone
filter_iteration filter_iteration_and_iteration_cadence
super super
end end
......
...@@ -12,7 +12,7 @@ module EE ...@@ -12,7 +12,7 @@ module EE
filter_assignee filter_assignee
filter_labels filter_labels
filter_milestone filter_milestone
filter_iteration filter_iteration_and_iteration_cadence
end end
override :permitted_params override :permitted_params
...@@ -20,7 +20,7 @@ module EE ...@@ -20,7 +20,7 @@ module EE
permitted = super permitted = super
if parent.feature_available?(:scoped_issue_board) 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 end
permitted permitted
......
...@@ -157,7 +157,7 @@ module Iterations ...@@ -157,7 +157,7 @@ module Iterations
def can_create_iterations_in_cadence? def can_create_iterations_in_cadence?
cadence && user && cadence.group.iteration_cadences_feature_flag_enabled? && 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 end
end end
......
...@@ -34,7 +34,11 @@ module Iterations ...@@ -34,7 +34,11 @@ module Iterations
def destroy_and_remove_references def destroy_and_remove_references
ApplicationRecord.transaction do 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 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 ...@@ -275,25 +275,48 @@ RSpec.describe 'Scoped issue boards', :js do
end end
context 'iteration' do context 'iteration' do
context 'board not scoped to iteration' do context 'group with iterations' do
it 'sets board to current iteration' do let_it_be(:cadence) { create(:iterations_cadence, group: group) }
expect(page).to have_selector('.board-card', count: 3) 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
end
context 'board scoped to current iteration' do context 'board scoped to current iteration' do
it 'removes current iteration from board' do it 'removes current iteration from board' do
create_board_scope('current_iteration', true) 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) 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 end
end end
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Board'] do RSpec.describe GitlabSchema.types['Board'] do
it 'includes the ee specific fields' do it 'includes the ee specific fields' do
expect(described_class).to have_graphql_fields( 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 ).at_least
end end
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 ...@@ -35,22 +35,20 @@ RSpec.describe Boards::CreateService, services: true do
end end
context 'when setting a timebox' do context 'when setting a timebox' do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
parent.add_reporter(user) parent.add_reporter(user)
end end
it_behaves_like 'setting a milestone scope' do subject { described_class.new(parent, user, args).execute.payload }
before do
parent.add_reporter(user)
end
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 end
it_behaves_like 'setting an iteration scope' do 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 end
end end
......
...@@ -51,26 +51,23 @@ RSpec.describe Boards::UpdateService, services: true do ...@@ -51,26 +51,23 @@ RSpec.describe Boards::UpdateService, services: true do
end end
context 'when setting a timebox' do context 'when setting a timebox' do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
parent.add_reporter(user) parent.add_reporter(user)
end end
it_behaves_like 'setting a milestone scope' do subject do
subject { board.reload } described_class.new(parent, user, args).execute(board)
board.reload
end
before do it_behaves_like 'setting a milestone scope' do
described_class.new(parent, user, milestone_id: milestone.id).execute(board) let(:args) { { milestone_id: milestone.id } }
end
end end
it_behaves_like 'setting an iteration scope' do it_behaves_like 'setting an iteration scope' do
subject { board.reload } let(:args) { { iteration_id: iteration.id } }
before do
described_class.new(parent, user, iteration_id: iteration.id).execute(board)
end
end end
end end
end end
......
...@@ -12,7 +12,8 @@ RSpec.describe Iterations::Cadences::DestroyService do ...@@ -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) { 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_list, refind: true) { create(:iteration_list, iteration: iteration) }
let_it_be(:iteration_event, refind: true) { create(:resource_iteration_event, 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(:issue) { create(:issue, namespace: group, iteration: iteration) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, 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 ...@@ -41,6 +42,7 @@ RSpec.describe Iterations::Cadences::DestroyService do
expect do expect do
results results
board.reload board.reload
board2.reload
issue.reload issue.reload
merge_request.reload merge_request.reload
end.to change(Iterations::Cadence, :count).by(-1).and( end.to change(Iterations::Cadence, :count).by(-1).and(
...@@ -51,6 +53,10 @@ RSpec.describe Iterations::Cadences::DestroyService do ...@@ -51,6 +53,10 @@ RSpec.describe Iterations::Cadences::DestroyService do
change(Iteration, :count).by(-1) change(Iteration, :count).by(-1)
).and( ).and(
change(board, :iteration_id).from(iteration.id).to(nil) 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( ).and(
change(issue, :iteration).from(iteration).to(nil) change(issue, :iteration).from(iteration).to(nil)
).and( ).and(
......
...@@ -27,11 +27,12 @@ RSpec.shared_examples 'setting a timebox scope' do |timebox_type| ...@@ -27,11 +27,12 @@ RSpec.shared_examples 'setting a timebox scope' do |timebox_type|
end end
end end
let(:ancestor_group) { create(:group) } let_it_be(:ancestor_group) { create(:group) }
let(:group) { create(:group, parent: ancestor_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 context 'for a group board' do
let(:parent) { group } let(:parent) { group.reload }
it_behaves_like "an invalid #{timebox_type}" it_behaves_like "an invalid #{timebox_type}"
it_behaves_like "a predefined #{timebox_type}" it_behaves_like "a predefined #{timebox_type}"
...@@ -39,8 +40,7 @@ RSpec.shared_examples 'setting a timebox scope' do |timebox_type| ...@@ -39,8 +40,7 @@ RSpec.shared_examples 'setting a timebox scope' do |timebox_type|
end end
context 'for a project board' do context 'for a project board' do
let(:project) { create(:project, :private, group: group) } let(:parent) { project.reload }
let(:parent) { project }
it_behaves_like "an invalid #{timebox_type}" it_behaves_like "an invalid #{timebox_type}"
it_behaves_like "a predefined #{timebox_type}" it_behaves_like "a predefined #{timebox_type}"
...@@ -88,22 +88,49 @@ end ...@@ -88,22 +88,49 @@ end
RSpec.shared_examples 'setting an iteration scope' do RSpec.shared_examples 'setting an iteration scope' do
shared_examples 'a predefined iteration' do shared_examples 'a predefined iteration' do
context 'None' do context 'without iteration cadence' do
let(:iteration) { ::Iteration::Predefined::None } let(:args) { { iteration_id: iteration.id }}
it { expect(subject.iteration).to eq(iteration) } context 'None' do
end let(:iteration) { ::Iteration::Predefined::None }
context 'Any' do it { expect { subject }.to raise_error ArgumentError, "No cadence could be found to scope board to NONE iteration." }
let(:iteration) { ::Iteration::Predefined::Any } 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 end
context 'Current' do context 'with iteration cadence' do
let(:iteration) { ::Iteration::Predefined::Current } 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
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: ...@@ -400,6 +400,7 @@ excluded_attributes:
boards: boards:
- :milestone_id - :milestone_id
- :iteration_id - :iteration_id
- :iteration_cadence_id
lists: lists:
- :board_id - :board_id
- :label_id - :label_id
......
...@@ -683,6 +683,7 @@ boards: ...@@ -683,6 +683,7 @@ boards:
- destroyable_lists - destroyable_lists
- milestone - milestone
- iteration - iteration
- iteration_cadence
- board_labels - board_labels
- board_assignee - board_assignee
- assignee - assignee
......
...@@ -762,6 +762,7 @@ Board: ...@@ -762,6 +762,7 @@ Board:
- group_id - group_id
- milestone_id - milestone_id
- iteration_id - iteration_id
- iteration_cadence_id
- weight - weight
- name - name
- hide_backlog_list - 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