Skip to content

Commit fd1c67c

Browse files
committed
Add receiving_projects scope and multi-project boards
Introduces `Agile::Sprint.receiving_projects(sprint)` scope returning all projects with work packages on the sprint. Updates `ensure_task_boards` to create boards for all receiving projects. Moves `board_name` from model to service.
1 parent 154f917 commit fd1c67c

File tree

6 files changed

+171
-18
lines changed

6 files changed

+171
-18
lines changed

modules/backlogs/app/models/agile/sprint.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Sprint < ApplicationRecord
4848
scopes :for_project,
4949
:not_completed,
5050
:order_by_date,
51+
:receiving_projects,
5152
:visible
5253

5354
enum :status,
@@ -84,10 +85,6 @@ def duration
8485
Day.working.from_range(from: start_date, to: finish_date).count
8586
end
8687

87-
def board_name
88-
"#{project.name}: #{name}"
89-
end
90-
9188
def task_board_for(project)
9289
task_boards.find_by(project:)
9390
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
module Agile::Sprints::Scopes::ReceivingProjects
32+
extend ActiveSupport::Concern
33+
34+
class_methods do
35+
def receiving_projects(sprint)
36+
Project.where(id: sprint.project_id)
37+
.or(Project.where(id: sprint.work_packages.select(:project_id)))
38+
end
39+
end
40+
end

modules/backlogs/app/services/sprints/start_service.rb

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def initialize(user:, model:, contract_class: Sprints::StartContract)
3737
private
3838

3939
def persist(service_call)
40-
result = ensure_task_board
40+
result = ensure_task_boards
4141
return result if result.failure?
4242

4343
model.active!
@@ -48,13 +48,34 @@ def persist(service_call)
4848
ServiceResult.failure(result: model, errors: model.errors)
4949
end
5050

51-
def ensure_task_board
52-
existing_board = model.task_board_for(model.project)
53-
return ServiceResult.success(result: existing_board) if existing_board.present?
51+
def ensure_task_boards
52+
projects = Agile::Sprint.receiving_projects(model)
5453

55-
Boards::SprintTaskBoardCreateService
56-
.new(user:)
57-
.call(project: model.project, sprint: model, name: model.board_name)
54+
results = projects.map do |project|
55+
next ServiceResult.success if model.task_board_for(project).present?
56+
57+
# TODO: Consider using User.system for board creation so that boards
58+
# are created even when the starting user lacks manage_board_views
59+
# permission in receiving projects.
60+
Boards::SprintTaskBoardCreateService
61+
.new(user:)
62+
.call(project:, sprint: model, name: board_name)
63+
end
64+
65+
aggregate_failures(results)
66+
end
67+
68+
def aggregate_failures(results)
69+
failed = results.select(&:failure?)
70+
return ServiceResult.success if failed.empty?
71+
72+
failed.each_with_object(ServiceResult.failure) do |result, combined|
73+
combined.add_dependent!(result)
74+
end
75+
end
76+
77+
def board_name
78+
"#{model.project.name}: #{model.name}"
5879
end
5980

6081
def add_only_one_active_sprint_error

modules/backlogs/spec/models/agile/sprint_spec.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,6 @@
120120
it { is_expected.to belong_to(:project) }
121121
end
122122

123-
describe "#board_name" do
124-
it "returns the project and sprint name" do
125-
expect(sprint.board_name).to eq("#{project.name}: Sprint 1")
126-
end
127-
end
128-
129123
describe "#task_board_for" do
130124
let(:sprint) { create(:agile_sprint, project:) }
131125
let(:other_project) { create(:project) }
@@ -151,7 +145,7 @@
151145
end
152146

153147
context "when only same-name or same-filter boards exist" do
154-
let!(:same_name_board) { create(:board_grid_with_query, project:, name: sprint.board_name) }
148+
let!(:same_name_board) { create(:board_grid_with_query, project:, name: "#{project.name}: #{sprint.name}") }
155149
let!(:matching_filters_board) do
156150
create(:board_grid_with_query,
157151
project:,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
require "spec_helper"
32+
33+
RSpec.describe Agile::Sprints::Scopes::ReceivingProjects do
34+
let(:project) { create(:project) }
35+
let(:other_project) { create(:project) }
36+
let(:sprint) { create(:agile_sprint, project:) }
37+
38+
describe ".receiving_projects" do
39+
subject { Agile::Sprint.receiving_projects(sprint) }
40+
41+
context "when sprint has no work packages in other projects" do
42+
it "returns only the owning project" do
43+
expect(subject).to contain_exactly(project)
44+
end
45+
end
46+
47+
context "when sprint has work packages in another project" do
48+
before do
49+
create(:work_package, project: other_project, sprint:)
50+
end
51+
52+
it "returns both the owning project and the receiving project" do
53+
expect(subject).to contain_exactly(project, other_project)
54+
end
55+
end
56+
57+
context "when sprint has work packages in multiple other projects" do
58+
let(:third_project) { create(:project) }
59+
60+
before do
61+
create(:work_package, project: other_project, sprint:)
62+
create(:work_package, project: third_project, sprint:)
63+
end
64+
65+
it "returns all projects" do
66+
expect(subject).to contain_exactly(project, other_project, third_project)
67+
end
68+
end
69+
70+
context "when sprint has work packages only in the owning project" do
71+
before do
72+
create(:work_package, project:, sprint:)
73+
end
74+
75+
it "returns only the owning project without duplicates" do
76+
expect(subject).to contain_exactly(project)
77+
end
78+
end
79+
80+
it "resolves in a single query" do
81+
sprint # ensure factory queries are done
82+
expect { Agile::Sprint.receiving_projects(sprint).load }.to have_a_query_limit(1)
83+
end
84+
end
85+
end

modules/backlogs/spec/services/sprints/start_service_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,20 @@
157157
expect(sprint.reload).to be_in_planning
158158
end
159159
end
160+
161+
context "when work packages exist in a receiving project" do
162+
let!(:receiving_project) { create(:project, types: [type_task]) }
163+
164+
before do
165+
create(:work_package, project: receiving_project, type: type_task, sprint:)
166+
end
167+
168+
it "creates boards for both owning and receiving projects", :aggregate_failures do
169+
expect { result }.to change(Boards::Grid, :count).by(2)
170+
expect(result).to be_success
171+
expect(sprint.reload).to be_active
172+
expect(sprint.task_board_for(project)).to be_present
173+
expect(sprint.task_board_for(receiving_project)).to be_present
174+
end
175+
end
160176
end

0 commit comments

Comments
 (0)