Add methods to resolve the user filter to users

This commit is contained in:
Klaus Zanders
2026-06-02 17:26:21 +02:00
parent 9be91ca804
commit 3466daba5e
4 changed files with 102 additions and 9 deletions
@@ -40,6 +40,7 @@ module ResourceAllocations
attribute :end_date
attribute :allocated_time
attribute :user_filter
attribute :filter_name
validate :user_allowed_to_allocate
@@ -94,6 +94,14 @@ class ResourceAllocation < ApplicationRecord
principal_id.present?
end
def candidate_query
UserQuery.new.tap do |query|
user_filter.each do |filter|
query.where(filter.field, filter.operator, filter.values)
end
end
end
def allocated_hours
return if allocated_time.nil?
@@ -44,6 +44,7 @@ FactoryBot.define do
trait :with_user_filter do
principal { nil }
filter_name { "Full stack Developer (DE-EN)" }
transient do
job_title_custom_field do
UserCustomField.find_by(name: "Job title") ||
@@ -51,17 +52,28 @@ FactoryBot.define do
name: "Job title",
possible_values: ["Developer", "Designer", "Project Manager", "Product Manager"])
end
spoken_language_custom_field do
UserCustomField.find_by(name: "Spoken language") ||
create(:user_custom_field, :list,
name: "Spoken language",
multi_value: true,
possible_values: %w[German English French Spanish Italian Dutch Portuguese Polish])
end
end
# Build real UserQuery filter objects (not hashes): the serialization
# coder dumps via `filter.field`, so it only accepts filter instances.
# The filter matches developers who speak German or English ("DE-EN"),
# leaving the other languages as non-matching values to test against.
user_filter do
cf = job_title_custom_field
developer_option = cf.custom_options.find_by(value: "Developer")
[
{
"attribute" => cf.column_name,
"operator" => "=",
"values" => [developer_option.id.to_s]
}
]
job_title = job_title_custom_field
language = spoken_language_custom_field
developer_option = job_title.custom_options.find_by(value: "Developer")
language_options = language.custom_options.where(value: %w[German English])
query = UserQuery.new
query.where(job_title.column_name, "=", [developer_option.id.to_s])
query.where(language.column_name, "=", language_options.map { |option| option.id.to_s })
query.filters
end
end
end
@@ -379,5 +379,77 @@ RSpec.describe ResourceAllocation do
allocation = create(:resource_allocation, entity: work_package, principal: owner)
expect(allocation.reload.user_filter).to eq([])
end
it "round-trips the custom-field filters from the :with_user_filter trait" do
allocation = create(:resource_allocation, :with_user_filter, entity: work_package)
filters = allocation.reload.user_filter
expect(filters.size).to eq(2)
job_title = UserCustomField.find_by(name: "Job title")
language = UserCustomField.find_by(name: "Spoken language")
job_title_filter = filters.find { |f| f.name.to_s == job_title.column_name }
language_filter = filters.find { |f| f.name.to_s == language.column_name }
expect(job_title_filter.operator).to eq("=")
expect(job_title_filter.values).to eq(job_title.custom_options.where(value: "Developer").pluck(:id).map(&:to_s))
# "is (OR)" — matches users speaking German or English.
expect(language_filter.operator).to eq("=")
expect(language_filter.values)
.to match_array(language.custom_options.where(value: %w[German English]).pluck(:id).map(&:to_s))
end
end
describe "matching users with the :with_user_filter criteria" do
shared_let(:project) { create(:project, enabled_module_names: %w[resource_management]) }
shared_let(:work_package) { create(:work_package, project:) }
# Materializes the "Job title" and "Spoken language" custom fields and the
# Developer + (German OR English) filter.
shared_let(:allocation) { create(:resource_allocation, :with_user_filter, entity: work_package) }
shared_let(:job_title) { UserCustomField.find_by(name: "Job title") }
shared_let(:language) { UserCustomField.find_by(name: "Spoken language") }
def option_id(custom_field, value)
custom_field.custom_options.find_by(value:).id
end
def user_with(job_title_value, *languages)
create(:user).tap do |user|
user.custom_field_values = {
job_title.id => option_id(job_title, job_title_value),
language.id => languages.map { |spoken| option_id(language, spoken) }
}
user.save!(validate: false)
end
end
shared_let(:german_developer) { user_with("Developer", "German") }
shared_let(:english_developer) { user_with("Developer", "English") }
shared_let(:bilingual_developer) { user_with("Developer", "French", "English") }
shared_let(:french_developer) { user_with("Developer", "French") }
shared_let(:german_designer) { user_with("Designer", "German") }
describe "#candidate_query" do
# `UserQuery#results` is scoped to what the current user may see.
current_user { create(:admin) }
it "is a UserQuery carrying the stored filter criteria" do
query = allocation.candidate_query
expect(query).to be_a(UserQuery)
expect(query.filters.map { |f| f.name.to_s })
.to contain_exactly(job_title.column_name, language.column_name)
end
it "resolves to developers speaking German or English (is (OR)), and excludes the rest" do
results = allocation.candidate_query.results
expect(results).to include(german_developer, english_developer, bilingual_developer)
expect(results).not_to include(french_developer, german_designer)
end
end
end
end