diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb index e345eb5352..2367280a8d 100644 --- a/app/policies/event_policy.rb +++ b/app/policies/event_policy.rb @@ -126,6 +126,7 @@ def google_analytics? :autoshow_pre_date_text, :autoshow_registration_close, :public_registration_enabled, + :ce_credits_eligible, :signed_in_one_click_enabled, :autoshow_registration_details, :hint_dates, diff --git a/app/views/event_registrations/_form.html.erb b/app/views/event_registrations/_form.html.erb index 13a1091ad2..e3412a68fe 100644 --- a/app/views/event_registrations/_form.html.erb +++ b/app/views/event_registrations/_form.html.erb @@ -133,12 +133,21 @@ - <%# ---- Organizations, scholarship, and continuing education — one row ---- %> + <%# ---- Organizations, scholarship, and continuing education — one row. + Scholarship only applies to paid events; CE credits only to events the + organizer flagged as eligible. Organizations fills whatever the hidden + boxes leave behind: it starts at two of four columns and absorbs one + more column for each box that's hidden. ---- %> + <% paid_event = f.object.event.cost_cents.to_i.positive? %> + <% show_scholarship = paid_event %> + <% show_ce = f.object.event.ce_credits_eligible? %> + <% org_span = 2 + (show_scholarship ? 0 : 1) + (show_ce ? 0 : 1) %> + <% org_span_class = { 2 => "sm:col-span-2", 3 => "sm:col-span-3", 4 => "sm:col-span-4" }.fetch(org_span) %> <% active_orgs = f.object.registrant.affiliations.select { |a| !a.inactive? && (a.end_date.nil? || a.end_date >= Date.current) }.map(&:organization).compact.uniq.sort_by(&:name) %> <% connected_org_ids = f.object.organizations.map(&:id) %> <% addable_orgs = active_orgs.reject { |org| connected_org_ids.include?(org.id) } %>
-
+
@@ -208,12 +217,15 @@
- <%= render "scholarship", event_registration: f.object, scholarship: f.object.scholarships.first %> + <% if show_scholarship %> + <%= render "scholarship", event_registration: f.object, scholarship: f.object.scholarships.first %> + <% end %> <%# ---- CE credits — toggled "Requested" flag, plus the registrant's CE details (license number + requested hours) and what they owe at the default hourly rate, recomputed live by the ce-credit-requested controller. The details collapse when CE isn't requested. ---- %> + <% if show_ce %>
@@ -274,6 +286,7 @@
+ <% end %> <% if f.object.payment_unresolved? %> diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb index b5790dc9fe..0abe1a2e27 100644 --- a/app/views/events/_form.html.erb +++ b/app/views/events/_form.html.erb @@ -163,6 +163,13 @@

Cost <%= f.object.errors[:cost].join(", ") %>

<% end %> + +
+ <%= f.input :ce_credits_eligible, + as: :boolean, + label: "Eligible for continuing education credits", + hint: "When unchecked, the CE credits box is hidden on registrations." %> +
diff --git a/db/migrate/20260622120000_add_ce_credits_eligible_to_events.rb b/db/migrate/20260622120000_add_ce_credits_eligible_to_events.rb new file mode 100644 index 0000000000..f41e3a4fc2 --- /dev/null +++ b/db/migrate/20260622120000_add_ce_credits_eligible_to_events.rb @@ -0,0 +1,9 @@ +class AddCeCreditsEligibleToEvents < ActiveRecord::Migration[8.1] + def up + add_column :events, :ce_credits_eligible, :boolean, default: true, null: false + end + + def down + remove_column :events, :ce_credits_eligible, if_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 87e2573c42..3580eb5114 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_06_21_213947) do +ActiveRecord::Schema[8.1].define(version: 2026_06_22_120000) 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 @@ -517,6 +517,7 @@ t.boolean "autoshow_title", default: true, null: false t.boolean "autoshow_videoconference_label", default: true, null: false t.boolean "autoshow_videoconference_link", default: true, null: false + t.boolean "ce_credits_eligible", default: true, null: false t.text "ce_hours_details" t.string "ce_hours_details_label", default: "CE hours", null: false t.integer "cost_cents" diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 19903ba5d3..0bcfc7d306 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -8,6 +8,12 @@ it { should validate_numericality_of(:cost_cents).is_greater_than_or_equal_to(0).allow_nil } end + describe "ce_credits_eligible" do + it "defaults to true" do + expect(build(:event).ce_credits_eligible).to be true + end + end + describe "destroying with form submissions" do it "is blocked and keeps the submission when the event has one" do event = create(:event) diff --git a/spec/system/event_registration_edit_spec.rb b/spec/system/event_registration_edit_spec.rb index 216bca7b60..db62cdfe02 100644 --- a/spec/system/event_registration_edit_spec.rb +++ b/spec/system/event_registration_edit_spec.rb @@ -234,6 +234,47 @@ end end + describe "conditional Organizations / scholarship / CE boxes" do + it "shows scholarship and CE boxes for a paid, CE-eligible event with organizations at half width" do + sign_in(admin) + visit edit_event_registration_path(registration) + + expect(page).to have_css("h2", text: "Scholarship") + expect(page).to have_css("h2", text: "CE credits") + expect(page).to have_css("section.sm\\:col-span-2", text: "Registration organizations") + end + + it "hides the scholarship box for a free event and widens organizations" do + event.update!(cost_cents: 0) + sign_in(admin) + visit edit_event_registration_path(registration) + + expect(page).to have_no_css("h2", text: "Scholarship") + expect(page).to have_css("h2", text: "CE credits") + expect(page).to have_css("section.sm\\:col-span-3", text: "Registration organizations") + end + + it "hides the CE box when the event is not CE-eligible and widens organizations" do + event.update!(ce_credits_eligible: false) + sign_in(admin) + visit edit_event_registration_path(registration) + + expect(page).to have_css("h2", text: "Scholarship") + expect(page).to have_no_css("h2", text: "CE credits") + expect(page).to have_css("section.sm\\:col-span-3", text: "Registration organizations") + end + + it "fills the full row with organizations for a free, non-CE-eligible event" do + event.update!(cost_cents: 0, ce_credits_eligible: false) + sign_in(admin) + visit edit_event_registration_path(registration) + + expect(page).to have_no_css("h2", text: "Scholarship") + expect(page).to have_no_css("h2", text: "CE credits") + expect(page).to have_css("section.sm\\:col-span-4", text: "Registration organizations") + end + end + describe "delete button" do it "deletes the registration" do sign_in(admin)