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.

diff --git a/app/views/events/_form_actions_menu.html.erb b/app/views/events/_form_actions_menu.html.erb index 9de3fe0e61..074eed3195 100644 --- a/app/views/events/_form_actions_menu.html.erb +++ b/app/views/events/_form_actions_menu.html.erb @@ -1,3 +1,6 @@ +<% button_label = local_assigns.fetch(:button_label, "Forms") %> +<% manage_label = local_assigns.fetch(:manage_label, "Edit form settings") %> +<% sample_return_to = local_assigns.fetch(:sample_return_to, "registrants") %> <% dropdown_id = "form-actions-menu-#{SecureRandom.hex(4)}" %> <% item_class = "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50" %>
@@ -5,29 +8,45 @@ data-action="dropdown#toggle" data-dropdown-payload-param='[{"<%= dropdown_id %>":"hidden"}]' class="btn btn-utility-outline"> - Form actions + <%= button_label %>
diff --git a/app/views/events/dashboard.html.erb b/app/views/events/dashboard.html.erb index 553935f130..9a979df633 100644 --- a/app/views/events/dashboard.html.erb +++ b/app/views/events/dashboard.html.erb @@ -6,6 +6,15 @@ <%= render "events/subnav", event: @event, current: :dashboard %>
+ <%# 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) %> +
+ <%= render "form_actions_menu", sample_return_to: "dashboard" %> +
+ <% end %> + <%# Centered title block %>
<%= link_to @event.decorate.compact_label, edit_event_path(@event), title: @event.title, class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %> @@ -20,43 +29,6 @@
- <%# 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)