diff --git a/app/controllers/concerns/ahoy_tracking.rb b/app/controllers/concerns/ahoy_tracking.rb
index f74a1e771..cbc7441da 100644
--- a/app/controllers/concerns/ahoy_tracking.rb
+++ b/app/controllers/concerns/ahoy_tracking.rb
@@ -14,7 +14,7 @@ def track_index_intent(resource_class, scope, params)
if scope.respond_to?(:total_entries) # will_paginate
scope.total_entries
elsif scope.respond_to?(:count)
- count = scope.count
+ count = scope.unscope(:select).count
count.is_a?(Hash) ? count.size : count
else
0
diff --git a/app/controllers/workshops_controller.rb b/app/controllers/workshops_controller.rb
index 01fbcf61a..3d17352c7 100644
--- a/app/controllers/workshops_controller.rb
+++ b/app/controllers/workshops_controller.rb
@@ -4,8 +4,8 @@ class WorkshopsController < ApplicationController
def index
authorize!
- @category_types = CategoryType.published.order(:name).decorate
- @sectors = Sector.published
+ @category_types = CategoryType.published.where(story_specific: false).order(:name).decorate
+ @sectors = Sector.published.order(:name)
@windows_types = WindowsType.all
if turbo_frame_request?
@@ -21,6 +21,7 @@ def index
render :workshop_results
else
+ @sort = params[:sort].presence || "title"
render :index
end
end
diff --git a/app/frontend/javascript/controllers/collection_controller.js b/app/frontend/javascript/controllers/collection_controller.js
index a9faf4c0b..bb43eaa1e 100644
--- a/app/frontend/javascript/controllers/collection_controller.js
+++ b/app/frontend/javascript/controllers/collection_controller.js
@@ -67,6 +67,7 @@ export default class extends Controller {
input.checked = false;
});
// this.element.reset();
+ this.element.reset();
this.submitForm();
}
diff --git a/app/services/workshop_search_service.rb b/app/services/workshop_search_service.rb
index e34c93280..2b8f7a5c8 100644
--- a/app/services/workshop_search_service.rb
+++ b/app/services/workshop_search_service.rb
@@ -29,7 +29,7 @@ def call
# Compute the effective sort
def default_sort
- params[:sort].presence || "created"
+ params[:sort].presence || "title"
# return params[:sort] if params[:sort].present?
# return 'keywords' if params[:query].present? # only when returning weighted results from # search_by_query
# 'title'
diff --git a/app/views/workshops/_filters.html.erb b/app/views/workshops/_filters.html.erb
index 9dc442319..924e40804 100644
--- a/app/views/workshops/_filters.html.erb
+++ b/app/views/workshops/_filters.html.erb
@@ -24,6 +24,13 @@
+ <%= render "dropdown_filter",
+ dom_id_prefix: "windows-types",
+ label_text: "Windows Audience",
+ items: @windows_types,
+ label_method: :short_name,
+ param_name: :windows_types %>
+
<%= render "categories_fields", category_types: @category_types %>
<%= render "dropdown_filter",
@@ -32,12 +39,5 @@
items: @sectors.reject { |sector| sector.name == "Other" },
param_name: :sectors %>
- <%= render "dropdown_filter",
- dom_id_prefix: "windows-types",
- label_text: "Windows Type",
- items: @windows_types,
- label_method: :short_name,
- param_name: :windows_types %>
-
<%= render "inactive_fields" if allowed_to?(:manage?, Workshop) %>
diff --git a/app/views/workshops/_sort_by_options.html.erb b/app/views/workshops/_sort_by_options.html.erb
index d98edb890..4a4707faa 100644
--- a/app/views/workshops/_sort_by_options.html.erb
+++ b/app/views/workshops/_sort_by_options.html.erb
@@ -1,7 +1,7 @@
diff --git a/db/migrate/20260216231506_add_not_null_constraints_to_affiliations.rb b/db/migrate/20260216231506_add_not_null_constraints_to_affiliations.rb
new file mode 100644
index 000000000..03db06f2f
--- /dev/null
+++ b/db/migrate/20260216231506_add_not_null_constraints_to_affiliations.rb
@@ -0,0 +1,28 @@
+class AddNotNullConstraintsToAffiliations < ActiveRecord::Migration[8.1]
+ def up
+ # Clean up any orphaned records before adding constraints
+ Affiliation.where(organization_id: nil).destroy_all
+ Affiliation.where(person_id: nil).destroy_all
+
+ change_column_null :affiliations, :organization_id, false
+ change_column_null :affiliations, :person_id, false
+
+ # Fix column type so foreign key can be added (people.id is int, not bigint)
+ change_column :affiliations, :person_id, :integer, null: false
+
+ unless foreign_key_exists?(:affiliations, :people, column: :person_id)
+ add_foreign_key :affiliations, :people, column: :person_id
+ end
+ end
+
+ def down
+ change_column_null :affiliations, :organization_id, true
+ change_column_null :affiliations, :person_id, true
+
+ if foreign_key_exists?(:affiliations, :people, column: :person_id)
+ remove_foreign_key :affiliations, :people, column: :person_id
+ end
+
+ change_column :affiliations, :person_id, :bigint
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 033518e80..068772f11 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2026_02_16_120927) do
+ActiveRecord::Schema[8.1].define(version: 2026_02_16_231506) do
create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.bigint "action_text_rich_text_id", null: false
t.datetime "created_at", null: false
diff --git a/spec/models/affiliation_spec.rb b/spec/models/affiliation_spec.rb
index 4f1002993..5cec19a27 100644
--- a/spec/models/affiliation_spec.rb
+++ b/spec/models/affiliation_spec.rb
@@ -11,7 +11,7 @@
build(:affiliation, organization: create(:organization), person: create(:person))
end
it { should validate_presence_of(:organization_id) }
- it { should belong_to(:person) }
+ # it { should validate_presence_of(:person_id) } # we needed to not have this to support nested attrs
end
describe 'enums' do
diff --git a/spec/services/workshop_search_service_spec.rb b/spec/services/workshop_search_service_spec.rb
index 80a9afade..cb6549b0e 100644
--- a/spec/services/workshop_search_service_spec.rb
+++ b/spec/services/workshop_search_service_spec.rb
@@ -49,6 +49,41 @@
end
end
+ context "sorting by title ignores punctuation" do
+ let!(:hyphenated) { create(:workshop, :published, title: "Self-Care Workshop") }
+ let!(:spaced) { create(:workshop, :published, title: "Self Care Zen") }
+ let!(:quoted) { create(:workshop, :published, title: '"I Am" Mandalas') }
+ let!(:numbered) { create(:workshop, :published, title: "100 New Ideas") }
+ let!(:lowercase) { create(:workshop, :published, title: "a lowercase title") }
+
+ it "sorts case-insensitively" do
+ service = WorkshopSearchService.new({ sort: "title" }, user: user).call
+ titles = service.workshops.map(&:title)
+
+ lowercase_idx = titles.index("a lowercase title")
+ b_workshop_idx = titles.index("B Workshop")
+ expect(lowercase_idx).to be < b_workshop_idx
+ end
+
+ it "sorts numbers before letters" do
+ service = WorkshopSearchService.new({ sort: "title" }, user: user).call
+ titles = service.workshops.map(&:title)
+
+ numbered_idx = titles.index("100 New Ideas")
+ quoted_idx = titles.index('"I Am" Mandalas')
+ expect(numbered_idx).to be < quoted_idx
+ end
+
+ it "sorts hyphenated and spaced titles adjacently" do
+ service = WorkshopSearchService.new({ sort: "title" }, user: user).call
+ titles = service.workshops.map(&:title)
+
+ care_idx = titles.index("Self-Care Workshop")
+ zen_idx = titles.index("Self Care Zen")
+ expect((care_idx - zen_idx).abs).to eq(1)
+ end
+ end
+
context "sorting by led" do
it "orders descending by led_count then title" do
service = WorkshopSearchService.new({ sort: 'led' }, user: user).call
@@ -125,9 +160,9 @@
expect(service.sort).to eq('keywords')
end
- it "defaults to created if no query or sort is provided" do
+ it "defaults to title if no query or sort is provided" do
service = WorkshopSearchService.new({}, user: user).call
- expect(service.sort).to eq('created')
+ expect(service.sort).to eq('title')
end
end
diff --git a/spec/system/workshops_spec.rb b/spec/system/workshops_spec.rb
index a0fbf8f93..38c76cd18 100644
--- a/spec/system/workshops_spec.rb
+++ b/spec/system/workshops_spec.rb
@@ -36,7 +36,7 @@
fill_in 'query', with: 'best workshop'
# Open the dropdown
- click_on "Windows Type" # this clicks the