diff --git a/app/controllers/events/bulk_payment_form_submissions_controller.rb b/app/controllers/events/bulk_payment_form_submissions_controller.rb
index 80babbcd38..efc7163af7 100644
--- a/app/controllers/events/bulk_payment_form_submissions_controller.rb
+++ b/app/controllers/events/bulk_payment_form_submissions_controller.rb
@@ -2,6 +2,7 @@ module Events
class BulkPaymentFormSubmissionsController < ApplicationController
skip_before_action :authenticate_user!, only: [ :new, :create, :show, :ticket, :resend_confirmation ]
before_action :set_event, only: [ :new, :create, :show ]
+ before_action :ensure_public_or_authenticated, only: [ :new, :create ]
before_action :set_form, only: [ :new, :create ]
def new
@@ -118,6 +119,14 @@ def set_form
end
end
+ # Anonymous visitors may only reach the public bulk-payment form when public
+ # registration is enabled; signed-in users (and admins) always may. Mirrors
+ # Events::PublicRegistrationPolicy for the registration/scholarship forms.
+ def ensure_public_or_authenticated
+ return if current_user.present? || @event.public_registration_enabled?
+ redirect_to event_path(@event), alert: "#{Form::BULK_PAYMENT_PUBLIC_NAME} form is not open for public submissions."
+ end
+
def validate_required_fields
# The nested attendees field is validated separately, so exclude it here.
fields = visible_form_fields.reject { |field| field.field_identifier == "bulk_payment_attendees" }
diff --git a/app/controllers/events/public_registrations_controller.rb b/app/controllers/events/public_registrations_controller.rb
index 5954a41e13..35613462d9 100644
--- a/app/controllers/events/public_registrations_controller.rb
+++ b/app/controllers/events/public_registrations_controller.rb
@@ -5,7 +5,7 @@ class PublicRegistrationsController < ApplicationController
before_action :ensure_registerable, only: [ :new, :create ]
def new
- authorize! :public_registration, to: :new?
+ authorize! @event, to: :new?, with: Events::PublicRegistrationPolicy
@registration_form = registration_form
unless @registration_form
@@ -21,7 +21,7 @@ def new
end
def create
- authorize! :public_registration, to: :create?
+ authorize! @event, to: :create?, with: Events::PublicRegistrationPolicy
if params[:public_registration][:website_url].present?
redirect_to new_event_public_registration_path(@event)
diff --git a/app/policies/events/public_registration_policy.rb b/app/policies/events/public_registration_policy.rb
index d00c405190..ba85ebff2c 100644
--- a/app/policies/events/public_registration_policy.rb
+++ b/app/policies/events/public_registration_policy.rb
@@ -1,10 +1,15 @@
class Events::PublicRegistrationPolicy < ApplicationPolicy
+ # Anonymous visitors may only reach the public registration/scholarship form
+ # when public registration is enabled for the event. Signed-in users (and
+ # admins, who are always signed in) may always reach it — mirroring the
+ # register button, which shows for any signed-in user on a registerable event.
+ # `record` is the event (authorized via `authorize! @event, with: self`).
def new?
- true
+ admin? || record.public_registration_enabled? || authenticated?
end
def create?
- true
+ new?
end
def show?
diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb
index 7cb00627d4..5280689941 100644
--- a/app/views/events/_form.html.erb
+++ b/app/views/events/_form.html.erb
@@ -352,7 +352,7 @@
-
+
Add an at-a-glance list of dates, time, platform/location, fee, and deadline above the registration form. Pulled from this event's details.
+ <%# Forms dropdown — sits top-right below the sub-nav, holding the public form
+ links, the sample-ticket preview, and "Edit form settings" (the Edit-event
+ form settings section). %>
+ <% if allowed_to?(:edit?, @event) %>
+
- <%# Quick links: registration forms and bulk payment. Each form link is gated by
- its event setting — registration on an associated registration form, scholarship
- on that form plus the "Enable scholarship application" checkbox (which is what
- creates the scholarship event_form). The scholarship link points at the public
- registration page, so it also needs the registration form or it dead-ends there.
- Bulk payment is gated on the event having a bulk payment form.
- Links open in a new tab. %>
- <% link_base = "inline-flex items-center gap-2 rounded-lg border bg-white px-3 py-2 text-sm font-medium shadow-sm transition-colors" %>
-
- <% if @event.event_forms.registration.exists? %>
- <%= link_to new_event_public_registration_path(@event), target: "_blank", rel: "noopener noreferrer",
- class: "#{link_base} #{DomainTheme.border_class_for(:events, intensity: 200)} text-gray-700 hover:#{DomainTheme.border_class_for(:events, intensity: 300)} hover:bg-gray-50" do %>
-
- Public registration form
-
- <% end %>
- <% end %>
-
- <% if @event.event_forms.registration.exists? && @event.scholarship_form %>
- <%= link_to new_event_public_registration_path(@event, scholarship_requested: true), target: "_blank", rel: "noopener noreferrer",
- class: "#{link_base} #{DomainTheme.border_class_for(:scholarships, intensity: 200)} text-gray-700 hover:#{DomainTheme.border_class_for(:scholarships, intensity: 300)} hover:bg-gray-50" do %>
-
- Scholarship form
-
- <% end %>
- <% end %>
-
- <% if @event.bulk_payment_form %>
- <%= link_to new_event_bulk_payment_path(@event), target: "_blank", rel: "noopener noreferrer",
- class: "#{link_base} #{DomainTheme.border_class_for(:payments, intensity: 200)} text-gray-700 hover:#{DomainTheme.border_class_for(:payments, intensity: 300)} hover:bg-gray-50" do %>
-
- Bulk payment form
-
- <% end %>
- <% end %>
-
-
<%# Money cards (paid events only) — shown first %>
<% if @dashboard.free? %>
diff --git a/app/views/events/sample_ticket.html.erb b/app/views/events/sample_ticket.html.erb
index 740d91f4cb..b0a2015590 100644
--- a/app/views/events/sample_ticket.html.erb
+++ b/app/views/events/sample_ticket.html.erb
@@ -1,7 +1,7 @@
<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
-<%# Eyebrow returns the admin to wherever they opened the preview from. The
- Form actions menu lives on the registrants page; the dashboard has its own
- quick link. Default to the dashboard when the origin is absent/unknown. %>
+<%# Eyebrow returns the admin to wherever they opened the preview from — the
+ Forms menu (shared by the dashboard and registrants page) passes the origin
+ via return_to. Default to the dashboard when the origin is absent/unknown. %>
<% case params[:return_to]
when "registrants" %>
<% back_label = "← Registrants" %>
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 24ee680315..54ea19338c 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -25,6 +25,10 @@
publicly_visible { true }
end
+ trait :publicly_registerable do
+ public_registration_enabled { true }
+ end
+
trait :publicly_featured do
publicly_featured { true }
end
diff --git a/spec/requests/events/bulk_payment_form_submissions_spec.rb b/spec/requests/events/bulk_payment_form_submissions_spec.rb
index abc0d38691..a1dabd080d 100644
--- a/spec/requests/events/bulk_payment_form_submissions_spec.rb
+++ b/spec/requests/events/bulk_payment_form_submissions_spec.rb
@@ -77,6 +77,38 @@ def post_bulk_payment(answer)
end
end
+ # Anonymous visitors may only reach the public bulk-payment form when public
+ # registration is enabled; signed-in users and admins always may. (The parent
+ # `before` signs in an admin, so sign out to exercise the anonymous path.)
+ describe "access control by public registration setting" do
+ it "redirects an anonymous visitor when public registration is disabled" do
+ sign_out admin
+ get new_event_bulk_payment_path(event)
+ expect(response).to redirect_to(event_path(event))
+ end
+
+ it "rejects an anonymous create when public registration is disabled" do
+ sign_out admin
+ post_bulk_payment("this answer easily has plenty of words")
+ expect(response).to redirect_to(event_path(event))
+ end
+
+ it "allows an anonymous visitor when public registration is enabled" do
+ sign_out admin
+ public_event = create(:event, :publicly_registerable, cost_cents: 0)
+ EventForm.create!(event: public_event, form: create(:form), role: "bulk_payment")
+
+ get new_event_bulk_payment_path(public_event)
+
+ expect(response).to have_http_status(:ok)
+ end
+
+ it "allows a signed-in admin when public registration is disabled" do
+ get new_event_bulk_payment_path(event)
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
describe "POST create with credit card payment" do
let(:admin) { create(:user, :admin, :with_person) }
let(:event) { create(:event, cost_cents: 15_00) }
@@ -133,6 +165,8 @@ def payer_params
end
describe "GET new with the seeded bulk payment form" do
+ # Public registration must be on for the signed-out (anonymous) view.
+ let(:event) { create(:event, :publicly_registerable, cost_cents: 0) }
let(:seeded_form) do
FormBuilderService.new(name: "Bulk Payment", sections: %i[bulk_payment], role: "bulk_payment").call
end
diff --git a/spec/requests/events/professional_field_identifiers_spec.rb b/spec/requests/events/professional_field_identifiers_spec.rb
index 47dab03b15..895fef25c7 100644
--- a/spec/requests/events/professional_field_identifiers_spec.rb
+++ b/spec/requests/events/professional_field_identifiers_spec.rb
@@ -14,7 +14,7 @@
# and form-submission show pages.
RSpec.describe "Events::PublicRegistrations professional fields", type: :request do
let(:admin) { create(:user, :admin) }
- let(:event) { create(:event, cost_cents: 0) }
+ let(:event) { create(:event, :publicly_registerable, cost_cents: 0) }
# An AgeRange type (profile-specific so the person edit form lists it) with the
# published ranges plus an unpublished range that must never be offered.
diff --git a/spec/requests/events/public_registrations_spec.rb b/spec/requests/events/public_registrations_spec.rb
index d14590847f..bd71b75d96 100644
--- a/spec/requests/events/public_registrations_spec.rb
+++ b/spec/requests/events/public_registrations_spec.rb
@@ -3,7 +3,7 @@
RSpec.describe "Events::PublicRegistrations", type: :request do
# A guest registering on a free event so we exercise the bare create path
# without payment or auth.
- let(:event) { create(:event, cost_cents: 0) }
+ let(:event) { create(:event, :publicly_registerable, cost_cents: 0) }
let(:form) { create(:form) }
let!(:essay_field) do
create(:form_field, form: form, answer_type: :free_form_input_paragraph,
@@ -17,6 +17,51 @@ def post_registration(answer)
params: { public_registration: { form_fields: { essay_field.id.to_s => answer } } }
end
+ # Anonymous visitors may only reach the public registration/scholarship form
+ # when public registration is enabled; signed-in users and admins always may.
+ describe "access control by public registration setting" do
+ let(:admin) { create(:user, :admin) }
+ let(:visitor) { create(:user, :with_person) }
+
+ context "when public registration is disabled" do
+ let(:event) { create(:event, cost_cents: 0) }
+
+ it "redirects an anonymous visitor from the registration form" do
+ get new_event_public_registration_path(event)
+ expect(response).to redirect_to(root_path)
+ end
+
+ it "redirects an anonymous visitor from the scholarship form" do
+ get new_event_public_registration_path(event, scholarship_requested: true)
+ expect(response).to redirect_to(root_path)
+ end
+
+ it "rejects an anonymous create" do
+ post_registration("this answer easily has plenty of words")
+ expect(response).to redirect_to(root_path)
+ end
+
+ it "allows a signed-in user" do
+ sign_in visitor
+ get new_event_public_registration_path(event)
+ expect(response).to have_http_status(:ok)
+ end
+
+ it "allows an admin" do
+ sign_in admin
+ get new_event_public_registration_path(event)
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ context "when public registration is enabled" do
+ it "allows an anonymous visitor to the registration form" do
+ get new_event_public_registration_path(event)
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+
describe "POST create with a minimum word count" do
it "rejects an answer with too few words" do
expect {
@@ -570,7 +615,7 @@ def post_with_scholarship(scholarship_answer)
describe "GET new payment method options" do
# A paid event so the payment section is not stripped from the form.
- let(:event) { create(:event, cost_cents: 150_00) }
+ let(:event) { create(:event, :publicly_registerable, cost_cents: 150_00) }
let!(:payment_method_field) do
field = create(:form_field, form: form, answer_type: :single_select_radio,
field_identifier: "payment_method", name: "Payment method",
diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb
index 6d17e0e883..5a274156f0 100644
--- a/spec/requests/events_spec.rb
+++ b/spec/requests/events_spec.rb
@@ -1613,6 +1613,100 @@ def ce_chip_text
expect(response.body).to include("Bulk payments")
expect(response.body).to include(bulk_payments_event_path(event))
end
+
+ # The Forms dropdown (app/views/events/_form_actions_menu.html.erb) shows a
+ # link per public-facing form, gated by the event's form settings, and
+ # prefixes each label with "Public" when public registration is enabled.
+ describe "Forms dropdown" do
+ it "renders the menu with an Edit form settings link to the edit form-settings section" do
+ create(:event_form, event: event, form: create(:form), role: "registration")
+
+ get dashboard_event_path(event)
+
+ expect(response.body).to include("Forms")
+ expect(response.body).to include("Edit form settings")
+ expect(response.body).to include(edit_event_path(event, anchor: "registration_form_section"))
+ end
+
+ context "registration link" do
+ it "is hidden when no registration form is selected" do
+ get dashboard_event_path(event)
+ expect(response.body).not_to include("Registration form")
+ expect(response.body).not_to include("Public registration form")
+ end
+
+ it "shows 'Registration form' when a form is selected and public registration is off" do
+ create(:event_form, event: event, form: create(:form), role: "registration")
+
+ get dashboard_event_path(event)
+
+ expect(response.body).to include("Registration form")
+ expect(response.body).not_to include("Public registration form")
+ end
+
+ it "shows 'Public registration form' when public registration is enabled" do
+ create(:event_form, event: event, form: create(:form), role: "registration")
+ event.update!(public_registration_enabled: true)
+
+ get dashboard_event_path(event)
+
+ expect(response.body).to include("Public registration form")
+ end
+
+ it "is hidden when one-click is on and public registration is off (nobody uses the form page)" do
+ create(:event_form, event: event, form: create(:form), role: "registration")
+ event.update!(signed_in_one_click_enabled: true)
+
+ get dashboard_event_path(event)
+
+ expect(response.body).not_to include("Registration form")
+ end
+
+ it "is shown when one-click is on but public registration is enabled (anonymous still use it)" do
+ create(:event_form, event: event, form: create(:form), role: "registration")
+ event.update!(signed_in_one_click_enabled: true, public_registration_enabled: true)
+
+ get dashboard_event_path(event)
+
+ expect(response.body).to include("Public registration form")
+ end
+ end
+
+ context "scholarship and bulk payment links" do
+ before do
+ create(:event_form, event: event, form: create(:form), role: "scholarship")
+ create(:event_form, event: event, form: create(:form), role: "bulk_payment")
+ end
+
+ it "shows them on a paid event" do
+ get dashboard_event_path(event)
+
+ expect(response.body).to include("Scholarship form")
+ expect(response.body).to include("Bulk payment form")
+ end
+
+ it "hides them on a free event" do
+ free_event = create(:event, cost_cents: 0)
+ create(:event_form, event: free_event, form: create(:form), role: "scholarship")
+ create(:event_form, event: free_event, form: create(:form), role: "bulk_payment")
+
+ get dashboard_event_path(free_event)
+
+ expect(response.body).not_to include("Scholarship form")
+ expect(response.body).not_to include("Bulk payment form")
+ end
+
+ it "prefixes them with Public when public registration is enabled" do
+ create(:event_form, event: event, form: create(:form), role: "registration")
+ event.update!(public_registration_enabled: true)
+
+ get dashboard_event_path(event)
+
+ expect(response.body).to include("Public scholarship form")
+ expect(response.body).to include("Public bulk payment form")
+ end
+ end
+ end
end
context "as non-admin non-owner" do
diff --git a/spec/system/public_registration_form_submission_spec.rb b/spec/system/public_registration_form_submission_spec.rb
index 381f0e2be0..a2e8d33083 100644
--- a/spec/system/public_registration_form_submission_spec.rb
+++ b/spec/system/public_registration_form_submission_spec.rb
@@ -8,7 +8,7 @@
# questions the dev seeds attach, so the DOM matches what registrants really see.
RSpec.describe "Public form submissions", type: :system do
let(:event) do
- create(:event, :published, :publicly_visible,
+ create(:event, :published, :publicly_visible, :publicly_registerable,
title: "AWBW Facilitator Training",
cost_cents: 150_000,
start_date: 10.days.from_now.change(hour: 9),
diff --git a/spec/system/public_registration_new_spec.rb b/spec/system/public_registration_new_spec.rb
index ddee63b7ed..0224371d27 100644
--- a/spec/system/public_registration_new_spec.rb
+++ b/spec/system/public_registration_new_spec.rb
@@ -6,6 +6,7 @@
:event,
:published,
:publicly_visible,
+ :publicly_registerable,
title: "My Event",
start_date: 2.days.from_now.change(hour: 10),
end_date: 2.days.from_now.change(hour: 12)