Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/allocations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ def set_scenario
end

def allocation_params
params.require(:allocation).permit(:allocation_category_id, :option, :percentage, :amount, :note, :type)
params.require(:allocation).permit(:allocation_category_id, :option, :percentage, :amount, :note, :type, preference_category_ids: [])
end
end
6 changes: 6 additions & 0 deletions app/javascript/controllers/dialog_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ export default class extends Controller {
close() {
this.dialogTarget.close()
}

backdropClose(event) {
if (event.target === this.dialogTarget) {
this.dialogTarget.close()
}
}
}
3 changes: 3 additions & 0 deletions app/models/allocation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ class Allocation < ApplicationRecord
belongs_to :scenario
belongs_to :allocation_category, optional: true

has_many :allocation_preferences, dependent: :destroy
has_many :preference_categories, through: :allocation_preferences, source: :allocation_category

validate :category_or_option_present

def display_label
Expand Down
1 change: 1 addition & 0 deletions app/models/allocation_category.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class AllocationCategory < ApplicationRecord
belongs_to :parent, class_name: "AllocationCategory", optional: true
has_many :children, class_name: "AllocationCategory", foreign_key: :parent_id, dependent: :destroy
has_many :allocations, dependent: :nullify
has_many :allocation_preferences, dependent: :destroy

validates :name, presence: true
validates :type, inclusion: { in: ->(_) { TAB_CLASSES } }
Expand Down
4 changes: 4 additions & 0 deletions app/models/allocation_preference.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class AllocationPreference < ApplicationRecord
belongs_to :allocation
belongs_to :allocation_category
end
28 changes: 28 additions & 0 deletions app/views/scenarios/_additional_preferences.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<%# locals: (allocation:, scenario:, suffix:) %>
<% roots_by_type = scenario.organization.allocation_categories
.where(parent_id: nil).order(:name).group_by(&:type) %>
<% selected_ids = allocation.preference_category_ids %>

<% if roots_by_type.any? %>
<div class="mt-6">
<span class="block text-sm text-ink-soft">Additional preferences</span>

<%# Ensures the list is cleared when nothing is checked. %>
<%= hidden_field_tag "allocation[preference_category_ids][]", "" %>

<% AllocationCategory::TAB_CLASSES.each do |klass_name| %>
<% roots = roots_by_type[klass_name] %>
<% next if roots.blank? %>
<p class="mt-4 text-xs font-medium uppercase tracking-wide text-ink-faint"><%= klass_name.constantize.tab_label %></p>
<div class="mt-2 flex flex-wrap gap-2">
<% roots.each do |cat| %>
<label class="inline-block cursor-pointer rounded-full border border-line bg-surface px-3 py-1 text-sm text-ink-soft transition hover:bg-canvas has-[:checked]:border-accent has-[:checked]:bg-accent has-[:checked]:text-white">
<%= check_box_tag "allocation[preference_category_ids][]", cat.id, selected_ids.include?(cat.id),
id: "pref_#{suffix}_#{cat.id}", class: "sr-only" %>
<%= cat.name %>
</label>
<% end %>
</div>
<% end %>
</div>
<% end %>
5 changes: 4 additions & 1 deletion app/views/scenarios/_allocation.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<div class="min-w-0">
<p class="font-medium text-ink truncate"><%= allocation.display_label %></p>
<p class="mt-0.5 text-sm text-ink-soft truncate">
<%= allocation.note.presence || "No additional preferences" %>
<%= allocation.preference_categories.map(&:name).join(", ").presence || "No additional preferences" %>
</p>
</div>
<div class="flex items-start gap-3 shrink-0">
Expand Down Expand Up @@ -63,6 +63,9 @@
<div class="flex items-start justify-between gap-3 rounded-lg border border-line-soft bg-surface px-4 py-3 transition hover:shadow-sm">
<div class="min-w-0">
<p class="font-medium text-ink truncate"><%= allocation.display_label %></p>
<% if allocation.preference_categories.any? %>
<p class="mt-0.5 text-sm text-ink-soft"><%= allocation.preference_categories.map(&:name).join(", ") %></p>
<% end %>
<% if allocation.note.present? %>
<p class="mt-1 text-sm text-ink-soft"><%= allocation.note %></p>
<% end %>
Expand Down
4 changes: 3 additions & 1 deletion app/views/scenarios/_allocation_modal.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<% suffix = editing ? dom_id(allocation) : klass.model_name.element %>
<% percentage = allocation.percentage || 20 %>
<% color ||= ScenariosHelper::CHART_COLORS.first %>
<dialog data-dialog-target="dialog" class="m-auto w-full max-w-lg rounded-2xl p-0 shadow-lg backdrop:bg-[rgba(20,18,14,0.48)]">
<dialog data-dialog-target="dialog" data-action="click->dialog#backdropClose" class="m-auto w-full max-w-lg rounded-2xl p-0 shadow-lg backdrop:bg-[rgba(20,18,14,0.48)]">
<%= form_with url: (editing ? scenario_allocation_path(scenario, allocation) : scenario_allocations_path(scenario)), method: (editing ? :patch : :post), class: "p-8" do |form| %>
<%= hidden_field_tag "allocation[type]", klass.name %>
<h2 class="font-serif font-medium text-2xl text-ink"><%= editing ? "Edit allocation" : "Create allocation" %></h2>
Expand Down Expand Up @@ -34,6 +34,8 @@
</div>
<% end %>

<%= render "scenarios/additional_preferences", allocation: allocation, scenario: scenario, suffix: suffix %>

<div class="mt-6">
<%= label_tag "allocation_note_#{suffix}", "Note (optional)", class: "block text-sm text-ink-soft" %>
<%= text_area_tag "allocation[note]", allocation.note, id: "allocation_note_#{suffix}", rows: 3, placeholder: "Extra preferences, restrictions, e.g. need-based scholarships only, exclude specific orgs…",
Expand Down
4 changes: 2 additions & 2 deletions app/views/scenarios/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
<%= render "allocation_section",
title: "On going giving",
klass: Allocation::Ongoing,
allocations: @scenario.ongoing_allocations,
allocations: @scenario.ongoing_allocations.includes(:allocation_category, :preference_categories),
scenario: @scenario %>

<%= render "allocation_section",
title: "One time giving",
klass: Allocation::OneTime,
allocations: @scenario.one_time_allocations,
allocations: @scenario.one_time_allocations.includes(:allocation_category, :preference_categories),
scenario: @scenario %>
</div>

Expand Down
12 changes: 12 additions & 0 deletions db/migrate/20260627000000_create_allocation_preferences.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateAllocationPreferences < ActiveRecord::Migration[8.1]
def change
create_table :allocation_preferences do |t|
t.references :allocation, null: false, foreign_key: true
t.references :allocation_category, null: false, foreign_key: true

t.timestamps
end

add_index :allocation_preferences, [ :allocation_id, :allocation_category_id ], unique: true, name: "index_allocation_preferences_uniqueness"
end
end
14 changes: 13 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@

education_category = arlington.allocation_categories.find_by!(name: "Education")
youth_category = arlington.allocation_categories.find_by!(name: "Children and Youth")
low_income_category = arlington.allocation_categories.find_by!(name: "Low-Income")

owner = User.find_by!(email_address: "owner@example.com")

Expand All @@ -79,7 +80,8 @@
end

if balanced.allocations.empty?
balanced.ongoing_allocations.create!(allocation_category: education_category, percentage: 30)
balanced.ongoing_allocations.create!(allocation_category: education_category, percentage: 30,
preference_categories: [ youth_category, low_income_category ])
balanced.ongoing_allocations.create!(allocation_category: youth_category, percentage: 40)
# Demonstrates the free-text fallback for needs without a curated category.
balanced.ongoing_allocations.create!(option: "Greatest Community Need", percentage: 30)
Expand Down
23 changes: 23 additions & 0 deletions test/controllers/allocations_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@ class AllocationsControllerTest < ActionDispatch::IntegrationTest
assert_equal category, @scenario.allocations.order(:created_at).last.allocation_category
end

test "creates an allocation with additional preferences" do
youth = allocation_categories(:population_youth)
education = allocation_categories(:program_education)
post scenario_allocations_url(@scenario), params: {
allocation: { type: "Allocation::Ongoing", option: "Mixed", percentage: 25,
preference_category_ids: [ "", youth.id, education.id ] }
}
assert_redirected_to scenario_path(@scenario)
allocation = @scenario.allocations.order(:created_at).last
assert_equal [ youth, education ].sort_by(&:id), allocation.preference_categories.sort_by(&:id)
end

test "updating with an empty preference list clears existing preferences" do
allocation = allocations(:greatest_need)
allocation.update!(preference_category_ids: [ allocation_categories(:population_youth).id ])

patch scenario_allocation_url(@scenario, allocation), params: {
allocation: { preference_category_ids: [ "" ] }
}
assert_redirected_to scenario_path(@scenario)
assert_empty allocation.reload.preference_categories
end

test "creates a one time allocation" do
assert_difference -> { @scenario.allocations.count }, 1 do
post scenario_allocations_url(@scenario), params: {
Expand Down
12 changes: 12 additions & 0 deletions test/models/allocation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ class AllocationTest < ActiveSupport::TestCase
assert_equal 1500, allocations(:greatest_need).dollar_amount
end

test "preference_categories can be assigned and destroying the allocation removes the join rows" do
allocation = allocations(:greatest_need)
youth = allocation_categories(:population_youth)
education = allocation_categories(:program_education)
allocation.update!(preference_category_ids: [ youth.id, education.id ])
assert_equal [ youth, education ].sort_by(&:id), allocation.preference_categories.sort_by(&:id)

assert_difference -> { AllocationPreference.count }, -2 do
allocation.destroy
end
end

test "kind predicates reflect the subclass" do
assert allocations(:greatest_need).ongoing?
assert_not allocations(:greatest_need).one_time?
Expand Down
Loading