Skip to content

Public pretty-URL endpoint for standalone forms + form-filterable submissions index - #2237

Merged
maebeale merged 14 commits into
mainfrom
maebeale/public-form-endpoint
Aug 18, 2026
Merged

Public pretty-URL endpoint for standalone forms + form-filterable submissions index#2237
maebeale merged 14 commits into
mainfrom
maebeale/public-form-endpoint

Conversation

@maebeale

@maebeale maebeale commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 new public unauthenticated endpoint that find-or-creates People + FormSubmissions and sends email; consolidates the answer-persistence used by event registration onto the model

What

  • Publish a standalone (event-less) Form at a public pretty URL (/f/:slug) — anyone can fill it out, no account.
  • View those submissions via the existing /form_submissions index, now filterable by form and lazy-loaded.
  • Email a confirmation to the submitter and an FYI to the AWBW team on every public submission.

Public form endpoint

  • Form gains slug (admin-set, URL-normalized, unique) + published; publicly_fillable? (standalone + published + slug) gates the route and FormPolicy#public_show?.
  • PublicFormsController (show/create/thank_you, unauthenticated) with honeypot + required-field validation; reuses the public-registration field partials.
  • PublicFormSubmission service: find-or-creates a Person from the form's name/email answers (email + last-name match reuses an existing person), records one-time consent, stores a role: "public" submission, captures "Other" answers, sends notifications.
  • The slug column never holds "". Truly blank input becomes nil so the unique index tolerates the many forms without a slug; input that parameterize strips to nothing ("!!!") is left intact for the format validation to reject, rather than silently blanked into a later "has already been taken" collision.

Emails

  • New notification kinds form_submission_confirmation (to submitter) + form_submission_confirmation_fyi (to admin), with NotificationMailer methods, html/text views, job dispatch, and previews — mirroring the event-registration confirmation/FYI pattern.

Answer persistence (consolidation)

  • Moved persist_answer + the hardened file-upload path onto FormSubmission#persist_answer. Event registration, public forms, and bulk payment share it; the interim mixin is gone.

Submissions index

  • Branches on the Turbo frame, filters by form_id, paginates, links from the forms index.
  • Event-less public submissions show a "Public form" pill.
  • Fix: the View link now breaks out of the results frame (turbo_frame: "_top") — previously it navigated the frame to a frameless show page and errored.
  • Eyebrow: arriving from the forms index shows a ← Forms back link anchored to that form's row.
  • Fix: the View link carries form_id, so the trip back keeps the form filter instead of dropping to that person's submissions.
  • Fix: the View link also carries the index's own origin, which the detail page hands back — so the ← Forms eyebrow survives the full Forms → submissions → detail → back round trip instead of vanishing on the return leg. The filter form carries it too, so changing the filter doesn't strip it from the frame's params.

Forms index / editor polish

  • Event-connected unpublished forms read "Event form", not "Not published".
  • Delete link removed from the index; on the editor, Save/Delete placement swapped and a reason shown when a form can't be deleted.
  • Editor's Public form card clarifies publish only controls the public link.

Dev seed

  • db/seeds/dev/public_forms.rb — two published event-less forms with submissions + answers, wired into db:seed:dev. Idempotent.

Open question

  • A form can be both an event form and publicly published (standalone + published + slug, regardless of event links). Currently allowed; a guard could restrict publishing to role-less forms if desired.

Follow-up

  • Rate limiting for the public POST endpoints is deliberately out of scope — tracked in Rate-limit the public, unauthenticated write endpoints #2252. /f/:slug is always open, unlike the event endpoints behind ensure_registerable; the honeypot is the only current defence.
  • Bulk payment: unhandled UnreadableUpload can 500 the public payment form #2254 — moving persist_answer onto the model gave the bulk-payment path a raise it couldn't produce before. BulkPayment#call rescues only RecordInvalid, so a file_upload field with a forged or stale signed id would 500 rather than re-render. Reachability is narrow today (the bulk-payment view renders no file input), hence a follow-up rather than an inline fix.

Tests

  • New: public_forms request, PublicFormSubmission service (incl. notifications + "Other" capture), Form slug/publish + slug-rejection, FormSubmission#persist_answer, FormPolicy#public_show?, NotificationMailer form-submission emails, forms-index public-link column, submissions eyebrow + frame-escape + form-filter + origin round trip.
  • Registration + bulk-payment + notification-job specs pass unchanged.
  • Full suite green: 6958 examples, 0 failures.

Copilot AI lite review requested due to automatic review settings August 17, 2026 01:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 17, 2026 01:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 17, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale requested a review from jmilljr24 August 17, 2026 13:46
@maebeale

maebeale commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

@jmilljr24 they need public forms for collaboration agreement (for reups outside events), annual survey. and could also use them for the immediate need of post-event survey, day-1-survey, day-2-survey until we get those wired up into event edit selections. any concerns w approach taken in this pr w the slug and published boolean?

Copilot AI review requested due to automatic review settings August 17, 2026 13:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 17, 2026 14:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 17, 2026 14:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 17, 2026 14:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 17, 2026 14:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale marked this pull request as ready for review August 17, 2026 14:56
maebeale and others added 10 commits August 18, 2026 07:38
…c forms

Foundation for a public pretty-URL endpoint for standalone (event-less) forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…missions index

Publish an event-less Form at /f/:slug for account-free public filling; view
those submissions via the form-filterable /form_submissions index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ons in index

Adds a dev seed (two published event-less forms with submissions/answers) and
marks event-less public submissions with a 'Public form' pill in the index so a
blank Event column reads as intentional, not missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nce onto FormSubmission; forms UX polish

- Bug: View link inside the submissions results Turbo frame navigated the frame
  to a frameless show page (Oopsie); break it out with turbo_frame _top.
- Move persist_answer + hardened file-upload handling onto FormSubmission;
  registration, public forms, and bulk payment now share it (mixin removed).
- Forms index: event-connected unpublished forms read 'Event form' not
  'Not published'; delete link removed; submissions count links back with a
  Forms eyebrow anchored to the row.
- Editor: clarify publish only controls the public link; swap Save/Delete
  placement and show a reason when a form can't be deleted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rt, forged upload)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror event registration: new form_submission_confirmation(_fyi) notification
kinds, NotificationMailer methods + views, job dispatch, and previews. Sent from
PublicFormSubmission to the submitter and the AWBW team.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A form can be both; render a chip for each that applies (rename column to
Availability) rather than one-or-the-other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt SearchFormHelper field/label classes + shared/_search_clear (introduced on
main in #2220) so the form filter matches every other index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/public-form-endpoint branch from d48f616 to 2a40dec Compare August 18, 2026 11:43
Copilot AI review requested due to automatic review settings August 18, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

authorize! @form, to: :public_show?

# Honeypot — a bot that fills the hidden field is silently bounced.
if params.dig(:public_registration, :website_url).present?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea! (and perfect comment).

Any other key we could use instead of website_url? That's a real value we use in other places and could be confusing or conflict at some point.

…ssions

Public forms were the only submission path that skipped
OtherResponses::CaptureFromSubmission, so a sector question answered
"Other: …" never reached the curation queue — it only lived in the form
answer, invisible to promotion.

The submissions View link carried person_id but not form_id, so arriving
from the forms index and clicking back landed on that person's
submissions rather than the form-filtered list you came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 12:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

maebeale and others added 2 commits August 18, 2026 10:16
parameterize strips "!!!" (or any input with no url-safe characters) down to
"", which the format validation waves through as blank and the unique index
then collides on for the second such form — surfacing to the admin as the
baffling "Slug has already been taken". Keep the typed value intact so the
format error fires instead, and the admin sees what they entered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The View link told the detail page it came from the submissions index, but
not where that index was itself reached from — so returning from a submission
opened via Forms landed on the filtered index with the "← Forms" back link
gone. Carry the origin forward on the link and hand it back on the way home.
The filter form carries it too, so changing the filter doesn't strip it out
of the frame's params and orphan the View links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 14:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch adds a controller and a service, and both directory counts were
already well behind the tree — the numbers are only useful if they're close.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale merged commit dcf6cc4 into main Aug 18, 2026
3 checks passed
@maebeale
maebeale deleted the maebeale/public-form-endpoint branch August 18, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants