-
Notifications
You must be signed in to change notification settings - Fork 27
Public pretty-URL endpoint for standalone forms + form-filterable submissions index #2237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
998733b
Add slug + published to Form; extract FormAnswerPersistence for publiβ¦
maebeale c1fea1b
Public pretty-URL endpoint for standalone forms + form-filterable subβ¦
maebeale 0741266
Refresh forms_controller brakeman ignore for :slug/:published permit
maebeale 1a75ee7
Trim comments to non-obvious whys per CLAUDE.md
maebeale a3d9b90
Seed standalone public forms + designate public (event-less) submissiβ¦
maebeale cabde8b
Fix View link escaping the results frame; consolidate answer persisteβ¦
maebeale 53239e6
Add FormSubmission#persist_answer model spec (text, multi-value, upseβ¦
maebeale 40221e1
Email confirmation + admin FYI on public form submission
maebeale 7666f17
Forms index: show public-link and event-form chips independently
maebeale 2a40dec
Align submissions filter with the shared search-bar helpers/partials
maebeale d62c2f3
Capture "Other" answers and keep the form filter on public-form submiβ¦
maebeale 6ec8085
Reject a slug that parameterizes away instead of blanking it
maebeale e783631
Keep the Forms eyebrow across the submission detail round trip
maebeale c8fe97c
Refresh the drifted controller and service counts in AGENTS.md
maebeale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # Public, account-free pretty-URL endpoint for a standalone, published form | ||
| # (/f/:slug). Reuses the public-registration field partials, so answers arrive | ||
| # under the shared `public_registration[form_fields]` param namespace. | ||
| class PublicFormsController < ApplicationController | ||
| skip_before_action :authenticate_user!, only: %i[show create thank_you] | ||
| before_action :set_form | ||
|
|
||
| def show | ||
| authorize! @form, to: :public_show? | ||
| @form_fields = ordered_fields | ||
| end | ||
|
|
||
| def create | ||
| authorize! @form, to: :public_show? | ||
|
|
||
| # Honeypot β a bot that fills the hidden field is silently bounced. | ||
| if params.dig(:public_registration, :website_url).present? | ||
| redirect_to public_form_path(@form.slug) | ||
| return | ||
| end | ||
|
|
||
| @form_fields = ordered_fields | ||
| form_params = merge_retained_uploads(params.dig(:public_registration, :form_fields)&.to_unsafe_h || {}) | ||
|
|
||
| @field_errors = validate_required_fields(form_params) | ||
| if @field_errors.any? | ||
| flash.now[:alert] = "Your submission is not complete yet. Scroll down to check for any errors or missing information." | ||
| render :show, status: :unprocessable_content | ||
| return | ||
| end | ||
|
|
||
| Current.source = "public_form" | ||
| result = PublicFormSubmission.call(form: @form, form_params: form_params) | ||
|
|
||
| if result.success? | ||
| redirect_to thank_you_public_form_path(@form.slug), notice: "Thank you β your response has been submitted!" | ||
| else | ||
| flash.now[:alert] = result.errors.join(", ").presence || "Something went wrong. Please try again." | ||
| render :show, status: :unprocessable_content | ||
| end | ||
| end | ||
|
|
||
| def thank_you | ||
| authorize! @form, to: :public_show? | ||
| end | ||
|
|
||
| private | ||
|
|
||
| # Scoped so a draft, an event form, or an unknown slug 404s. | ||
| def set_form | ||
| @form = Form.standalone.published.find_by!(slug: params[:slug]) | ||
| end | ||
|
|
||
| def ordered_fields | ||
| @form.form_fields.reorder(position: :asc) | ||
| end | ||
|
|
||
| # A file input can't be repopulated, so on re-render after an error fall back to | ||
| # the already-uploaded blob's signed id (carried in retained_uploads). | ||
| def merge_retained_uploads(form_params) | ||
| retained = params.dig(:public_registration, :retained_uploads)&.to_unsafe_h || {} | ||
| return form_params if retained.blank? | ||
|
|
||
| retained.each do |field_id, signed_id| | ||
| next if signed_id.blank? || form_params[field_id].present? | ||
|
|
||
| form_params[field_id] = signed_id | ||
| end | ||
| form_params | ||
| end | ||
|
|
||
| def validate_required_fields(form_params) | ||
| fields = @form_fields.reject(&:group_header?) | ||
| errors = FormAnswerValidator.call(fields, form_params) | ||
|
|
||
| fields_by_identifier = fields.select { |f| f.field_identifier.present? }.index_by(&:field_identifier) | ||
| confirm_field = fields_by_identifier["confirm_email"] | ||
| email_field = fields_by_identifier["primary_email"] | ||
| if confirm_field && email_field && errors[confirm_field.id].nil? | ||
| confirm_value = form_params[confirm_field.id.to_s].to_s.strip | ||
| email_value = form_params[email_field.id.to_s].to_s.strip | ||
| errors[confirm_field.id] = "must match email" if confirm_value.present? && confirm_value != email_value | ||
| end | ||
|
|
||
| errors | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,8 @@ | ||
| class FormPolicy < ApplicationPolicy | ||
| # Admin-only β all CRUD actions inherit manage? from ApplicationPolicy | ||
|
|
||
| # The public /f/:slug form β open to anyone, but only a published standalone form. | ||
| def public_show? | ||
| record.publicly_fillable? | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.