From 16d9c86f775099dfd5f355ccecd2bda79a030749 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 12 Jun 2026 12:46:21 +0200 Subject: [PATCH] Implement scope to get all `ResourceAllocation` objects for a given project --- .../app/models/resource_allocation.rb | 22 ++++++++++++++++--- .../spec/models/resource_allocation_spec.rb | 19 ++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/modules/resource_management/app/models/resource_allocation.rb b/modules/resource_management/app/models/resource_allocation.rb index d7f07b163f6..c5268739d05 100644 --- a/modules/resource_management/app/models/resource_allocation.rb +++ b/modules/resource_management/app/models/resource_allocation.rb @@ -31,9 +31,17 @@ class ResourceAllocation < ApplicationRecord ALLOWED_ENTITY_TYPES = %w[WorkPackage].freeze - # `allocated_time` is stored in minutes in an integer column. Cap it at 5000 - # hours so an absurdly large input is rejected with a validation error rather - # than overflowing the column and raising ActiveModel::RangeError on save. + # How to reach a project from each polymorphic entity type. Must have one entry for each ALLOWED_ENTITY_TYPES + ENTITY_PROJECT_JOINS = { + "WorkPackage" => { + join: <<~SQL.squish, + LEFT JOIN work_packages ON work_packages.id = resource_allocations.entity_id AND resource_allocations.entity_type = 'WorkPackage' + SQL + project_id: "work_packages.project_id" + } + }.freeze + + # Cap to avoid integer overflows. MAX_ALLOCATED_TIME = (5000.hours / 1.minute).to_i belongs_to :entity, polymorphic: true, optional: false @@ -53,6 +61,7 @@ class ResourceAllocation < ApplicationRecord register_journal_formatted_fields "entity_gid", formatter_key: :polymorphic_association register_journal_formatted_fields "filter_name", formatter_key: :plaintext + # State machine is ignored for the current implementation. All allocations go directly to the `allocated` state enum :state, { requested: "requested", allocated: "allocated", @@ -62,6 +71,13 @@ class ResourceAllocation < ApplicationRecord scope :needs_principal_assignment, -> { where(principal_explicit: false, principal_id: nil) } scope :for_principal, ->(principal) { where(principal:) } + scope :for_project, ->(project_or_project_id) { + project_id = project_or_project_id.is_a?(Project) ? project_or_project_id.id : project_or_project_id + joins = ENTITY_PROJECT_JOINS.values.pluck(:join) + conditions = ENTITY_PROJECT_JOINS.values.map { |source| "#{source[:project_id]} = :project_id" } + + joins(joins.join(" ")).where(conditions.join(" OR "), project_id: project_id) + } # The `allocated` allocations for the given work packages, grouped by work # package id and with principals eager-loaded. Loaded once per page so the diff --git a/modules/resource_management/spec/models/resource_allocation_spec.rb b/modules/resource_management/spec/models/resource_allocation_spec.rb index e5d47a6e826..9023c3c3bfc 100644 --- a/modules/resource_management/spec/models/resource_allocation_spec.rb +++ b/modules/resource_management/spec/models/resource_allocation_spec.rb @@ -269,6 +269,25 @@ RSpec.describe ResourceAllocation do end end + describe ".for_project" do + shared_let(:project) { create(:project) } + shared_let(:other_project) { create(:project) } + shared_let(:work_package) { create(:work_package, project:) } + shared_let(:other_work_package) { create(:work_package, project: other_project) } + + let!(:in_project) { create(:resource_allocation, entity: work_package) } + + before { create(:resource_allocation, entity: other_work_package) } + + it "returns only allocations whose entity belongs to the given project" do + expect(described_class.for_project(project)).to contain_exactly(in_project) + end + + it "accepts a project id as well as a record" do + expect(described_class.for_project(project.id)).to contain_exactly(in_project) + end + end + describe "entity GlobalID handling" do shared_let(:project) { create(:project) } shared_let(:work_package) { create(:work_package, project:) }