From 6ece95935c1a652dc7f7079c4bf90edb3fc84d5f Mon Sep 17 00:00:00 2001 From: Stefanni Brasil Date: Mon, 6 Jul 2026 15:31:02 -0600 Subject: [PATCH 1/4] Filter product drive participants by business and contact names Currently there is no way to filter the table and users need to be able to filter down to the info they need. This commit adds two filters to product drive participants following the pattern of other screens such as Product Drives: - Business name: text field that does a partial match - Contact name: text field that does a partial match for example, "March" matches "Spring March Drive" - Exports (CSV) also respect these filters in the output Export filtered product drive participants --- .../product_drive_participants_controller.rb | 15 +++++-- app/models/product_drive_participant.rb | 5 ++- .../product_drive_participants/index.html.erb | 30 ++++++++++++++ spec/models/product_drive_participant_spec.rb | 20 ++++++++++ ...roduct_drive_participants_requests_spec.rb | 40 ++++++++++++++++++- 5 files changed, 104 insertions(+), 6 deletions(-) diff --git a/app/controllers/product_drive_participants_controller.rb b/app/controllers/product_drive_participants_controller.rb index 9925d4a05d..45bd3859f0 100644 --- a/app/controllers/product_drive_participants_controller.rb +++ b/app/controllers/product_drive_participants_controller.rb @@ -3,10 +3,15 @@ class ProductDriveParticipantsController < ApplicationController include Importable - # TODO: Should there be a :destroy action for this? - def index - @product_drive_participants = current_organization.product_drive_participants.includes(:donations).with_volumes.order(:business_name) + @product_drive_participants = current_organization + .product_drive_participants + .includes(:donations) + .with_volumes + .class_filter(filter_params) + .order(:business_name) + @selected_business_name_filter = filter_params[:by_business_name] + @selected_contact_name_filter = filter_params[:by_contact_name] respond_to do |format| format.html @@ -65,6 +70,8 @@ def product_drive_participant_params helper_method \ def filter_params - {} + return {} unless params.key?(:filters) + + params.require(:filters).permit(:by_business_name, :by_contact_name) end end diff --git a/app/models/product_drive_participant.rb b/app/models/product_drive_participant.rb index dc1ab6823e..7606c6798d 100644 --- a/app/models/product_drive_participant.rb +++ b/app/models/product_drive_participant.rb @@ -18,8 +18,9 @@ class ProductDriveParticipant < ApplicationRecord has_paper_trail - include Provideable + include Filterable include Geocodable + include Provideable has_many :donations, inverse_of: :product_drive_participant, dependent: :destroy @@ -30,6 +31,8 @@ class ProductDriveParticipant < ApplicationRecord validates :comment, length: { maximum: 500 } scope :alphabetized, -> { order(:contact_name) } + scope :by_business_name, ->(business_name) { where("business_name ILIKE ?", "%#{business_name}%") } + scope :by_contact_name, ->(contact_name) { where("contact_name ILIKE ?", "%#{contact_name}%") } scope :with_volumes, -> { left_joins(donations: :line_items) .select("product_drive_participants.*, SUM(COALESCE(line_items.quantity, 0)) AS volume") diff --git a/app/views/product_drive_participants/index.html.erb b/app/views/product_drive_participants/index.html.erb index 8fe5864221..0fb393614b 100644 --- a/app/views/product_drive_participants/index.html.erb +++ b/app/views/product_drive_participants/index.html.erb @@ -22,6 +22,36 @@ +
+
+
+
+
+

Product Drive Participants Filters

+
+
+ <%= form_tag(product_drive_participants_path, method: :get) do |f| %> +
+
+ <%= filter_text(label: "Filter By Business Name", scope: :by_business_name, selected: @selected_business_name_filter) %> +
+
+ <%= filter_text(label: "Filter By Contact Name", scope: :by_contact_name, selected: @selected_contact_name_filter) %> +
+
+
+ + <% end%> +
+
+
+
+
+
+
diff --git a/spec/models/product_drive_participant_spec.rb b/spec/models/product_drive_participant_spec.rb index 94abe444bd..2b38a8b36f 100644 --- a/spec/models/product_drive_participant_spec.rb +++ b/spec/models/product_drive_participant_spec.rb @@ -49,6 +49,26 @@ expect(subject.first.volume).to eq(10) end end + + describe ".by_business_name" do + it "returns the product drive participant with a given business name" do + cats_biz = create(:product_drive_participant, business_name: "Cats") + another_cats_biz = create(:product_drive_participant, business_name: "I like cats") + create(:product_drive_participant, business_name: "Dogs") + + expect(ProductDriveParticipant.by_business_name('Cats')).to match_array([cats_biz, another_cats_biz]) + end + end + + describe ".by_contact_name" do + it "returns the product drive participant with a given contact name" do + eleanor = create(:product_drive_participant, contact_name: "Eleanor Shellstrop") + donna = create(:product_drive_participant, contact_name: "Donna Shellstrop") + create(:product_drive_participant, contact_name: "Jason Mendoza") + + expect(ProductDriveParticipant.by_contact_name('Shellstrop')).to match_array([eleanor, donna]) + end + end end context "Methods" do diff --git a/spec/requests/product_drive_participants_requests_spec.rb b/spec/requests/product_drive_participants_requests_spec.rb index 54b17fccee..aef3edf32d 100644 --- a/spec/requests/product_drive_participants_requests_spec.rb +++ b/spec/requests/product_drive_participants_requests_spec.rb @@ -2,7 +2,7 @@ let(:organization) { create(:organization) } let(:user) { create(:user, organization: organization) } - context "While signed in" do + context "when signed in" do before do sign_in(user) end @@ -25,6 +25,44 @@ let(:response_format) { 'csv' } it { is_expected.to be_successful } + + it "exports the filtered results" do + create(:product_drive_participant, business_name: "A business named after cats") + create(:product_drive_participant, business_name: "Dogs are cool") + + get product_drive_participants_path(format: :csv, params: {filters: { by_business_name: 'cats'}}) + + csv = CSV.parse(response.body, headers: true) + + expect(csv.count).to eq(1) + expect(csv.first["Business Name"]).to eq("A business named after cats") + end + end + + context "when filtering by business name" do + it 'displays only results that matches the business name' do + create(:product_drive_participant, business_name: "A business named after cats") + create(:product_drive_participant, business_name: "Dogs are cool") + + get product_drive_participants_path(filters: {by_business_name: 'Cats'}) + + expect(response).to be_successful + expect(response.body).to include("Cats") + expect(response.body).not_to include("Dogs") + end + end + + context "when filtering by contact name" do + it 'displays only results that matches the contact name' do + create(:product_drive_participant, contact_name: "Bob Cat") + create(:product_drive_participant, contact_name: "Freddie the dog") + + get product_drive_participants_path(filters: {by_contact_name: 'Cats'}) + + expect(response).to be_successful + expect(response.body).to include("Cats") + expect(response.body).not_to include("Dogs") + end end end From 0643123793ed720f70b83c1004911de7da643847 Mon Sep 17 00:00:00 2001 From: Stefanni Brasil Date: Mon, 6 Jul 2026 15:56:14 -0600 Subject: [PATCH 2/4] Refactor ProductDriveParticipants controller To prevent this controller from getting bigger as new filters and functionalities are added, extracting the view variables to a View object keeps the controller cleaner. --- .../product_drive_participants_controller.rb | 11 ++----- app/models/view/product_drive_participants.rb | 33 +++++++++++++++++++ .../product_drive_participants/index.html.erb | 10 +++--- .../view/product_drive_participants_spec.rb | 21 ++++++++++++ 4 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 app/models/view/product_drive_participants.rb create mode 100644 spec/models/view/product_drive_participants_spec.rb diff --git a/app/controllers/product_drive_participants_controller.rb b/app/controllers/product_drive_participants_controller.rb index 45bd3859f0..211287394f 100644 --- a/app/controllers/product_drive_participants_controller.rb +++ b/app/controllers/product_drive_participants_controller.rb @@ -4,18 +4,11 @@ class ProductDriveParticipantsController < ApplicationController include Importable def index - @product_drive_participants = current_organization - .product_drive_participants - .includes(:donations) - .with_volumes - .class_filter(filter_params) - .order(:business_name) - @selected_business_name_filter = filter_params[:by_business_name] - @selected_contact_name_filter = filter_params[:by_contact_name] + @products = View::ProductDriveParticipants.new(params: params, organization: current_organization) respond_to do |format| format.html - format.csv { send_data ProductDriveParticipant.generate_csv(@product_drive_participants), filename: "ProductDriveParticipants-#{Time.zone.today}.csv" } + format.csv { send_data ProductDriveParticipant.generate_csv(@products.participants), filename: "ProductDriveParticipants-#{Time.zone.today}.csv" } end end diff --git a/app/models/view/product_drive_participants.rb b/app/models/view/product_drive_participants.rb new file mode 100644 index 0000000000..95287cf4ed --- /dev/null +++ b/app/models/view/product_drive_participants.rb @@ -0,0 +1,33 @@ +module View + class ProductDriveParticipants + attr_reader :filters, :params, :participants + + def initialize(params:, organization:) + @params = params + @filters = filter_params(params) + + @participants = organization + .product_drive_participants + .includes(:donations) + .with_volumes + .class_filter(filters) + .order(:business_name) + end + + def filter_params(params = {}) + if params.key?(:filters) + params.require(:filters).permit(:by_business_name, :by_contact_name) + else + {} + end + end + + def selected_business_name + filters[:by_business_name] + end + + def selected_contact_name + filters[:by_contact_name] + end + end +end diff --git a/app/views/product_drive_participants/index.html.erb b/app/views/product_drive_participants/index.html.erb index 0fb393614b..4da2763984 100644 --- a/app/views/product_drive_participants/index.html.erb +++ b/app/views/product_drive_participants/index.html.erb @@ -33,10 +33,10 @@ <%= form_tag(product_drive_participants_path, method: :get) do |f| %>
- <%= filter_text(label: "Filter By Business Name", scope: :by_business_name, selected: @selected_business_name_filter) %> + <%= filter_text(label: "Filter By Business Name", scope: :by_business_name, selected: @products.selected_business_name) %>
- <%= filter_text(label: "Filter By Contact Name", scope: :by_contact_name, selected: @selected_contact_name_filter) %> + <%= filter_text(label: "Filter By Contact Name", scope: :by_contact_name, selected: @products.selected_contact_name) %>
@@ -61,8 +61,8 @@
diff --git a/spec/models/view/product_drive_participants_spec.rb b/spec/models/view/product_drive_participants_spec.rb new file mode 100644 index 0000000000..3ff31f8f4b --- /dev/null +++ b/spec/models/view/product_drive_participants_spec.rb @@ -0,0 +1,21 @@ +RSpec.describe View::ProductDriveParticipants do + describe "selected filter params" do + it "returns the given filter params" do + organization = build(:organization) + build(:product_drive_participant, organization:) + params = ActionController::Parameters.new( + { + filters: { + by_business_name: 'The Good Place', + by_contact_name: 'Jason Mendoza' + } + } + ).permit! + + requests = View::ProductDriveParticipants.new(params:, organization:) + + expect(requests.selected_business_name).to eq("The Good Place") + expect(requests.selected_contact_name).to eq("Jason Mendoza") + end + end +end From bf2796da12d7b374c70054c592ba416c92c43871 Mon Sep 17 00:00:00 2001 From: Stefanni Brasil Date: Mon, 6 Jul 2026 16:10:50 -0600 Subject: [PATCH 3/4] linter linter --- app/views/product_drive_participants/index.html.erb | 2 +- spec/models/product_drive_participant_spec.rb | 4 ++-- spec/models/view/product_drive_participants_spec.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/product_drive_participants/index.html.erb b/app/views/product_drive_participants/index.html.erb index 4da2763984..2e471df069 100644 --- a/app/views/product_drive_participants/index.html.erb +++ b/app/views/product_drive_participants/index.html.erb @@ -44,7 +44,7 @@ <%= filter_button %> <%= clear_filter_button %>
- <% end%> + <% end %>
diff --git a/spec/models/product_drive_participant_spec.rb b/spec/models/product_drive_participant_spec.rb index 2b38a8b36f..6af8b0b7d8 100644 --- a/spec/models/product_drive_participant_spec.rb +++ b/spec/models/product_drive_participant_spec.rb @@ -56,7 +56,7 @@ another_cats_biz = create(:product_drive_participant, business_name: "I like cats") create(:product_drive_participant, business_name: "Dogs") - expect(ProductDriveParticipant.by_business_name('Cats')).to match_array([cats_biz, another_cats_biz]) + expect(ProductDriveParticipant.by_business_name("Cats")).to match_array([cats_biz, another_cats_biz]) end end @@ -66,7 +66,7 @@ donna = create(:product_drive_participant, contact_name: "Donna Shellstrop") create(:product_drive_participant, contact_name: "Jason Mendoza") - expect(ProductDriveParticipant.by_contact_name('Shellstrop')).to match_array([eleanor, donna]) + expect(ProductDriveParticipant.by_contact_name("Shellstrop")).to match_array([eleanor, donna]) end end end diff --git a/spec/models/view/product_drive_participants_spec.rb b/spec/models/view/product_drive_participants_spec.rb index 3ff31f8f4b..7e295f0f14 100644 --- a/spec/models/view/product_drive_participants_spec.rb +++ b/spec/models/view/product_drive_participants_spec.rb @@ -6,8 +6,8 @@ params = ActionController::Parameters.new( { filters: { - by_business_name: 'The Good Place', - by_contact_name: 'Jason Mendoza' + by_business_name: "The Good Place", + by_contact_name: "Jason Mendoza" } } ).permit! From c73dbdc0b88015834554d721852448dd4a5230cb Mon Sep 17 00:00:00 2001 From: Stefanni Brasil Date: Mon, 6 Jul 2026 16:20:52 -0600 Subject: [PATCH 4/4] Trigger Build