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) } %>
-
+
- <%= 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 %>
+ <% 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)