diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index d2a397df7..49c5618c7 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -8,7 +8,8 @@ def index if turbo_frame_request? per_page = params[:number_of_items_per_page].presence || 25 base_scope = authorized_scope(Organization.includes( - :windows_type, :organization_status, :sectors, :addresses, + :organization_status, :sectors, :addresses, + { windows_type: { categorizable_items: { category: :category_type } } }, { categorizable_items: { category: :category_type } }, logo_attachment: :blob )) @@ -189,6 +190,7 @@ def set_form_variables def set_index_variables @organization_statuses = OrganizationStatus.all + @age_range_categories = Category.age_ranges.published.ordered_by_position_and_name end def populations_served diff --git a/app/models/organization.rb b/app/models/organization.rb index b1ed44097..7350ef586 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -91,6 +91,26 @@ class Organization < ApplicationRecord scope :organization_ids, ->(organization_ids) { where(id: organization_ids.to_s.split("-").map(&:to_i)) } scope :project_ids, ->(project_ids) { where(id: project_ids.to_s.split("-").map(&:to_i)) } scope :published, -> { active } + # Filters by AgeRange category name(s), powering the unified "Age range" column + # on the index. An org matches a selected age range when EITHER it (or an + # affiliated person) is tagged with that age range, OR its windows audience + # semantically covers it. The windows-type leg keeps orgs that carry a windows + # audience but no age-range tags — or tags that disagree with it — in results. + scope :age_range_names, ->(names) do + return all if names.blank? + parsed = Array(names).flat_map { |n| n.to_s.split("--") }.map(&:strip).reject(&:blank?).map(&:downcase) + return all if parsed.empty? + + category_ids = Category.age_ranges.where("LOWER(categories.name) IN (?)", parsed).select(:id) + org_tagged = CategorizableItem.where(categorizable_type: "Organization", category_id: category_ids).select(:categorizable_id) + person_tagged = CategorizableItem.where(categorizable_type: "Person", category_id: category_ids).select(:categorizable_id) + via_affiliations = Affiliation.where(person_id: person_tagged).select(:organization_id) + windows_type_ids = WindowsType.joins(:categorizable_items) + .where(categorizable_items: { category_id: category_ids }) + .select(:id) + + where(id: org_tagged).or(where(id: via_affiliations)).or(where(windows_type_id: windows_type_ids)) + end def self.search_by_params(params) organizations = is_a?(ActiveRecord::Relation) ? self : all @@ -98,7 +118,7 @@ def self.search_by_params(params) organizations = organizations.sector_names_all(params[:sector_names_all]) if params[:sector_names_all].present? organizations = organizations.category_names_all(params[:category_names_all]) if params[:category_names_all].present? organizations = organizations.address(params[:address]) if params[:address].present? - organizations = organizations.windows_type_name(params[:windows_type_name]) if params[:windows_type_name].present? + organizations = organizations.age_range_names(params[:age_range_name]) if params[:age_range_name].present? organizations = organizations.organization_ids(params[:organization_ids]) if params[:organization_ids].present? organizations = organizations.where(organization_status_id: params[:organization_status_id]) if params[:organization_status_id].present? organizations @@ -265,6 +285,17 @@ def all_additional_age_groups collect_age_groups(:additional_age_groups) - all_primary_age_groups end + # Whether the windows audience adds age information the org's age-range tags + # don't already convey — drives the fallback windows-type badge in the index's + # age range column. True when the org has a windows type and either carries no + # age-range tags at all, or its tags omit an age range that windows type covers. + def windows_audience_unreflected_by_age_tags? + return false unless windows_type + tagged = all_primary_age_groups + all_additional_age_groups + return true if tagged.empty? + windows_type.tagged_age_range_categories.any? { |category| tagged.exclude?(category) } + end + remote_searchable_by :name # Returns the website as a clickable, scheme-qualified URL — prepending diff --git a/app/models/windows_type.rb b/app/models/windows_type.rb index 0b3c7239a..81bfa5086 100644 --- a/app/models/windows_type.rb +++ b/app/models/windows_type.rb @@ -16,4 +16,12 @@ class WindowsType < ApplicationRecord validates :name, presence: true validates :short_name, presence: true + + # AgeRange categories tagged on this windows type, read from already-loaded + # categorizable_items (no query) so the organizations index can compare them + # against an org's age-range tags without an N+1. Use the :age_ranges + # association when you want them ordered and don't already have the items loaded. + def tagged_age_range_categories + categorizable_items.map(&:category).compact.select { |category| category.category_type&.name == "AgeRange" } + end end diff --git a/app/views/organizations/_search_boxes.html.erb b/app/views/organizations/_search_boxes.html.erb index c06da8533..75375195a 100644 --- a/app/views/organizations/_search_boxes.html.erb +++ b/app/views/organizations/_search_boxes.html.erb @@ -10,12 +10,12 @@ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>