Ensure ResourceAllocation is scoped to a work package for now, allow further classes later

This commit is contained in:
Klaus Zanders
2026-06-02 11:22:27 +02:00
parent 63cf1dea7e
commit 45afae52d6
10 changed files with 47 additions and 24 deletions
@@ -29,6 +29,8 @@
#++
class ResourceAllocation < ApplicationRecord
ALLOWED_ENTITY_TYPES = %w[WorkPackage].freeze
belongs_to :entity, polymorphic: true, optional: false
belongs_to :principal, class_name: "User", optional: true, inverse_of: :resource_allocations
@@ -46,6 +48,10 @@ class ResourceAllocation < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :entity_type,
inclusion: { in: ALLOWED_ENTITY_TYPES },
allow_blank: true
validate :end_date_after_start_date
# Resource allocations are scoped to whatever project their (polymorphic)
@@ -43,8 +43,8 @@ RSpec.describe ResourceAllocations::CreateContract do
let(:current_user) do
create(:user, member_with_permissions: { project => %i[view_resource_planners allocate_user_resources] })
end
let(:planner) { create(:resource_planner, project:, principal: current_user) }
let(:resource_allocation) { build_stubbed(:resource_allocation, entity: planner, principal: current_user) }
let(:work_package) { create(:work_package, project:) }
let(:resource_allocation) { build_stubbed(:resource_allocation, entity: work_package, principal: current_user) }
let(:contract) { described_class.new(resource_allocation, current_user) }
it "allows entity to be set" do
@@ -36,9 +36,9 @@ RSpec.describe ResourceAllocations::DeleteContract do
shared_let(:project) { create(:project, enabled_module_names: %w[resource_management]) }
shared_let(:owner) { create(:user) }
shared_let(:planner) { create(:resource_planner, project:, principal: owner) }
shared_let(:work_package) { create(:work_package, project:) }
let(:resource_allocation) { build_stubbed(:resource_allocation, entity: planner, principal: owner) }
let(:resource_allocation) { build_stubbed(:resource_allocation, entity: work_package, principal: owner) }
let(:contract) { described_class.new(resource_allocation, current_user) }
context "when user has allocate_user_resources" do
@@ -34,10 +34,10 @@ require "contracts/shared/model_contract_shared_context"
RSpec.shared_examples_for "resource allocation contract" do
shared_let(:project) { create(:project, enabled_module_names: %w[resource_management]) }
shared_let(:owner) { create(:user) }
shared_let(:planner) { create(:resource_planner, project:, principal: owner) }
shared_let(:work_package) { create(:work_package, project:) }
let(:resource_allocation) do
build_stubbed(:resource_allocation, entity: planner, principal: owner)
build_stubbed(:resource_allocation, entity: work_package, principal: owner)
end
context "when user has the allocate_user_resources permission" do
@@ -43,8 +43,8 @@ RSpec.describe ResourceAllocations::UpdateContract do
let(:current_user) do
create(:user, member_with_permissions: { project => %i[view_resource_planners allocate_user_resources] })
end
let(:planner) { create(:resource_planner, project:, principal: current_user) }
let(:resource_allocation) { build_stubbed(:resource_allocation, entity: planner, principal: current_user) }
let(:work_package) { create(:work_package, project:) }
let(:resource_allocation) { build_stubbed(:resource_allocation, entity: work_package, principal: current_user) }
let(:contract) { described_class.new(resource_allocation, current_user) }
it "does not allow entity to be set" do
@@ -30,7 +30,7 @@
FactoryBot.define do
factory :resource_allocation, class: "ResourceAllocation" do
entity factory: :resource_planner
entity factory: :work_package
principal factory: :user
state { "requested" }
start_date { Date.new(2026, 1, 5) }
@@ -70,9 +70,9 @@ RSpec.describe ResourceAllocation do
describe "validations" do
shared_let(:project) { create(:project, enabled_module_names: %w[resource_management]) }
shared_let(:owner) { create(:user, member_with_permissions: { project => %i[view_resource_planners] }) }
shared_let(:planner) { create(:resource_planner, project:, principal: owner) }
shared_let(:work_package) { create(:work_package, project:) }
let(:allocation) { build(:resource_allocation, entity: planner, principal: owner) }
let(:allocation) { build(:resource_allocation, entity: work_package, principal: owner) }
it "is valid with the factory defaults" do
expect(allocation).to be_valid
@@ -115,6 +115,23 @@ RSpec.describe ResourceAllocation do
end
end
describe "entity type" do
it "lists the supported entity types" do
expect(described_class::ALLOWED_ENTITY_TYPES).to eq(%w[WorkPackage])
end
it "is valid when the entity type is in the allowed list" do
allocation.entity = work_package
expect(allocation).to be_valid
end
it "is invalid when the entity type is outside the allowed list" do
allocation.entity = create(:resource_planner, project:, principal: owner)
expect(allocation).not_to be_valid
expect(allocation.errors.symbols_for(:entity_type)).to include(:inclusion)
end
end
describe "allocated_time numericality" do
it "is invalid when zero" do
allocation.allocated_time = 0
@@ -175,7 +192,7 @@ RSpec.describe ResourceAllocation do
describe "user_filter serialization" do
shared_let(:project) { create(:project, enabled_module_names: %w[resource_management]) }
shared_let(:owner) { create(:user, member_with_permissions: { project => %i[view_resource_planners] }) }
shared_let(:planner) { create(:resource_planner, project:, principal: owner) }
shared_let(:work_package) { create(:work_package, project:) }
it "serializes filters using the same coder as UserQuery" do
coder = described_class.type_for_attribute(:user_filter).coder
@@ -191,7 +208,7 @@ RSpec.describe ResourceAllocation do
filter.operator = "~"
filter.values = ["alice"]
allocation = create(:resource_allocation, entity: planner, principal: owner, user_filter: [filter])
allocation = create(:resource_allocation, entity: work_package, principal: owner, user_filter: [filter])
reloaded = described_class.find(allocation.id)
expect(reloaded.user_filter.size).to eq(1)
@@ -201,7 +218,7 @@ RSpec.describe ResourceAllocation do
end
it "defaults to an empty array" do
allocation = create(:resource_allocation, entity: planner, principal: owner)
allocation = create(:resource_allocation, entity: work_package, principal: owner)
expect(allocation.reload.user_filter).to eq([])
end
end
@@ -35,12 +35,12 @@ RSpec.describe ResourceAllocations::CreateService, type: :model do
shared_let(:owner) do
create(:user, member_with_permissions: { project => %i[view_resource_planners allocate_user_resources] })
end
shared_let(:planner) { create(:resource_planner, project:, principal: owner) }
shared_let(:work_package) { create(:work_package, project:) }
let(:assignee) { create(:user, member_with_permissions: { project => %i[view_resource_planners] }) }
let(:params) do
{
entity: planner,
entity: work_package,
principal: assignee,
start_date: Date.new(2026, 1, 1),
end_date: Date.new(2026, 1, 31),
@@ -53,7 +53,7 @@ RSpec.describe ResourceAllocations::CreateService, type: :model do
it "creates a resource allocation" do
result = service_call
expect(result).to be_success, "expected success but got: #{result.errors.full_messages}"
expect(result.result.entity).to eq(planner)
expect(result.result.entity).to eq(work_package)
expect(result.result.principal).to eq(assignee)
expect(result.result.allocated_time).to eq(8)
end
@@ -35,9 +35,9 @@ RSpec.describe ResourceAllocations::DeleteService, type: :model do
shared_let(:owner) do
create(:user, member_with_permissions: { project => %i[view_resource_planners allocate_user_resources] })
end
shared_let(:planner) { create(:resource_planner, project:, principal: owner) }
shared_let(:work_package) { create(:work_package, project:) }
let!(:resource_allocation) { create(:resource_allocation, entity: planner, principal: owner) }
let!(:resource_allocation) { create(:resource_allocation, entity: work_package, principal: owner) }
subject(:service_call) do
described_class.new(user:, model: resource_allocation).call
@@ -35,10 +35,10 @@ RSpec.describe ResourceAllocations::UpdateService, type: :model do
shared_let(:owner) do
create(:user, member_with_permissions: { project => %i[view_resource_planners allocate_user_resources] })
end
shared_let(:planner) { create(:resource_planner, project:, principal: owner) }
shared_let(:work_package) { create(:work_package, project:) }
let!(:resource_allocation) do
create(:resource_allocation, entity: planner, principal: owner, state: "requested", allocated_time: 8)
create(:resource_allocation, entity: work_package, principal: owner, state: "requested", allocated_time: 8)
end
subject(:service_call) do
@@ -52,13 +52,13 @@ RSpec.describe ResourceAllocations::UpdateService, type: :model do
end
context "when attempting to change the entity" do
let(:other_planner) { create(:resource_planner, project:, principal: owner) }
let(:other_work_package) { create(:work_package, project:) }
it "fails because entity is not writable" do
result = described_class.new(user: owner, model: resource_allocation).call(entity: other_planner)
result = described_class.new(user: owner, model: resource_allocation).call(entity: other_work_package)
expect(result).not_to be_success
expect(result.errors.symbols_for(:entity_id)).to include(:error_readonly)
expect(resource_allocation.reload.entity).to eq(planner)
expect(resource_allocation.reload.entity).to eq(work_package)
end
end