Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 64 additions & 9 deletions app/controllers/events/public_registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ def show
return
end

@person_form = @form.person_forms.find_by(person: person)
unless @person_form
@form_submission = @form.form_submissions.find_by(person: person)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

changing name from person_forms to form_submissions

unless @form_submission
redirect_to event_path(@event), alert: "No registration form submission found."
return
end

@form_fields = @form.form_fields.where(status: :active).reorder(position: :asc)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

removing status from form_fields bc now they'll either be on the form or removed, no need for soft delete

@responses = @person_form.person_form_form_fields.index_by(&:form_field_id)
@form_fields = @form.form_fields.reorder(position: :asc)
@responses = @form_submission.form_answers.index_by(&:form_field_id)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

changing responses from person_form_form_fields to form_answers

@event = @event.decorate
end

Expand All @@ -108,15 +108,70 @@ def scholarship_mode?
end

def visible_form_fields
scope = @form.form_fields.where(status: :active)
if scholarship_mode?
scope = scope.where.not(field_group: "payment")
else
scope = @form.form_fields
unless scholarship_mode?
scope = scope.where.not(field_group: "scholarship")
end

person = current_user&.person
if person
if @form.hide_answered_person_questions?
known_keys = person_known_field_keys(person)
if known_keys.any?
known_ids = @form.form_fields
.where(field_group: %w[person_identifier person_contact_info], field_key: known_keys)
.ids
scope = scope.where.not(id: known_ids) if known_ids.any?
end

# Background fields (e.g. ethnicity) are always hidden for logged-in users
# since Person doesn't store these — they're collected once per event
scope = scope.where.not(field_group: "background")
end

if @form.hide_answered_form_questions?
existing_submission = @form.form_submissions.find_by(person: person)
if existing_submission
answered_field_ids = existing_submission.form_answers
.joins(:form_field)
.where(form_fields: { field_group: %w[professional marketing] })
.where.not(text: [ nil, "" ])
.pluck(:form_field_id)
scope = scope.where.not(id: answered_field_ids) if answered_field_ids.any?
end
end
end

scope.reorder(position: :asc)
end

def person_known_field_keys(person)
keys = []
keys << "first_name" if person.first_name.present?
keys << "last_name" if person.last_name.present?
keys << "primary_email" << "confirm_email" if person.email.present?
keys << "primary_email_type" if person.email_type.present?
keys << "nickname" if person.legal_first_name.present? || person.first_name.present?
keys << "pronouns" if person.pronouns.present?
keys << "secondary_email" if person.email_2.present?
keys << "secondary_email_type" if person.email_2_type.present?

if person.addresses.exists?
address = person.addresses.find_by(primary: true) || person.addresses.first
keys << "mailing_street" if address.street_address.present?
keys << "mailing_address_type" if address.address_type.present?
keys << "mailing_city" if address.city.present?
keys << "mailing_state" if address.state.present?
keys << "mailing_zip" if address.zip_code.present?
end

if person.contact_methods.where(kind: :phone).exists?
keys << "phone" << "phone_type"
end

keys
end

def ensure_registerable
unless @event.registerable?
redirect_to event_path(@event), alert: "Registration is closed for this event."
Expand All @@ -142,7 +197,7 @@ def validate_required_fields(form_params)

if field.number_integer? && value.to_s !~ /\A\d+\z/
errors[field.id] = "must be a whole number"
elsif field.field_key&.match?(/email(?!_type)/) && value.to_s !~ /\A[^@\s]+@[^@\s]+\z/
elsif field.field_key&.match?(/email(?!_type|_confirmation)/) && value.to_s !~ /\A[^@\s]+@[^@\s]+\z/
errors[field.id] = "must be a valid email address"
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def assign_event_forms(event)
end
end

scholarship_form = Form.standalone.find_by(name: ScholarshipApplicationFormBuilder::FORM_NAME)
scholarship_form = Form.standalone.scholarship_application.first
if scholarship_form && event.cost_cents.to_i > 0
event.event_forms.find_or_create_by!(form: scholarship_form, role: "scholarship")
elsif event.cost_cents.to_i == 0
Expand Down
129 changes: 129 additions & 0 deletions app/controllers/forms_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
class FormsController < ApplicationController
before_action :set_form, only: %i[show edit update destroy reorder_field reorder_fields edit_sections update_sections]

def index
authorize!
@forms = Form.standalone.order(:name)
end

def show
authorize! @form
@form_fields = preview_form_fields
end

def new
authorize!
end

def create
authorize!

sections = (params[:sections] || []).reject(&:blank?).map(&:to_sym)
if sections.empty?
flash.now[:alert] = "Please select at least one section."
render :new, status: :unprocessable_content
return
end

form = FormBuilderService.new(
name: params[:name].presence || "New Form",
sections: sections,
scholarship_application: params[:scholarship_application] == "1"
).call

redirect_to edit_form_path(form), notice: "Form created with #{form.form_fields.size} fields."
end

def edit
authorize! @form
@form_fields = @form.form_fields.reorder(position: :asc)
end

def update
authorize! @form

if @form.update(form_params)
redirect_to edit_form_path(@form), notice: "Form updated."
else
@form_fields = @form.form_fields.reorder(position: :asc)
render :edit, status: :unprocessable_content
end
end

def destroy
authorize! @form
@form.destroy!
redirect_to forms_path, notice: "Form deleted."
end

def edit_sections
authorize! @form
end

def update_sections
authorize! @form

sections = (params[:sections] || []).reject(&:blank?).map(&:to_sym)
if sections.empty?
flash.now[:alert] = "Please select at least one section."
render :edit_sections, status: :unprocessable_content
return
end

FormBuilderService.update_sections!(@form, sections)
redirect_to edit_form_path(@form), notice: "Sections updated."
end

def reorder_field
authorize! @form
field = @form.form_fields.find(params[:field_id])
field.update!(position: params[:position].to_i)
head :ok
end

def reorder_fields
authorize! @form
positions = JSON.parse(request.body.read)["positions"] || []
positions.each do |item|
@form.form_fields.where(id: item["id"]).update_all(position: item["position"])
end
head :ok
end

private

def set_form
@form = Form.find(params[:id])
end

def preview_form_fields
scope = @form.form_fields
form_sections = (@form.sections || []).map(&:to_s)

if params[:preview_scholarship].present?
# Scholarship mode: show scholarship fields (and keep payment visible)
elsif form_sections.include?("scholarship")
scope = scope.where.not(field_group: "scholarship")
end

if params[:preview_logged_in].present? && @form.hide_answered_person_questions?
scope = scope.where.not(field_group: %w[person_identifier person_contact_info background])
end

if params[:preview_answered].present? && @form.hide_answered_form_questions?
scope = scope.where.not(field_group: %w[professional marketing])
end

scope.reorder(position: :asc)
end

def form_params
params.require(:form).permit(
:name, :hide_answered_person_questions, :hide_answered_form_questions,
form_fields_attributes: [
:id, :question, :answer_type, :is_required, :instructional_hint,
:field_key, :field_group, :position, :_destroy
]
)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Controller } from "@hotwired/stimulus"
import { put } from "@rails/request.js"
import Sortable from "sortablejs"

/**
* Sortable controller for form field editing.
*
* Group-aware: dragging a group header also moves all fields
* that share the same field_group.
*
* Usage:
* data-controller="form-fields-sortable"
* data-form-fields-sortable-url-value="/forms/:id/reorder_fields"
*
* Each item needs:
* data-sortable-id="<field_id>"
* data-sortable-handle (on the drag handle element)
* data-field-group="<group_name>" (optional)
* data-group-header (on header rows)
*/
export default class extends Controller {
static values = { url: String }

connect() {
this.sortable = Sortable.create(this.element, {
onEnd: this.onEnd.bind(this),
handle: "[data-sortable-handle]",
})
}

disconnect() {
this.sortable.destroy()
}

onEnd(event) {
const { item } = event

// If a group header was moved, relocate its group members to follow it
if (item.dataset.groupHeader !== undefined) {
const group = item.dataset.fieldGroup
const members = []

for (const el of this.element.children) {
if (el !== item && el.dataset.fieldGroup === group && el.dataset.groupHeader === undefined) {
members.push(el)
}
}

let ref = item
for (const member of members) {
ref.after(member)
ref = member
}
}

// Update all positions based on current DOM order
const items = [...this.element.children]
const positions = []

items.forEach((el, index) => {
const pos = index + 1
positions.push({ id: parseInt(el.dataset.sortableId), position: pos })

// Update hidden position field so form submit stays in sync
const posInput = el.querySelector("input[name*='[position]']")
if (posInput) posInput.value = pos
})

put(this.urlValue, {
body: JSON.stringify({ positions }),
})
}
}
2 changes: 1 addition & 1 deletion app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def public_registration_just_enabled?
def build_public_registration_form
return if event_forms.registration.exists?

form_name = title&.match?(/training/i) ? ExtendedEventRegistrationFormBuilder::FORM_NAME : ShortEventRegistrationFormBuilder::FORM_NAME
form_name = title&.match?(/training/i) ? "Extended Event Registration" : "Short Event Registration"
form = Form.standalone.find_by(name: form_name)
return unless form

Expand Down
2 changes: 1 addition & 1 deletion app/models/form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class Form < ApplicationRecord
has_many :form_fields, dependent: :destroy, inverse_of: :form
has_many :event_forms, dependent: :destroy
has_many :user_forms
has_many :person_forms
has_many :form_submissions
has_many :reports, as: :owner
# has_many through
has_many :events, through: :event_forms
Expand Down
8 changes: 8 additions & 0 deletions app/models/form_answer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class FormAnswer < ApplicationRecord
belongs_to :form_field, optional: true
belongs_to :form_submission

def name
"#{question_text.presence || form_field&.question}: #{text}"
end
end
7 changes: 7 additions & 0 deletions app/models/form_submission.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class FormSubmission < ApplicationRecord
belongs_to :person
belongs_to :form
has_many :form_answers, dependent: :destroy

accepts_nested_attributes_for :form_answers
end
2 changes: 1 addition & 1 deletion app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Person < ApplicationRecord
has_many :events, through: :event_registrations
has_many :categories, through: :categorizable_items
has_many :sectors, through: :sectorable_items
has_many :person_forms, dependent: :destroy
has_many :form_submissions, dependent: :destroy

# Asset associations
has_one_attached :avatar, dependent: :purge do |attachable|
Expand Down
7 changes: 0 additions & 7 deletions app/models/person_form.rb

This file was deleted.

8 changes: 0 additions & 8 deletions app/models/person_form_form_field.rb

This file was deleted.

3 changes: 3 additions & 0 deletions app/policies/form_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class FormPolicy < ApplicationPolicy
# Admin-only — all CRUD actions inherit manage? from ApplicationPolicy
end
Loading