Skip to content

Commit d61ccdc

Browse files
committed
simplify assignability check in contract
By extracting the assignable scope from the contract, the setup becomes simpler. This then lead to merging two validations into one. It also contains a drive by fix in the sprints api where only non completed sprints would be visible.
1 parent 2a902ae commit d61ccdc

12 files changed

Lines changed: 157 additions & 61 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ class Sprint < ApplicationRecord
4545
inverse_of: :linked,
4646
dependent: :nullify
4747

48-
scopes :for_project,
48+
scopes :assignable,
49+
:for_project,
4950
:not_completed,
5051
:order_by_date,
5152
:receiving_projects,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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::Assignable
32+
extend ActiveSupport::Concern
33+
34+
class_methods do
35+
def assignable(project:, user: User.current)
36+
for_project(project)
37+
.visible(user)
38+
.not_completed
39+
end
40+
end
41+
end

modules/backlogs/config/locales/en.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ en:
8080
version_id:
8181
task_version_must_be_the_same_as_story_version: "must be the same as the parent story's version."
8282
sprint:
83-
not_shared_with_project: "is not shared with the project the work package is in."
83+
not_assignable: "is not assignable since it is either not shared with the project or already finished."
8484
not_eligible_for_moving: "is not an active sprint in the project which holds the sprint the work package is moved out of."
8585
agile/sprint:
8686
attributes:

modules/backlogs/lib/api/v3/sprints/sprints_by_project_api.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class SprintsByProjectAPI < ::API::OpenProjectAPI
4040
get &::API::V3::Utilities::Endpoints::Index
4141
.new(
4242
model: Agile::Sprint,
43-
scope: -> { @project.assignable_sprints }
43+
scope: -> { Agile::Sprint.for_project(@project).visible }
4444
)
4545
.mount
4646
end

modules/backlogs/lib/open_project/backlogs/patches/base_contract_patch.rb

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,14 @@ module OpenProject::Backlogs::Patches::BaseContractPatch
4242

4343
validate :backlog_bucket_xor_sprint
4444
validate :backlog_bucket_belongs_to_project
45-
validate :sprint_shared_with_project
4645
validate :validate_sprint_is_assignable
4746

4847
def assignable_sprints
49-
model.try(:assignable_sprints)
48+
if model.project
49+
Agile::Sprint.assignable(project: model.project, user:)
50+
else
51+
Agile::Sprint.none
52+
end
5053
end
5154

5255
private
@@ -65,16 +68,11 @@ def backlog_bucket_belongs_to_project
6568
end
6669

6770
def validate_sprint_is_assignable
68-
if model.sprint_id && model.sprint_id_changed? && model.assignable_sprints.map(&:id).exclude?(model.sprint_id)
69-
errors.add :sprint_id, :inclusion
71+
if model.sprint_id &&
72+
model.sprint_id_changed? &&
73+
!assignable_sprints.exists?(id: model.sprint_id)
74+
errors.add :sprint, :not_assignable
7075
end
7176
end
72-
73-
def sprint_shared_with_project
74-
return if model.sprint.nil? ||
75-
Agile::Sprint.for_project(model.project).exists?(id: model.sprint_id)
76-
77-
errors.add :sprint, :not_shared_with_project
78-
end
7977
end
8078
end

modules/backlogs/lib/open_project/backlogs/patches/project_patch.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ module OpenProject::Backlogs::Patches::ProjectPatch
4141
def backlogs_enabled?
4242
module_enabled? "backlogs"
4343
end
44-
45-
def assignable_sprints
46-
@assignable_sprints ||= Agile::Sprint.for_project(self).visible.not_completed
47-
end
4844
end
4945

5046
Project.include OpenProject::Backlogs::Patches::ProjectPatch

modules/backlogs/lib/open_project/backlogs/patches/work_package_patch.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,6 @@ def done?
6363
def backlogs_enabled?
6464
project&.backlogs_enabled?
6565
end
66-
67-
def assignable_sprints
68-
project.try(:assignable_sprints)
69-
end
7066
end
7167
end
7268

modules/backlogs/spec/contracts/work_packages/create_contract_spec.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,5 @@
5555
]
5656
end
5757

58-
before do
59-
allow(work_package_project).to receive(:assignable_sprints)
60-
.and_return(shared_sprints)
61-
end
62-
6358
it_behaves_like "work package contract with backlogs extensions"
6459
end

modules/backlogs/spec/contracts/work_packages/shared_contract_examples.rb

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
let(:work_package_story_points) { 5 }
4040
let(:work_package_sprint) { build_stubbed(:agile_sprint) }
4141
let(:work_package_position) { 5 }
42-
let(:shared_sprints) { [work_package_sprint] }
42+
let(:assignable_sprints) { [work_package_sprint] }
4343
let(:backlogs_enabled) { true }
4444
let(:work_package_project) do
4545
build_stubbed(:project, types: [work_package_type]) do |project|
@@ -59,16 +59,16 @@
5959
let(:effective_permissions) { permissions }
6060

6161
before do
62-
shared_sprints_scope = instance_double(ActiveRecord::Relation)
62+
assignable_sprints_scope = instance_double(ActiveRecord::Relation)
6363

6464
allow(Agile::Sprint)
65-
.to receive(:for_project)
66-
.with(work_package.project)
67-
.and_return(shared_sprints_scope)
65+
.to receive(:assignable)
66+
.with(project: work_package.project, user:)
67+
.and_return(assignable_sprints_scope)
6868

69-
allow(shared_sprints_scope)
69+
allow(assignable_sprints_scope)
7070
.to receive(:exists?) do |id:|
71-
shared_sprints.map(&:id).include?(id.to_i)
71+
assignable_sprints.map(&:id).include?(id.to_i)
7272
end
7373
end
7474

@@ -119,25 +119,10 @@
119119
it_behaves_like "contract is valid"
120120
end
121121

122-
context "when sprint is set to a sprint not shared with the wp's project" do
123-
let(:shared_sprints) { [] }
122+
context "when sprint is set to a sprint not assignable" do
123+
let(:assignable_sprints) { [] }
124124

125-
it_behaves_like "contract is invalid", sprint: :not_shared_with_project
126-
end
127-
128-
context "when sprint is completed (shared with project but not assignable)" do
129-
let(:completed_sprint) { build_stubbed(:agile_sprint, status: :completed) }
130-
let(:work_package_sprint) { completed_sprint }
131-
132-
before do
133-
# The sprint is still shared with the project (the outer before mock makes
134-
# `Agile::Sprint.for_project.exists?` return true), so `sprint_shared_with_project`
135-
# passes. We stub assignable_sprints to exclude it, simulating the `.not_completed`
136-
# scope, so only `validate_sprint_is_assignable` fires.
137-
allow(work_package_project).to receive(:assignable_sprints).and_return([])
138-
end
139-
140-
it_behaves_like "contract is invalid", sprint_id: :inclusion
125+
it_behaves_like "contract is invalid", sprint: :not_assignable
141126
end
142127

143128
context "when sprint is set while the user lacks the :manage_sprint_items permission" do

modules/backlogs/spec/contracts/work_packages/update_contract_spec.rb

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@
5353
end
5454

5555
before do
56-
allow(work_package_project).to receive(:assignable_sprints)
57-
.and_return(shared_sprints)
58-
5956
visible_scope = instance_double(ActiveRecord::Relation)
6057

6158
allow(WorkPackage)
@@ -95,16 +92,14 @@
9592
story_points: :error_readonly
9693
end
9794

98-
context "when sprint is completed (shared with project but not assignable) but the assignment did not change" do
95+
context "when sprint is not assignable but the assignment did not change" do
9996
let(:completed_sprint) { build_stubbed(:agile_sprint, status: :completed) }
10097
let(:work_package_sprint) { completed_sprint }
98+
let(:assignable_sprints) { [] }
10199

102100
before do
103101
# So that the changes look like they came out of the database
104102
work_package.clear_changes_information
105-
106-
# The mocks in the shared context need to be taken into account.
107-
allow(work_package_project).to receive(:assignable_sprints).and_return([])
108103
end
109104

110105
it_behaves_like "contract is valid"

0 commit comments

Comments
 (0)