diff --git a/app/controllers/grants_controller.rb b/app/controllers/grants_controller.rb index 38be97e29..92ad1f63a 100644 --- a/app/controllers/grants_controller.rb +++ b/app/controllers/grants_controller.rb @@ -100,6 +100,12 @@ def filter_grants(scope) scope = scope.where(funder_type: params[:funder_type]) if Grant::FUNDER_TYPES.include?(params[:funder_type]) scope = scope.where(funder_id: params[:funder_id]) if params[:funder_id].present? + scope = case params[:planned_giving] + when "yes" then scope.where(planned_giving: true) + when "no" then scope.where(planned_giving: false) + else scope + end + # Sector/category filters back the "View all grants" deep link from the # taggings browse page (see TaggingsHelper#tagged_index_path). scope = scope.sector_names_all(params[:sector_names_all]) if params[:sector_names_all].present? @@ -155,6 +161,7 @@ def grant_params params.require(:grant).permit( :name, :description, :amount_dollars, :amount_cents, :funder_sgid, :funds_allocation_deadline, :funds_received_on, :eligibility_criteria, :tasks, + :planned_giving, sector_ids: [], category_ids: [], primary_asset_attributes: [ :id, :file, :_destroy ], gallery_assets_attributes: [ :id, :file, :_destroy ] diff --git a/app/decorators/grant_decorator.rb b/app/decorators/grant_decorator.rb index ac78b1278..2bcbf3516 100644 --- a/app/decorators/grant_decorator.rb +++ b/app/decorators/grant_decorator.rb @@ -33,6 +33,16 @@ def fully_allocated? object.remaining_cents <= 0 end + # Amber pill flagging a grant as a Legacy scholarship (funded through planned + # giving). Renders nothing for ordinary grants, so callers can inline it. + def legacy_scholarship_badge + return unless object.planned_giving? + + h.tag.span("Legacy scholarship", + class: "inline-flex items-center rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-800", + title: "Funded through planned giving") + end + # Whole-number percentage of the grant awarded in scholarships, clamped to # 0–100, for rendering the allocation progress bar on the index. def allocation_percentage diff --git a/app/views/grants/_filters.html.erb b/app/views/grants/_filters.html.erb index 0fab8f3d7..b58e8356f 100644 --- a/app/views/grants/_filters.html.erb +++ b/app/views/grants/_filters.html.erb @@ -32,6 +32,13 @@ include_blank: "All", class: field_class %> +
+ <%= label_tag :planned_giving, "Legacy scholarship", class: label_class %> + <%= select_tag :planned_giving, + options_for_select([ [ "Legacy scholarships", "yes" ], [ "Other grants", "no" ] ], params[:planned_giving]), + include_blank: "All", + class: field_class %> +
<%= label_tag :tasks, "Tasks completed", class: label_class %> <%= select_tag :tasks, diff --git a/app/views/grants/_form.html.erb b/app/views/grants/_form.html.erb index 11b24531c..da2a4f6c2 100644 --- a/app/views/grants/_form.html.erb +++ b/app/views/grants/_form.html.erb @@ -3,10 +3,21 @@ <%= render "shared/errors", resource: @grant if @grant.errors.any? %>
-
- <%= f.input :name, - label: "Grant name", - input_html: { class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" } %> +
+
+ <%= f.input :name, + label: "Grant name", + input_html: { class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" } %> +
+
+ <%= f.label :amount_dollars, "Funding amount", class: "block text-sm font-medium text-gray-700 mb-1" %> +
+ $ + <%= f.number_field :amount_dollars, step: 0.01, value: @grant.amount_dollars, + class: "pl-7 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" %> +
+

Total scholarship amounts awarded from this grant cannot exceed this.

+
@@ -27,15 +38,13 @@ label: "Funder", hint: "The organization or person funding the grant." %>
-
- <%= f.label :amount_dollars, "Funding amount", class: "block text-sm font-medium text-gray-700 mb-1" %> -
- $ - <%= f.number_field :amount_dollars, step: 0.01, value: @grant.amount_dollars, - class: "pl-7 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" %> -
-

Total scholarship amounts awarded from this grant cannot exceed this.

-
+
diff --git a/app/views/grants/_grant.html.erb b/app/views/grants/_grant.html.erb index 1292559e1..255227842 100644 --- a/app/views/grants/_grant.html.erb +++ b/app/views/grants/_grant.html.erb @@ -4,7 +4,10 @@ rather than their formatted text. %> - <%= link_to grant.name, grant_path(grant), class: "inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-900 shadow-sm transition-colors hover:border-gray-400 hover:bg-gray-50", data: { turbo_frame: "_top" } %> +
+ <%= link_to grant.name, grant_path(grant), class: "inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-900 shadow-sm transition-colors hover:border-gray-400 hover:bg-gray-50", data: { turbo_frame: "_top" } %> + <%= grant.legacy_scholarship_badge %> +
<%= grant_funder_badge(grant) %> <%= grant.amount %> diff --git a/app/views/grants/show.html.erb b/app/views/grants/show.html.erb index 8cfed35ad..ffd44a364 100644 --- a/app/views/grants/show.html.erb +++ b/app/views/grants/show.html.erb @@ -16,7 +16,10 @@ <% end %>
-

<%= grant.name %>

+
+

<%= grant.name %>

+ <%= grant.legacy_scholarship_badge %> +

<%= grant_funder_badge(grant) %>

<% if allowed_to?(:edit?, @grant) %> diff --git a/db/migrate/20260814230559_add_planned_giving_to_grants.rb b/db/migrate/20260814230559_add_planned_giving_to_grants.rb new file mode 100644 index 000000000..eb97a63ad --- /dev/null +++ b/db/migrate/20260814230559_add_planned_giving_to_grants.rb @@ -0,0 +1,9 @@ +class AddPlannedGivingToGrants < ActiveRecord::Migration[8.1] + def up + add_column :grants, :planned_giving, :boolean, default: false, null: false + end + + def down + remove_column :grants, :planned_giving, if_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 8da6a717f..964b86fdf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -715,6 +715,7 @@ t.date "funds_allocation_deadline" t.date "funds_received_on" t.string "name", null: false + t.boolean "planned_giving", default: false, null: false t.text "tasks" t.datetime "updated_at", null: false t.bigint "updated_by_id" diff --git a/spec/decorators/grant_decorator_spec.rb b/spec/decorators/grant_decorator_spec.rb index 39c889e20..1e52ae714 100644 --- a/spec/decorators/grant_decorator_spec.rb +++ b/spec/decorators/grant_decorator_spec.rb @@ -132,4 +132,15 @@ expect(grant.decorate).not_to be_fully_allocated end end + + describe "#legacy_scholarship_badge" do + it "renders a Legacy scholarship pill for planned-giving grants" do + badge = create(:grant, :planned_giving).decorate.legacy_scholarship_badge + expect(badge).to include("Legacy scholarship") + end + + it "renders nothing for ordinary grants" do + expect(create(:grant).decorate.legacy_scholarship_badge).to be_nil + end + end end diff --git a/spec/factories/grants.rb b/spec/factories/grants.rb index c36fb65b1..66634f763 100644 --- a/spec/factories/grants.rb +++ b/spec/factories/grants.rb @@ -21,5 +21,9 @@ trait :donated_by_person do association :funder, factory: :person end + + trait :planned_giving do + planned_giving { true } + end end end diff --git a/spec/models/grant_spec.rb b/spec/models/grant_spec.rb index c594b5154..5846e59c8 100644 --- a/spec/models/grant_spec.rb +++ b/spec/models/grant_spec.rb @@ -49,6 +49,12 @@ end end + describe "planned_giving" do + it "defaults to false" do + expect(Grant.new.planned_giving).to be(false) + end + end + describe "validations" do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_numericality_of(:amount_cents).is_greater_than_or_equal_to(0) } diff --git a/spec/requests/grants_spec.rb b/spec/requests/grants_spec.rb index c373a697e..9d9a1e540 100644 --- a/spec/requests/grants_spec.rb +++ b/spec/requests/grants_spec.rb @@ -81,6 +81,19 @@ expect(response.body).not_to include("Org grant") end + it "filters by legacy scholarship (planned giving)" do + legacy = create(:grant, :planned_giving, name: "Legacy fund") + ordinary = create(:grant, name: "Ordinary fund") + + get grants_url(planned_giving: "yes"), headers: frame_headers + expect(response.body).to include("Legacy fund") + expect(response.body).not_to include("Ordinary fund") + + get grants_url(planned_giving: "no"), headers: frame_headers + expect(response.body).to include("Ordinary fund") + expect(response.body).not_to include("Legacy fund") + end + it "filters by grant name" do create(:grant, name: "Healing Arts Fund") create(:grant, name: "Music Therapy Grant") @@ -227,6 +240,12 @@ expect(Grant.last.primary_asset.file).to be_attached end + it "flags the grant as planned giving when checked" do + post grants_url, params: { grant: valid_attributes.merge(planned_giving: "1") } + + expect(Grant.last).to be_planned_giving + end + it "attaches the selected sectors and categories" do sector = create(:sector, :published) category = create(:category, :published)