diff --git a/app/controllers/community_news_controller.rb b/app/controllers/community_news_controller.rb index ac5792d9c..11012c7b0 100644 --- a/app/controllers/community_news_controller.rb +++ b/app/controllers/community_news_controller.rb @@ -71,7 +71,7 @@ def create end success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Community news create failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end @@ -95,7 +95,7 @@ def update assign_associations(@community_news) success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Community news update failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb index 20bfb6a9a..144178bd7 100644 --- a/app/controllers/event_registrations_controller.rb +++ b/app/controllers/event_registrations_controller.rb @@ -56,27 +56,15 @@ def create if @event_registration.save respond_to do |format| format.html { - if current_user.super_user? - return_to = params.dig(:event_registration, :event_id).present? ? "manage" : "ticket" - redirect_to confirm_event_registration_path(@event_registration, return_to: return_to) - elsif params.dig(:event_registration, :event_id).present? - redirect_to manage_event_path(@event_registration.event), - notice: "Registration created." - else - redirect_to registration_ticket_path(@event_registration.slug), - notice: "Registration created." - end + redirect_to confirm_event_registration_path(@event_registration, return_to: params[:return_to]) } end else respond_to do |format| format.html { - if @event_registration.event_id.present? - redirect_to manage_event_path(@event_registration.event), - alert: @event_registration.errors.full_messages.to_sentence - else - redirect_to event_registrations_path, - alert: @event_registration.errors.full_messages.to_sentence + case params[:return_to] + when "manage" then redirect_to manage_event_path(@event_registration.event), alert: @event_registration.errors.full_messages.to_sentence + else redirect_to event_registrations_path, alert: @event_registration.errors.full_messages.to_sentence end } end @@ -93,10 +81,10 @@ def update respond_to do |format| format.turbo_stream format.html { - if params[:return_to] == "manage" - redirect_to manage_event_path(@event_registration.event), notice: "Registration was successfully updated.", status: :see_other - else - redirect_to registration_ticket_path(@event_registration.slug), notice: "Registration was successfully updated.", status: :see_other + case params[:return_to] + when "manage" then redirect_to manage_event_path(@event_registration.event), notice: "Registration was successfully updated.", status: :see_other + when "index" then redirect_to event_registrations_path, notice: "Registration was successfully updated.", status: :see_other + else redirect_to registration_ticket_path(@event_registration.slug), notice: "Registration was successfully updated.", status: :see_other end } end @@ -133,22 +121,26 @@ def process_confirm current_user: current_user ) - if params[:return_to] == "manage" - redirect_to manage_event_path(@event_registration.event), notice: result.summary - else - redirect_to registration_ticket_path(@event_registration.slug), notice: result.summary + case params[:return_to] + when "manage" then redirect_to manage_event_path(@event_registration.event), notice: result.summary + when "index" then redirect_to event_registrations_path, notice: result.summary + else redirect_to registration_ticket_path(@event_registration.slug), notice: result.summary end end def destroy authorize! @event_registration + event = @event_registration.event if @event_registration.destroy flash[:notice] = "Registration deleted." - else flash[:alert] = @event_registration.errors.full_messages.to_sentence end - redirect_to event_registrations_path + + case params[:return_to] + when "manage" then redirect_to manage_event_path(event) + else redirect_to event_registrations_path + end end # Optional hooks for setting variables for forms or index @@ -167,14 +159,15 @@ def set_form_variables private def set_event_registration - @event_registration = EventRegistration.includes(:registrant, { event: [ :location, :event_forms ] }, comments: [ :created_by, :updated_by ]).find(params[:id]) + @event_registration = EventRegistration.includes({ registrant: { affiliations: :organization } }, { event: [ :location, :event_forms ] }, :organizations, comments: [ :created_by, :updated_by ]).find(params[:id]) end # Strong parameters def event_registration_params params.require(:event_registration).permit( :event_id, :registrant_id, :status, - :scholarship_recipient, :scholarship_tasks_completed, + :scholarship_requested, :scholarship_recipient, :scholarship_tasks_completed, + organization_ids: [], comments_attributes: [ :id, :body, :_destroy ] ) end diff --git a/app/controllers/events/public_registrations_controller.rb b/app/controllers/events/public_registrations_controller.rb index ade7bfcec..d9dd62012 100644 --- a/app/controllers/events/public_registrations_controller.rb +++ b/app/controllers/events/public_registrations_controller.rb @@ -142,7 +142,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 diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 4188bc8dc..c043b3f52 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -39,7 +39,7 @@ def manage authorize! @event, to: :manage? @event = @event.decorate scope = @event.event_registrations - .includes(:payments, :comments, registrant: [ :user, :contact_methods, { affiliations: :organization }, { avatar_attachment: :blob } ]) + .includes(:payments, :comments, :organizations, registrant: [ :user, :contact_methods, { avatar_attachment: :blob } ]) .joins(:registrant) scope = scope.keyword(params[:keyword]) if params[:keyword].present? scope = scope.attendance_status(params[:attendance_status]) if params[:attendance_status].present? @@ -75,7 +75,7 @@ def create end success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Event create failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end @@ -105,7 +105,7 @@ def update else raise ActiveRecord::Rollback end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Event update failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end diff --git a/app/controllers/forms_controller.rb b/app/controllers/forms_controller.rb new file mode 100644 index 000000000..7de7111d3 --- /dev/null +++ b/app/controllers/forms_controller.rb @@ -0,0 +1,141 @@ +class FormsController < ApplicationController + before_action :set_form, only: %i[show edit update destroy question_library add_questions] + + def index + authorize! + @forms = authorized_scope(Form.all) + .includes(:form_fields, :events) + .order(:name) + .paginate(page: params[:page], per_page: 25) + end + + def show + authorize! @form + @form_fields_by_group = grouped_fields + @linked_events = @form.events.order(start_date: :desc) + end + + def new + authorize! Form.new + @builders = builder_options + end + + def create + authorize! Form.new + + @form = case params[:builder_type] + when "short_registration" + ShortEventRegistrationFormBuilder.build_standalone! + when "extended_registration" + ExtendedEventRegistrationFormBuilder.build_standalone! + when "scholarship_application" + ScholarshipApplicationFormBuilder.build_standalone! + when "generic" + Form.create!(name: params[:form_name].presence || "New Form") + else + Form.create!(name: "New Form") + end + + redirect_to edit_form_path(@form), notice: "Form was successfully created. Customize it below." + end + + def edit + authorize! @form + set_form_variables + end + + def update + authorize! @form + if @form.update(form_params) + redirect_to @form, notice: "Form was successfully updated.", status: :see_other + else + set_form_variables + render :edit, status: :unprocessable_content + end + end + + def destroy + authorize! @form + @form.destroy! + redirect_to forms_path, notice: "Form was successfully destroyed." + end + + def question_library + authorize! @form + existing_keys = @form.form_fields.reorder(position: :asc).pluck(:field_key).compact + scope = FormField.unscoped + .where.not(form_id: @form.id) + .where.not(field_key: [ nil, "" ]) + scope = scope.where.not(field_key: existing_keys) if existing_keys.any? + @available_fields = scope.order(:field_group, :position).group_by(&:field_group) + render layout: false + end + + def add_questions + authorize! @form + source_field_ids = Array(params[:source_field_ids]).map(&:to_i) + max_position = @form.form_fields.reorder(position: :asc).maximum(:position) || 0 + + FormField.unscoped.where(id: source_field_ids).find_each do |source| + max_position += 1 + new_field = @form.form_fields.create!( + question: source.question, + answer_type: source.answer_type, + answer_datatype: source.answer_datatype, + status: source.status, + position: max_position, + is_required: source.is_required, + instructional_hint: source.instructional_hint, + field_key: source.field_key, + field_group: source.field_group + ) + + source.form_field_answer_options.each do |ffao| + new_field.form_field_answer_options.create!(answer_option: ffao.answer_option) + end + end + + redirect_to edit_form_path(@form), notice: "Questions added successfully." + end + + private + + def set_form + @form = Form.find(params[:id]) + end + + def set_form_variables + @form_fields_by_group = grouped_fields + @field_groups = @form.form_fields.reorder(position: :asc).pluck(:field_group).compact.uniq + @answer_type_options = FormField.answer_types.keys.map { |k| [ k.humanize, k ] } + end + + def grouped_fields + @form.form_fields.reorder(position: :asc).group_by(&:field_group) + end + + def builder_options + [ + { key: "short_registration", name: ShortEventRegistrationFormBuilder::FORM_NAME, + description: "Contact, consent, qualitative, and scholarship fields" }, + { key: "extended_registration", name: ExtendedEventRegistrationFormBuilder::FORM_NAME, + description: "Contact, background, professional, qualitative, scholarship, and payment fields" }, + { key: "scholarship_application", name: ScholarshipApplicationFormBuilder::FORM_NAME, + description: "Scholarship-specific fields" }, + { key: "generic", name: "Generic Form", + description: "Start with a blank form and add fields manually" } + ] + end + + def form_params + params.require(:form).permit( + :name, + :scholarship_application, + form_fields_attributes: [ + :id, :question, :answer_type, :answer_datatype, + :status, :position, :is_required, :instructional_hint, + :field_key, :field_group, :_destroy + ] + ) + end +end diff --git a/app/controllers/resources_controller.rb b/app/controllers/resources_controller.rb index c12c50b0e..a519531a7 100644 --- a/app/controllers/resources_controller.rb +++ b/app/controllers/resources_controller.rb @@ -73,7 +73,7 @@ def create assign_associations(@resource) success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Resource create failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end @@ -99,7 +99,7 @@ def update assign_associations(@resource) success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Resource update failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index f483efcfb..bc1fdb081 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -75,7 +75,7 @@ def create end success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Story create failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end @@ -101,7 +101,7 @@ def update end success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Story update failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end diff --git a/app/controllers/story_ideas_controller.rb b/app/controllers/story_ideas_controller.rb index e7e287759..5e1bfe803 100644 --- a/app/controllers/story_ideas_controller.rb +++ b/app/controllers/story_ideas_controller.rb @@ -59,7 +59,7 @@ def create notification_type: 0) success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "StoryIdea create failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end @@ -88,7 +88,7 @@ def update assign_associations(@story_idea) success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "StoryIdea update failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end diff --git a/app/controllers/tutorials_controller.rb b/app/controllers/tutorials_controller.rb index aca8c3f7d..17d5faf23 100644 --- a/app/controllers/tutorials_controller.rb +++ b/app/controllers/tutorials_controller.rb @@ -51,7 +51,7 @@ def create assign_associations(@tutorial) success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Tutorial create failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end @@ -75,7 +75,7 @@ def update assign_associations(@tutorial) success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e Rails.logger.error "Tutorial update failed: #{e.class} - #{e.message}" raise ActiveRecord::Rollback end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 78ed72cba..966402b7f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -151,6 +151,8 @@ def destroy authorize! @user @user.destroy! redirect_to users_path, notice: "User was successfully destroyed." + rescue ActiveRecord::InvalidForeignKey + redirect_to @user, alert: "Unable to delete this user because they have associated records that cannot be removed." end # --------------------------------------------------------- diff --git a/app/controllers/workshop_ideas_controller.rb b/app/controllers/workshop_ideas_controller.rb index 2b1eda2c6..681e2fd0a 100644 --- a/app/controllers/workshop_ideas_controller.rb +++ b/app/controllers/workshop_ideas_controller.rb @@ -43,6 +43,11 @@ def create set_form_variables render :new, status: :unprocessable_content end + rescue ActiveRecord::RecordNotUnique => e + Rails.logger.error "WorkshopIdea create failed: #{e.class} - #{e.message}" + @workshop_idea.errors.add(:base, "could not be saved due to a conflict. Please try again.") + set_form_variables + render :new, status: :unprocessable_content end def edit @@ -59,6 +64,11 @@ def update set_form_variables render :edit, status: :unprocessable_content end + rescue ActiveRecord::RecordNotUnique => e + Rails.logger.error "WorkshopIdea update failed: #{e.class} - #{e.message}" + @workshop_idea.errors.add(:base, "could not be saved due to a conflict. Please try again.") + set_form_variables + render :edit, status: :unprocessable_content end def destroy diff --git a/app/controllers/workshop_variation_ideas_controller.rb b/app/controllers/workshop_variation_ideas_controller.rb index 773cc8cda..b3c305cf1 100644 --- a/app/controllers/workshop_variation_ideas_controller.rb +++ b/app/controllers/workshop_variation_ideas_controller.rb @@ -70,6 +70,11 @@ def create set_form_variables render :new, status: :unprocessable_content end + rescue ActiveRecord::RecordNotUnique => e + Rails.logger.error "WorkshopVariationIdea create failed: #{e.class} - #{e.message}" + @workshop_variation_idea.errors.add(:base, "could not be saved due to a conflict. Please try again.") + set_form_variables + render :new, status: :unprocessable_content end def update @@ -80,6 +85,11 @@ def update set_form_variables render :edit, status: :unprocessable_content end + rescue ActiveRecord::RecordNotUnique => e + Rails.logger.error "WorkshopVariationIdea update failed: #{e.class} - #{e.message}" + @workshop_variation_idea.errors.add(:base, "could not be saved due to a conflict. Please try again.") + set_form_variables + render :edit, status: :unprocessable_content end def destroy diff --git a/app/controllers/workshop_variations_controller.rb b/app/controllers/workshop_variations_controller.rb index 94fb27403..3c55fa029 100644 --- a/app/controllers/workshop_variations_controller.rb +++ b/app/controllers/workshop_variations_controller.rb @@ -35,7 +35,7 @@ def create end success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e log_workshop_error("creation", e) raise ActiveRecord::Rollback end diff --git a/app/controllers/workshops_controller.rb b/app/controllers/workshops_controller.rb index d132fc940..03ac9f226 100644 --- a/app/controllers/workshops_controller.rb +++ b/app/controllers/workshops_controller.rb @@ -100,7 +100,7 @@ def create end success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e log_workshop_error("creation", e) raise ActiveRecord::Rollback end @@ -158,7 +158,7 @@ def update assign_associations(@workshop) success = true end - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e log_workshop_error("update", e) raise ActiveRecord::Rollback end diff --git a/app/decorators/resource_decorator.rb b/app/decorators/resource_decorator.rb index f0c1c5f54..2d37dca29 100644 --- a/app/decorators/resource_decorator.rb +++ b/app/decorators/resource_decorator.rb @@ -1,6 +1,7 @@ class ResourceDecorator < ApplicationDecorator def detail(length: nil) - length ? body&.truncate(length) : body # TODO - rename field + text = rhino_body&.to_plain_text + length ? text&.truncate(length) : text end def default_display_image @@ -20,7 +21,7 @@ def truncated_title end def truncated_text(ln = 100) - h.truncate(html.text.gsub(/(<[^>]+>)/, ""), length: ln) + h.truncate(rhino_body.to_plain_text, length: ln) end def display_title @@ -28,7 +29,7 @@ def display_title end def flex_text - h.truncate(html.text, length: 200) + h.truncate(rhino_body.to_plain_text, length: 200) end def breadcrumbs @@ -44,7 +45,7 @@ def display_date end def display_text - "
- <%= simple_format(@event.detail) %> + <%= simple_format(@event.rhino_description.to_plain_text) %>
+ <% active_orgs.each_with_index do |org, i| %> + <%= org.name %><% unless connected_org_ids.include?(org.id) %><% end %><%= ", " unless i == active_orgs.size - 1 %> + <% end %> +
<% end %>Click to toggle. Crossed-out organizations will be removed on save.
+Free event
<% end %> - <% if f.object.scholarship? %> - Scholarship recipient - <% end %><%= @event_registrations.size %> registrant<%= "s" if @event_registrations.size != 1 %>
- <% reg_form = @event.registration_form %> - <% form_submissions = reg_form ? reg_form.person_forms.where(person_id: @event_registrations.map(&:registrant_id)).pluck(:person_id, :created_at).to_h : {} %> + <% event_form_ids = @event.form_ids %> + <% form_submissions = event_form_ids.any? ? PersonForm.joins(:form).where(form_id: event_form_ids, person_id: @event_registrations.map(&:registrant_id)).pluck(:person_id, :created_at, Arel.sql("forms.name")).group_by(&:first).transform_values { |rows| rows.map { |_, ts, name| [ name, ts ] } } : {} %> <% if @event_registrations.any? %>|
- <% orgs = person.affiliations.select { |a| !a.inactive? && (a.end_date.nil? || a.end_date >= Date.current) }.map(&:organization).compact.uniq %>
- <% if orgs.any? %>
-
- <% orgs.first(3).each do |org| %>
- <%= link_to org.name, organization_path(org),
- data: { turbo_frame: "_top" },
- class: "inline-block px-3 py-1 rounded-md text-sm font-medium #{DomainTheme.bg_class_for(:organizations, intensity: 100)} #{DomainTheme.text_class_for(:organizations)} #{DomainTheme.bg_class_for(:organizations, intensity: 100, hover: true)}" %>
+ <% if registration.organizations.any? %>
+
+
<% else %>
—
<% end %>
diff --git a/app/views/events/manage.html.erb b/app/views/events/manage.html.erb
index 013ed4fdb..cb16c0305 100644
--- a/app/views/events/manage.html.erb
+++ b/app/views/events/manage.html.erb
@@ -15,7 +15,7 @@
<%= link_to "View", event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
<% if allowed_to?(:edit?, @event) %>
<%= link_to "Edit", edit_event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
- <%= link_to "New registration", new_event_registration_path(event_id: @event.id), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <%= link_to "New registration", new_event_registration_path(event_id: @event.id, return_to: "manage"), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
<% if @event.event_forms.registration.exists? %>
<%= link_to "Public registration", new_event_public_registration_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1", target: "_blank" %>
<%= link_to "Scholarship version", new_event_public_registration_path(@event, scholarship: 1), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1", target: "_blank" %>
diff --git a/app/views/events/public_registrations/_form_field.html.erb b/app/views/events/public_registrations/_form_field.html.erb
index d5fe9730f..9cf64881d 100644
--- a/app/views/events/public_registrations/_form_field.html.erb
+++ b/app/views/events/public_registrations/_form_field.html.erb
@@ -1,4 +1,4 @@
-<%# locals: (field:, value: nil) %>
+<%# locals: (field:, value: nil, label: nil) %>
<% return if field.group_header? %>
<% error = @field_errors&.dig(field.id) %>
@@ -6,7 +6,7 @@
+
+
+ <%# Fields grouped by section %>
+
+ <%= f.input :name, label: "Form name",
+ input_html: { class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" } %>
+
+
+ <%= f.input :scholarship_application, as: :boolean %>
+
+
+ <% @field_groups.each do |group| %>
+
+
+
+<% end %>
+
+<%# Question Library (outside the main form to avoid nested forms) %>
+
+
+ <% end %>
+
+ <%# Show ungrouped section if no groups exist yet %>
+ <% if @field_groups.empty? %>
+
+
+
+ <%= group.presence || "Ungrouped" %>+
+ <% (@form_fields_by_group[group] || []).each do |field| %>
+ <%= f.simple_fields_for :form_fields, field do |ff| %>
+ <%= render "form_field_row", ff: ff, field: field %>
+ <% end %>
+ <% end %>
+
+
+
+ <%= link_to_add_association "Add blank field", f, :form_fields,
+ class: "text-blue-600 hover:underline text-sm",
+ data: {
+ association_insertion_node: "#fields_#{group}",
+ association_insertion_method: "append"
+ } %>
+
+
+
+ <% end %>
+ Fields+ +
+
+
+
+ <%= link_to_add_association "Add blank field", f, :form_fields,
+ class: "text-blue-600 hover:underline text-sm",
+ data: {
+ association_insertion_node: "#fields_new",
+ association_insertion_method: "append"
+ } %>
+
+
+ <%= turbo_frame_tag "question_library", src: question_library_form_path(@form), loading: :lazy do %>
+
diff --git a/app/views/forms/_form_field_fields.html.erb b/app/views/forms/_form_field_fields.html.erb
new file mode 100644
index 000000000..11c0a19f5
--- /dev/null
+++ b/app/views/forms/_form_field_fields.html.erb
@@ -0,0 +1,44 @@
+Loading question library... + <% end %> +
+
diff --git a/app/views/forms/_form_field_row.html.erb b/app/views/forms/_form_field_row.html.erb
new file mode 100644
index 000000000..082550ea5
--- /dev/null
+++ b/app/views/forms/_form_field_row.html.erb
@@ -0,0 +1,47 @@
+
+ <%# Question label %>
+
+
+ <%= f.input :question, label: false, placeholder: "Question text",
+ input_html: { class: "w-full rounded border-gray-300 px-2 py-1 text-sm" } %>
+
+
+ <%# Answer type %>
+
+ <%= f.input :answer_type,
+ collection: FormField.answer_types.keys.map { |k| [ k.humanize, k ] },
+ label: false,
+ input_html: { class: "w-full text-xs rounded border-gray-300" } %>
+
+
+ <%# Field group %>
+
+ <%= f.input :field_group, label: false, placeholder: "Group",
+ input_html: { class: "w-full rounded border-gray-300 px-2 py-1 text-xs" } %>
+
+
+ <%# Field key %>
+
+ <%= f.input :field_key, label: false, placeholder: "field_key",
+ input_html: { class: "w-full rounded border-gray-300 px-2 py-1 text-xs font-mono text-gray-500" } %>
+
+
+ <%# Required %>
+
+
+
+
+ <%# Remove %>
+
+ <%= link_to_remove_association "Remove",
+ f,
+ class: "text-xs text-red-600 hover:text-red-800 underline dynamic" %>
+
+
+ <%= ff.hidden_field :id %>
+
+
diff --git a/app/views/forms/_question_library.html.erb b/app/views/forms/_question_library.html.erb
new file mode 100644
index 000000000..659a9437c
--- /dev/null
+++ b/app/views/forms/_question_library.html.erb
@@ -0,0 +1,46 @@
+<%= turbo_frame_tag "question_library" do %>
+
+ <%# Question label (editable) %>
+
+
+ <%= ff.input :question, label: false, placeholder: "Question text",
+ input_html: { class: "w-full rounded border-gray-300 px-2 py-1 text-sm" } %>
+
+
+ <%# Answer type %>
+
+ <%= ff.input :answer_type,
+ collection: FormField.answer_types.keys.map { |k| [ k.humanize, k ] },
+ label: false,
+ input_html: { class: "w-full text-xs rounded border-gray-300" } %>
+
+
+ <%# Field group %>
+
+ <%= ff.input :field_group, label: false, placeholder: "Group",
+ input_html: { class: "w-full rounded border-gray-300 px-2 py-1 text-xs" } %>
+
+
+ <%# Field key (readonly reference) %>
+
+ <%= ff.input :field_key, label: false, placeholder: "field_key",
+ input_html: { class: "w-full rounded border-gray-300 px-2 py-1 text-xs font-mono text-gray-500",
+ readonly: field.persisted? } %>
+
+
+ <%# Required + Status toggles %>
+
+
+
+
+ <%# Remove %>
+
+ <%= link_to_remove_association "Remove",
+ ff,
+ class: "text-xs text-red-600 hover:text-red-800 underline" %>
+
+
+
+<% end %>
diff --git a/app/views/forms/edit.html.erb b/app/views/forms/edit.html.erb
new file mode 100644
index 000000000..f6cfb6df9
--- /dev/null
+++ b/app/views/forms/edit.html.erb
@@ -0,0 +1,16 @@
+<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
+Add questions from library+Select questions from other forms to add to this form. + + <% if @available_fields.present? %> + <%= form_with url: add_questions_form_path(@form), method: :post do |f| %> +
+ <%# Search filter %>
+
+
+
+ <% end %>
+ <% else %>
+
+ <% @available_fields.each do |group, fields| %>
+
+
+
+
+
+ <% end %>
+ <%= group.presence || "Ungrouped" %>+
+ <% fields.uniq(&:field_key).each do |field| %>
+
+ <% end %>
+
+ No additional questions available to add. + <% end %> +
+
diff --git a/app/views/forms/index.html.erb b/app/views/forms/index.html.erb
new file mode 100644
index 000000000..e9447b98d
--- /dev/null
+++ b/app/views/forms/index.html.erb
@@ -0,0 +1,66 @@
+<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
+
+ <%= link_to "Home", root_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <%= link_to "Forms", forms_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <%= link_to "View", form_path(@form), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+
+ Edit form+ +
+
+
+ <%= render "form" %>
+ <%= render "shared/audit_info", resource: @form %>
+
+
+
diff --git a/app/views/forms/new.html.erb b/app/views/forms/new.html.erb
new file mode 100644
index 000000000..0c9787695
--- /dev/null
+++ b/app/views/forms/new.html.erb
@@ -0,0 +1,38 @@
+<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
+
+
+
+
+
+ Forms+Manage registration and application forms. +
+ <% if allowed_to?(:new?, Form) %>
+ <%= link_to "New Form", new_form_path, class: "admin-only bg-blue-100 btn btn-primary-outline" %>
+ <% end %>
+
+
+ <% if @forms.present? %>
+
+
+
+
<%= tailwind_paginate @forms %>
+ <% else %>
+ No forms found. + <% end %> +
+
diff --git a/app/views/forms/question_library.html.erb b/app/views/forms/question_library.html.erb
new file mode 100644
index 000000000..d03cbc8f7
--- /dev/null
+++ b/app/views/forms/question_library.html.erb
@@ -0,0 +1 @@
+<%= render "question_library" %>
diff --git a/app/views/forms/show.html.erb b/app/views/forms/show.html.erb
new file mode 100644
index 000000000..ea0beaf96
--- /dev/null
+++ b/app/views/forms/show.html.erb
@@ -0,0 +1,76 @@
+<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
+
+ <%= link_to "Home", root_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <%= link_to "Forms", forms_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+
+
+ Create a new form+Choose a template to start with, or create a blank form. + +
+ <% @builders.each do |builder| %>
+ <% if builder[:key] == "generic" %>
+ <%= form_with url: forms_path, method: :post, class: "block" do %>
+
+
+
+
+ <% end %>
+ <% else %>
+ <%= form_with url: forms_path, method: :post, class: "block" do %>
+
+
+ <% end %>
+ <% end %>
+ <% end %>
+ <%= builder[:name] %>+<%= builder[:description] %> +
+
+
+
+
+
diff --git a/app/views/people/_form.html.erb b/app/views/people/_form.html.erb
index 7805f3c6a..51b83e1f4 100644
--- a/app/views/people/_form.html.erb
+++ b/app/views/people/_form.html.erb
@@ -379,10 +379,10 @@
<% if f.object.persisted? %>
<% if allowed_to?(:manage?, Comment) %>
-
+ <%= link_to "Home", root_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <%= link_to "Forms", forms_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <% if allowed_to?(:edit?, @form) %>
+ <%= link_to "Edit", edit_form_path(@form), class: "admin-only bg-blue-100 text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <% end %>
+
+
+
+
+
+ <%# Linked Events %>
+ <% if @linked_events.any? %>
+ <%= @form.display_name %>+ <% if @form.scholarship_application? %> + Scholarship + <% end %> +
+
+ <% end %>
+
+ <%# Form Fields by Group %>
+ Linked Events+
+ <% @linked_events.each do |event| %>
+ <%= link_to event.title, event_path(event),
+ class: "inline-block px-3 py-1 text-sm bg-blue-50 text-blue-700 rounded hover:bg-blue-100" %>
+ <% end %>
+
+
+ <% @form_fields_by_group.each do |group, fields| %>
+
+
+ <%# Actions %>
+
+
+ <% end %>
+ <%= group.presence || "Ungrouped" %>+
+ <% fields.each do |field| %>
+
+
+
+ <% end %>
+
+
+
+ <%= field.question %>
+ <% if field.is_required? %>
+ Required
+ <% end %>
+ <% if field.inactive? %>
+ Inactive
+ <% end %>
+
+ <% if field.instructional_hint.present? %>
+ <%= field.instructional_hint %> + <% end %> +
+ <%= field.answer_type.humanize %>
+ <% if field.field_key.present? %>
+ <%= field.field_key %>
+ <% end %>
+
+
+ <% if allowed_to?(:edit?, @form) %>
+ <%= link_to "Edit", edit_form_path(@form), class: "btn btn-primary-outline" %>
+ <% end %>
+ <% if allowed_to?(:destroy?, @form) %>
+ <%= link_to "Delete", @form, class: "btn btn-danger-outline",
+ data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete this form?" } %>
+ <% end %>
+
+
+
diff --git a/app/views/shared/_navbar_user.html.erb b/app/views/shared/_navbar_user.html.erb
index 37d84fe64..16558087a 100644
--- a/app/views/shared/_navbar_user.html.erb
+++ b/app/views/shared/_navbar_user.html.erb
@@ -31,7 +31,7 @@
<% end %>
<% end %>
- <% if person && allowed_to?(:show?, person) %>
+ <% if person && allowed_to?(:manage?, Person) %>
<%= link_to person_path(person),
class: "admin-only bg-blue-100 flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 space-x-2" do %>
diff --git a/app/views/story_shares/_sector_index.html.erb b/app/views/story_shares/_sector_index.html.erb
index 1f18e4140..fb5741bc2 100644
--- a/app/views/story_shares/_sector_index.html.erb
+++ b/app/views/story_shares/_sector_index.html.erb
@@ -41,7 +41,7 @@
Comments
-
<%= f.simple_fields_for :comments do |cf| %>
diff --git a/app/views/people/show.html.erb b/app/views/people/show.html.erb
index af8e2b48f..048793e04 100644
--- a/app/views/people/show.html.erb
+++ b/app/views/people/show.html.erb
@@ -21,10 +21,12 @@
<% end %>
<%= link_to "Home", root_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
- <%= link_to "People", people_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <% if allowed_to?(:index?, Person) %>
+ <%= link_to "People", people_path, class: "admin-only bg-blue-100 text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ <% end %>
<% if allowed_to?(:edit?, @person) %>
<%= link_to "Edit", edit_person_path(@person),
- class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
+ class: "admin-only bg-blue-100 text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
<% end %>
<%= render "bookmarks/editable_bookmark_button", resource: @person.object %>
@@ -66,10 +68,16 @@
<% email = @person.user&.email || @person.email %>
- <% if (@person.profile_show_email? || allowed_to?(:manage?, Person)) && email.present? %>
-
- 📧 <%= mail_to email, email, class: "text-blue-600 hover:underline" %>
-
+ <% if email.present? %>
+ <% if @person.profile_show_email? %>
+
+ 📧 <%= mail_to email, email, class: "text-blue-600 hover:underline" %>
+
+ <% elsif allowed_to?(:manage?, Person) %>
+
+ 📧 <%= mail_to email, email, class: "text-blue-600 hover:underline" %>
+
+ <% end %>
<% end %>
<% phone = @person.phone_number || @person.user&.phone %>
<% if @person.profile_show_phone? && phone.present? %>
@@ -210,7 +218,7 @@
<% end %>
- <% if allowed_to?(:edit?, @person) %>
+ <% if allowed_to?(:show?, @person) %>
@@ -263,12 +271,6 @@
<% end %>
-
- <% if allowed_to?(:manage?, Comment) %>
-
- <%= render "comments/section", commentable: @person.object %>
-
- <% end %>
- <%= truncate(strip_tags(story.body.to_s), length: 140) %> + <%= truncate(story.rhino_body.to_plain_text, length: 140) %>
diff --git a/app/views/story_shares/_sector_index_row.html.erb b/app/views/story_shares/_sector_index_row.html.erb
index 45498dccb..d04a110e5 100644
--- a/app/views/story_shares/_sector_index_row.html.erb
+++ b/app/views/story_shares/_sector_index_row.html.erb
@@ -14,7 +14,7 @@
- <%= truncate(strip_tags(story.body.to_s), length: 280) %> + <%= truncate(story.rhino_body.to_plain_text, length: 280) %>
diff --git a/app/views/tutorials/_form.html.erb b/app/views/tutorials/_form.html.erb
index a2c17caa6..ef211188b 100644
--- a/app/views/tutorials/_form.html.erb
+++ b/app/views/tutorials/_form.html.erb
@@ -33,8 +33,7 @@
<%= render "shared/form_image_fields", f: f, include_primary_asset: true %>
- <%= f.input :body, as: :text,
- input_html: { rows: 10, value: f.object.body } %>
+ <%= f.input :body, as: :text, input_html: { rows: 10, value: f.object.body } %>
diff --git a/app/views/tutorials/_tutorial.html.erb b/app/views/tutorials/_tutorial.html.erb
index 20c7f619d..119ca7e8c 100644
--- a/app/views/tutorials/_tutorial.html.erb
+++ b/app/views/tutorials/_tutorial.html.erb
@@ -35,9 +35,9 @@
<% end %>
- <% if tutorial.body.present? %>
+ <% if tutorial.rhino_body.present? %>
- <%= tutorial.body.to_s.html_safe %>
+ <%= tutorial.rhino_body %>
<% end %>
diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb
index 1ff64fb68..f4c1a93df 100644
--- a/app/views/users/_form.html.erb
+++ b/app/views/users/_form.html.erb
@@ -140,7 +140,7 @@
<% end %>
- <% if allowed_to?(:destroy?, f.object) %>
+ <% if allowed_to?(:destroy?, f.object) && f.object.deletable? %>
<%= link_to "Delete", @user, class: "btn btn-danger-outline",
data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete?" } %>
<% end %>
diff --git a/app/views/workshop_logs/show.html.erb b/app/views/workshop_logs/show.html.erb
index d6f3f8ac5..3318e59fb 100644
--- a/app/views/workshop_logs/show.html.erb
+++ b/app/views/workshop_logs/show.html.erb
@@ -12,7 +12,7 @@
-
<%= @workshop_log.workshop&.title || "Workshop Log" %>+<%= @workshop_log.workshop&.title || @workshop_log.external_workshop_title || "Workshop Log" %>
@@ -49,8 +49,18 @@
<%= link_to @workshop_log.workshop.title, workshop_path(@workshop_log.workshop),
data: { turbo_frame: "_top" },
class: "inline-block px-3 py-1 rounded-md text-sm font-medium #{DomainTheme.bg_class_for(:workshops, intensity: 100)} #{DomainTheme.text_class_for(:workshops)} #{DomainTheme.bg_class_for(:workshops, intensity: 100, hover: true)}" %>
+ <% if @workshop_log.external_workshop_title.present? %>
+ <%= @workshop_log.external_workshop_title %>
+ <% end %>
+ <% elsif @workshop_log.workshop %>
+ <%= @workshop_log.workshop.title %>
+ <% if @workshop_log.external_workshop_title.present? %>
+ <%= @workshop_log.external_workshop_title %>
+ <% end %>
+ <% elsif @workshop_log.external_workshop_title.present? %>
+ <%= @workshop_log.external_workshop_title %>
<% else %>
- <%= @workshop_log.workshop&.title || "—" %>
+ —
<% end %>
diff --git a/app/views/workshop_variations/index.html.erb b/app/views/workshop_variations/index.html.erb
index a15b3fe52..4f17fbd8c 100644
--- a/app/views/workshop_variations/index.html.erb
+++ b/app/views/workshop_variations/index.html.erb
@@ -41,8 +41,9 @@
<%= @workshop_log.date&.strftime("%B %d, %Y") || "—" %> |
<%= truncate(workshop_variation.name, length: 30) %> | - - <%= truncate(strip_tags(workshop_variation.body), length: 30) %> + <% plain = workshop_variation.rhino_body.to_plain_text %> + + <%= truncate(plain, length: 30) %> |
diff --git a/app/views/workshops/_form.html.erb b/app/views/workshops/_form.html.erb
index 3a60feceb..e84f6f791 100644
--- a/app/views/workshops/_form.html.erb
+++ b/app/views/workshops/_form.html.erb
@@ -44,14 +44,16 @@
<% if allowed_to?(:manage?, Workshop) %>
-
- <%= f.input :full_name, as: :text,
- label: "Author's name",
- hint: "Display name",
- input_html: {
- rows: 1,
- class: "block w-full rounded-md border-gray-300 shadow-sm
- focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
+
+ <% if f.object.full_name.present? && params[:admin] == "true" %>
+ <%= f.input :full_name, as: :text,
+ label: "Author's name",
+ hint: "Display name",
+ input_html: {
+ rows: 1,
+ class: "block w-full rounded-md border-gray-300 shadow-sm
+ focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
+ <% end %>
diff --git a/config/routes.rb b/config/routes.rb
index bbd10c7d9..a511b0f2f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -117,6 +117,12 @@
resources :comments, only: [ :index, :create ]
end
resources :faqs
+ resources :forms do
+ member do
+ get :question_library
+ post :add_questions
+ end
+ end
resources :notifications, only: [ :index, :show ] do
member do
post :resend
diff --git a/db/migrate/20260301143000_create_event_registration_organizations.rb b/db/migrate/20260301143000_create_event_registration_organizations.rb
new file mode 100644
index 000000000..f151d00e4
--- /dev/null
+++ b/db/migrate/20260301143000_create_event_registration_organizations.rb
@@ -0,0 +1,15 @@
+class CreateEventRegistrationOrganizations < ActiveRecord::Migration[8.1]
+ def change
+ create_table :event_registration_organizations do |t|
+ t.references :event_registration, null: false, foreign_key: true
+ t.references :organization, null: false, foreign_key: true, type: :integer
+
+ t.timestamps
+ end
+
+ add_index :event_registration_organizations,
+ [ :event_registration_id, :organization_id ],
+ unique: true,
+ name: "idx_event_reg_orgs_on_registration_and_org"
+ end
+end
diff --git a/db/migrate/20260301150000_make_workshop_id_optional_on_reports.rb b/db/migrate/20260301150000_make_workshop_id_optional_on_reports.rb
new file mode 100644
index 000000000..4e950d49b
--- /dev/null
+++ b/db/migrate/20260301150000_make_workshop_id_optional_on_reports.rb
@@ -0,0 +1,5 @@
+class MakeWorkshopIdOptionalOnReports < ActiveRecord::Migration[8.1]
+ def change
+ change_column_null :reports, :workshop_id, true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ee45b0729..c5332b2a8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2026_03_01_120200) do
+ActiveRecord::Schema[8.1].define(version: 2026_03_01_150000) do
create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.bigint "action_text_rich_text_id", null: false
t.datetime "created_at", null: false
@@ -408,6 +408,16 @@
t.index ["form_id"], name: "index_event_forms_on_form_id"
end
+ create_table "event_registration_organizations", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "event_registration_id", null: false
+ t.integer "organization_id", null: false
+ t.datetime "updated_at", null: false
+ t.index ["event_registration_id", "organization_id"], name: "idx_event_reg_orgs_on_registration_and_org", unique: true
+ t.index ["event_registration_id"], name: "idx_on_event_registration_id_806bdcd019"
+ t.index ["organization_id"], name: "index_event_registration_organizations_on_organization_id"
+ end
+
create_table "event_registrations", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.bigint "event_id"
@@ -820,7 +830,7 @@
t.string "type"
t.datetime "updated_at", precision: nil, null: false
t.integer "windows_type_id", null: false
- t.integer "workshop_id", null: false
+ t.integer "workshop_id"
t.string "workshop_name"
t.index ["created_by_id"], name: "index_reports_on_created_by_id"
t.index ["organization_id"], name: "index_reports_on_organization_id"
@@ -1342,6 +1352,8 @@
add_foreign_key "contact_methods", "addresses"
add_foreign_key "event_forms", "events"
add_foreign_key "event_forms", "forms"
+ add_foreign_key "event_registration_organizations", "event_registrations"
+ add_foreign_key "event_registration_organizations", "organizations"
add_foreign_key "event_registrations", "events"
add_foreign_key "event_registrations", "people", column: "registrant_id"
add_foreign_key "events", "locations"
diff --git a/db/seeds/dummy_dev_seeds.rb b/db/seeds/dummy_dev_seeds.rb
index 4bc35700e..9cf0f20d0 100644
--- a/db/seeds/dummy_dev_seeds.rb
+++ b/db/seeds/dummy_dev_seeds.rb
@@ -943,7 +943,250 @@
end
end
+puts "Creating Event Registrations…"
+# Key people for named scenarios
+amy_person = User.find_by(email: "amy.user@example.com")&.person
+maria_j = Person.find_by(first_name: "Maria", last_name: "Johnson")
+anna_g = Person.find_by(first_name: "Anna", last_name: "Garcia")
+sarah_s = Person.find_by(first_name: "Sarah", last_name: "Smith")
+lisa_w = Person.find_by(first_name: "Lisa", last_name: "Williams")
+jessica_b = Person.find_by(first_name: "Jessica", last_name: "Brown")
+kim_d = Person.find_by(first_name: "Kim", last_name: "Davis")
+rosa_dlc = Person.find_by(first_name: "Rosa", last_name: "De La Cruz")
+mario_j = Person.find_by(first_name: "Mario", last_name: "Johnson") # no user
+angel_g = Person.find_by(first_name: "Angel", last_name: "Garcia") # no user
+linda_w = Person.find_by(first_name: "Linda", last_name: "Williams") # no user
+
+# Events by name for clarity
+facilitator_training = Event.find_by(title: "AWBW Facilitator Training")
+trauma_training = Event.find_by(title: "Facilitator Training: Trauma-Informed Art Practices")
+wellness_day = Event.find_by(title: "A Year of Healing and Rebuilding Together Wellness Day")
+youth_day = Event.find_by(title: "Youth Creativity Day")
+mindful_art = Event.find_by(title: "Mindful Art for Survivors Workshop")
+virtual_session = Event.find_by(title: "Art as Healing: Virtual Group Session")
+roundtable = Event.find_by(title: "Leaders in Creativity: Facilitator Roundtable")
+family_day = Event.find_by(title: "Family Creative Expression Day")
+# "Community Open Studio Night" and "Annual Celebration of Voices" have no registration forms — left with zero registrations
+
+registrations_data = []
+
+# --- Facilitator Training: multiple registrations from different people, extended form ---
+# Amy: registered, with form submission, scholarship recipient
+# Maria Johnson: registered, with form submission (has user)
+# Anna Garcia: attended, with form submission (has user)
+# Mario Johnson: registered, no form submission (no user)
+# Kim Davis: cancelled (has user)
+if facilitator_training
+ [
+ { person: amy_person, status: "registered", scholarship_recipient: true, scholarship_tasks_completed: false },
+ { person: maria_j, status: "registered" },
+ { person: anna_g, status: "attended" },
+ { person: mario_j, status: "registered" },
+ { person: kim_d, status: "cancelled" }
+ ].each do |data|
+ next unless data[:person]
+ registrations_data << data.merge(event: facilitator_training)
+ end
+end
+
+# --- Trauma Training: extended form, scholarship ---
+# Sarah Smith: registered with form (has user)
+# Jessica Brown: registered with form, scholarship (has user)
+# Angel Garcia: registered, no form (no user)
+# Linda Williams: no_show (no user)
+if trauma_training
+ [
+ { person: sarah_s, status: "registered" },
+ { person: jessica_b, status: "registered", scholarship_recipient: true, scholarship_tasks_completed: true },
+ { person: angel_g, status: "registered" },
+ { person: linda_w, status: "no_show" }
+ ].each do |data|
+ next unless data[:person]
+ registrations_data << data.merge(event: trauma_training)
+ end
+end
+
+# --- Amy registered to multiple events (person registered across events) ---
+if amy_person
+ [ wellness_day, mindful_art, virtual_session ].compact.each do |evt|
+ registrations_data << { person: amy_person, event: evt, status: "registered" }
+ end
+end
+
+# --- Maria Johnson also registered to multiple events ---
+if maria_j
+ [ wellness_day, youth_day ].compact.each do |evt|
+ registrations_data << { person: maria_j, event: evt, status: "registered" }
+ end
+end
+
+# --- Rosa De La Cruz registered to a couple events (has user) ---
+if rosa_dlc
+ [ wellness_day, family_day ].compact.each do |evt|
+ registrations_data << { person: rosa_dlc, event: evt, status: "registered" }
+ end
+end
+
+# --- Lisa Williams: incomplete_attendance on one event ---
+if lisa_w && roundtable
+ registrations_data << { person: lisa_w, event: roundtable, status: "incomplete_attendance" }
+end
+
+# --- People with multiple active affiliations — ensures org snapshots get exercised ---
+mariana_j = Person.find_by(first_name: "Mariana", last_name: "Johnson")
+samuel_s = Person.find_by(first_name: "Samuel", last_name: "Smith")
+lisa_wn = Person.find_by(first_name: "Lisa", last_name: "Williamson")
+kim_dv = Person.find_by(first_name: "Kim", last_name: "Davidson")
+sarah_d = Person.find_by(first_name: "Sarah", last_name: "Davis")
+
+{ mariana_j => youth_day, samuel_s => mindful_art, lisa_wn => virtual_session,
+ kim_dv => family_day, sarah_d => roundtable }.each do |person, evt|
+ next unless person && evt
+ registrations_data << { person: person, event: evt, status: "registered" }
+end
+
+# --- Wellness Day gets extra registrations (popular free event, short form) ---
+if wellness_day
+ [ sarah_s, jessica_b, lisa_w, kim_d ].compact.each do |person|
+ registrations_data << { person: person, event: wellness_day, status: "registered" }
+ end
+end
+
+# Create all registrations
+registrations_data.each do |data|
+ next unless data[:event] && data[:person]
+ next if EventRegistration.exists?(event: data[:event], registrant: data[:person])
+
+ EventRegistration.create!(
+ event: data[:event],
+ registrant: data[:person],
+ status: data[:status] || "registered",
+ scholarship_recipient: data[:scholarship_recipient] || false,
+ scholarship_tasks_completed: data[:scholarship_tasks_completed] || false,
+ scholarship_requested: data[:scholarship_recipient] || false
+ )
+end
+
+puts "Creating Registration Form Submissions…"
+# Create person_form records linking registrants to their event's registration form.
+# This simulates people who filled out the registration form.
+form_submissions = []
+
+# Facilitator Training (extended form) — some registrants filled it out, one didn't
+if facilitator_training
+ reg_form = facilitator_training.registration_form
+ if reg_form
+ # People with users who filled out the form
+ [ amy_person, maria_j, anna_g ].compact.each do |person|
+ form_submissions << { person: person, form: reg_form }
+ end
+ # Mario Johnson (no user) did NOT fill out the form — registration without form submission
+ end
+
+ # Amy also filled out the scholarship form
+ scholarship_f = facilitator_training.scholarship_form
+ if scholarship_f && amy_person
+ form_submissions << { person: amy_person, form: scholarship_f }
+ end
+end
+
+# Trauma Training (extended form)
+if trauma_training
+ reg_form = trauma_training.registration_form
+ if reg_form
+ # Sarah Smith (has user) and Jessica Brown (has user) filled out forms
+ [ sarah_s, jessica_b ].compact.each do |person|
+ form_submissions << { person: person, form: reg_form }
+ end
+ # Angel Garcia (no user) filled out the form — person without user + form
+ form_submissions << { person: angel_g, form: reg_form } if angel_g
+ # Linda Williams (no user) did NOT fill out the form
+ end
+
+ # Jessica filled out the scholarship form
+ scholarship_f = trauma_training.scholarship_form
+ if scholarship_f && jessica_b
+ form_submissions << { person: jessica_b, form: scholarship_f }
+ end
+end
+
+# Wellness Day (short form) — most filled it out
+if wellness_day
+ reg_form = wellness_day.registration_form
+ if reg_form
+ # People with users
+ [ amy_person, maria_j, sarah_s, jessica_b, kim_d ].compact.each do |person|
+ form_submissions << { person: person, form: reg_form }
+ end
+ # Rosa (has user) filled it out too
+ form_submissions << { person: rosa_dlc, form: reg_form } if rosa_dlc
+ # Lisa Williams (has user) registered but didn't fill out the form — person with user + no form
+ end
+end
+
+# Mindful Art (short form, has scholarship) — Amy filled out both
+if mindful_art
+ reg_form = mindful_art.registration_form
+ form_submissions << { person: amy_person, form: reg_form } if reg_form && amy_person
+
+ scholarship_f = mindful_art.scholarship_form
+ form_submissions << { person: amy_person, form: scholarship_f } if scholarship_f && amy_person
+end
+
+# Youth Day (short form) — Maria filled it out
+if youth_day
+ reg_form = youth_day.registration_form
+ form_submissions << { person: maria_j, form: reg_form } if reg_form && maria_j
+end
+
+# Virtual Session (short form) — Amy (has user) registered but no form submission — person with user + no form
+# Family Day (short form) — Rosa filled it out
+if family_day
+ reg_form = family_day.registration_form
+ form_submissions << { person: rosa_dlc, form: reg_form } if reg_form && rosa_dlc
+end
+
+# Create all form submissions with sample field responses
+form_submissions.each do |data|
+ next unless data[:person] && data[:form]
+ next if PersonForm.exists?(person: data[:person], form: data[:form])
+
+ pf = PersonForm.create!(person: data[:person], form: data[:form])
+
+ # Fill in required text fields with sample data
+ data[:form].form_fields.where(answer_type: [ :free_form_input_one_line, :free_form_input_paragraph ]).each do |field|
+ sample_text = case field.field_key
+ when "first_name" then data[:person].first_name
+ when "last_name" then data[:person].last_name
+ when "primary_email", "enter_email", "confirm_email" then data[:person].preferred_email || "sample@example.com"
+ when "phone" then "(555) #{rand(100..999)}-#{rand(1000..9999)}"
+ when "street_address", "agency_street_address" then Faker::Address.street_address
+ when "city", "agency_city" then Faker::Address.city
+ when "state_province", "agency_state_province" then Faker::Address.state_abbr
+ when "zip_postal_code", "agency_zip_postal_code" then Faker::Address.zip_code
+ when "agency_organization_name" then Faker::Company.name
+ when "position_title" then "Facilitator"
+ when "agency_website" then "https://example.org"
+ when "racial_ethnic_identity" then "Prefer not to say"
+ when "secondary_email" then data[:person].email_2
+ when "preferred_nickname" then data[:person].first_name
+ when "pronouns" then [ "she/her", "he/him", "they/them" ].sample
+ else
+ if field.answer_type == "free_form_input_paragraph"
+ Faker::Lorem.paragraph(sentence_count: 3)
+ else
+ Faker::Lorem.word.capitalize
+ end
+ end
+
+ PersonFormFormField.create!(
+ person_form: pf,
+ form_field: field,
+ text: sample_text.to_s
+ )
+ end
+end
puts "Creating Resources…"
10.times do |i|
diff --git a/lib/domain_theme.rb b/lib/domain_theme.rb
index d188127fb..a9c155abf 100644
--- a/lib/domain_theme.rb
+++ b/lib/domain_theme.rb
@@ -18,6 +18,7 @@ module DomainTheme
categories: :lime,
category_types: :lime,
+ forms: :stone,
faqs: :pink,
tutorials: :cyan,
diff --git a/spec/factories/event_registration_organizations.rb b/spec/factories/event_registration_organizations.rb
new file mode 100644
index 000000000..624721636
--- /dev/null
+++ b/spec/factories/event_registration_organizations.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :event_registration_organization do
+ association :event_registration
+ association :organization
+ end
+end
diff --git a/spec/factories/forms.rb b/spec/factories/forms.rb
index 26d2b2570..5f5ee3e22 100644
--- a/spec/factories/forms.rb
+++ b/spec/factories/forms.rb
@@ -9,5 +9,16 @@
trait :with_owner do
association :owner, factory: :user
end
+
+ trait :with_fields do
+ after(:create) do |form|
+ create_list(:form_field, 3, form: form)
+ end
+ end
+
+ trait :scholarship do
+ scholarship_application { true }
+ name { "Scholarship Application" }
+ end
end
end
diff --git a/spec/factories/person_forms.rb b/spec/factories/person_forms.rb
new file mode 100644
index 000000000..39a87691b
--- /dev/null
+++ b/spec/factories/person_forms.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :person_form do
+ association :person
+ association :form
+ end
+end
diff --git a/spec/mailers/event_mailer_spec.rb b/spec/mailers/event_mailer_spec.rb
new file mode 100644
index 000000000..5ed83c5d2
--- /dev/null
+++ b/spec/mailers/event_mailer_spec.rb
@@ -0,0 +1,49 @@
+require "rails_helper"
+
+RSpec.describe EventMailer, type: :mailer do
+ describe "#event_registration_confirmation" do
+ let(:event_registration) { create(:event_registration) }
+ let(:mail) { described_class.event_registration_confirmation(event_registration) }
+
+ it "renders without raising" do
+ expect { mail.deliver_now }.not_to raise_error
+ end
+
+ it "sends to the registrant" do
+ expect(mail.to).to eq([ event_registration.registrant.preferred_email ])
+ end
+
+ it "includes the event title in the subject" do
+ expect(mail.subject).to include(event_registration.event.title)
+ end
+
+ it "includes the event title in the body" do
+ expect(mail.body.encoded).to include(event_registration.event.title)
+ end
+
+ it "includes the registrant name in the body" do
+ expect(mail.body.encoded).to include(event_registration.registrant.full_name)
+ end
+
+ context "when the event has a rhino_description" do
+ let(:event) { create(:event, rhino_description: "Join us for an art healing workshop") }
+ let(:event_registration) { create(:event_registration, event: event) }
+
+ it "includes the rhino_description in the body" do
+ expect(mail.body.encoded).to include("Join us for an art healing workshop")
+ end
+
+ it "includes the Details heading" do
+ expect(mail.body.encoded).to include("Details")
+ end
+ end
+
+ context "when the event has no rhino_description" do
+ let(:event_registration) { create(:event_registration) }
+
+ it "does not include the Details section" do
+ expect(mail.body.encoded).not_to include("Details")
+ end
+ end
+ end
+end
diff --git a/spec/models/event_registration_organization_spec.rb b/spec/models/event_registration_organization_spec.rb
new file mode 100644
index 000000000..385deb7fa
--- /dev/null
+++ b/spec/models/event_registration_organization_spec.rb
@@ -0,0 +1,14 @@
+require "rails_helper"
+
+RSpec.describe EventRegistrationOrganization, type: :model do
+ describe "associations" do
+ it { should belong_to(:event_registration).required }
+ it { should belong_to(:organization).required }
+ end
+
+ describe "validations" do
+ subject { create(:event_registration_organization) }
+
+ it { should validate_uniqueness_of(:organization_id).scoped_to(:event_registration_id) }
+ end
+end
diff --git a/spec/models/event_registration_spec.rb b/spec/models/event_registration_spec.rb
index 6079feacf..863bd77f0 100644
--- a/spec/models/event_registration_spec.rb
+++ b/spec/models/event_registration_spec.rb
@@ -7,6 +7,8 @@
it { should belong_to(:event).required }
it { should belong_to(:registrant).required }
it { should have_many(:comments).dependent(:destroy) }
+ it { should have_many(:event_registration_organizations).dependent(:destroy) }
+ it { should have_many(:organizations).through(:event_registration_organizations) }
end
describe "#active?" do
@@ -259,6 +261,52 @@
end
end
+ describe "snapshot_registrant_organizations" do
+ it "copies active affiliations to the registration on create" do
+ org = create(:organization)
+ person = create(:person)
+ create(:affiliation, person: person, organization: org)
+
+ reg = create(:event_registration, registrant: person)
+ expect(reg.organizations).to include(org)
+ end
+
+ it "copies multiple active affiliations" do
+ org1 = create(:organization)
+ org2 = create(:organization)
+ person = create(:person)
+ create(:affiliation, person: person, organization: org1)
+ create(:affiliation, person: person, organization: org2)
+
+ reg = create(:event_registration, registrant: person)
+ expect(reg.organizations).to contain_exactly(org1, org2)
+ end
+
+ it "skips inactive affiliations" do
+ org = create(:organization)
+ person = create(:person)
+ create(:affiliation, person: person, organization: org, inactive: true)
+
+ reg = create(:event_registration, registrant: person)
+ expect(reg.organizations).to be_empty
+ end
+
+ it "skips affiliations with past end dates" do
+ org = create(:organization)
+ person = create(:person)
+ create(:affiliation, person: person, organization: org, end_date: 1.day.ago)
+
+ reg = create(:event_registration, registrant: person)
+ expect(reg.organizations).to be_empty
+ end
+
+ it "creates no records when registrant has no affiliations" do
+ person = create(:person)
+ reg = create(:event_registration, registrant: person)
+ expect(reg.organizations).to be_empty
+ end
+ end
+
describe "slug" do
it "generates a slug on create" do
registration = create(:event_registration)
diff --git a/spec/models/tutorial_spec.rb b/spec/models/tutorial_spec.rb
index 1872dc491..3b444536c 100644
--- a/spec/models/tutorial_spec.rb
+++ b/spec/models/tutorial_spec.rb
@@ -2,8 +2,8 @@
RSpec.describe Tutorial, type: :model do
describe '.search_by_params' do
- let!(:published_tutorial) { create(:tutorial, :published, title: 'Getting Started Guide', body: 'Welcome to AWBW') }
- let!(:draft_tutorial) { create(:tutorial, title: 'Advanced Workshop Tips', body: 'For experienced facilitators') }
+ let!(:published_tutorial) { create(:tutorial, :published, title: 'Getting Started Guide', rhino_body: 'Welcome to AWBW') }
+ let!(:draft_tutorial) { create(:tutorial, title: 'Advanced Workshop Tips', rhino_body: 'For experienced facilitators') }
it 'returns all when no params' do
results = Tutorial.search_by_params({})
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 7e992aabf..b57c8ce3b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -267,6 +267,28 @@
end
end
+ describe "#deletable?" do
+ it "returns true when user has no created records" do
+ user = create(:user)
+ expect(user.deletable?).to be true
+ end
+
+ it "returns false when user has created reports" do
+ report = create(:report, workshop: create(:workshop))
+ expect(report.created_by.deletable?).to be false
+ end
+
+ it "returns false when user has created workshops" do
+ workshop = create(:workshop)
+ expect(workshop.created_by.deletable?).to be false
+ end
+
+ it "returns false when user has created resources" do
+ resource = create(:resource)
+ expect(resource.created_by.deletable?).to be false
+ end
+ end
+
describe '.search_by_params' do
let!(:admin_user) { create(:user, first_name: 'Alice', last_name: 'Admin', email: 'alice@example.com', super_user: true) }
let!(:regular_user) { create(:user, first_name: 'Bob', last_name: 'Regular', email: 'bob@example.com', super_user: false) }
diff --git a/spec/models/workshop_log_spec.rb b/spec/models/workshop_log_spec.rb
index c40d85b4f..4b2b9e5c8 100644
--- a/spec/models/workshop_log_spec.rb
+++ b/spec/models/workshop_log_spec.rb
@@ -9,7 +9,7 @@
describe 'associations' do
# Explicitly defined here
- it { should belong_to(:workshop) }
+ it { should belong_to(:workshop).optional }
it { should belong_to(:created_by) }
it { should belong_to(:organization) } # Inherited via Report but also explicit?
it { should have_many(:media_files) }
@@ -22,13 +22,21 @@
end
describe 'validations' do
- # Inherited from Report
- # Add specific WorkshopLog validations if any
- end
+ it "is valid with a workshop_id" do
+ workshop_log = build(:workshop_log)
+ expect(workshop_log).to be_valid
+ end
- it 'is valid with valid attributes' do
- # Note: Factory needs associations uncommented for create
- # expect(build(:workshop_log)).to be_valid
+ it "is valid with external_workshop_title and no workshop" do
+ workshop_log = build(:workshop_log, workshop: nil, owner: nil, external_workshop_title: "Community Art Workshop")
+ expect(workshop_log).to be_valid
+ end
+
+ it "is invalid without workshop_id or external_workshop_title" do
+ workshop_log = build(:workshop_log, workshop: nil, owner: nil, external_workshop_title: nil)
+ expect(workshop_log).not_to be_valid
+ expect(workshop_log.errors[:base]).to include("Please select a workshop or provide an external workshop title")
+ end
end
describe '#workshop_title' do
diff --git a/spec/policies/form_policy_spec.rb b/spec/policies/form_policy_spec.rb
new file mode 100644
index 000000000..694abb81a
--- /dev/null
+++ b/spec/policies/form_policy_spec.rb
@@ -0,0 +1,40 @@
+require "rails_helper"
+
+RSpec.describe FormPolicy, type: :policy do
+ let(:admin_user) { build_stubbed :user, super_user: true }
+ let(:regular_user) { build_stubbed :user, super_user: false }
+ let(:persisted_form) { create :form }
+ let(:new_form) { build :form }
+
+ def policy_for(record: nil, user:)
+ described_class.new(record, user: user)
+ end
+
+ describe "admin user" do
+ subject { policy_for(record: persisted_form, user: admin_user) }
+
+ it { is_expected.to be_allowed_to(:index?) }
+ it { is_expected.to be_allowed_to(:show?) }
+ it { is_expected.to be_allowed_to(:create?) }
+ it { is_expected.to be_allowed_to(:update?) }
+ it { is_expected.to be_allowed_to(:destroy?) }
+ end
+
+ describe "regular user" do
+ subject { policy_for(record: persisted_form, user: regular_user) }
+
+ it { is_expected.not_to be_allowed_to(:index?) }
+ it { is_expected.not_to be_allowed_to(:show?) }
+ it { is_expected.not_to be_allowed_to(:create?) }
+ it { is_expected.not_to be_allowed_to(:update?) }
+ it { is_expected.not_to be_allowed_to(:destroy?) }
+ end
+
+ describe "#destroy?" do
+ context "with non-persisted record" do
+ subject { policy_for(record: new_form, user: admin_user) }
+
+ it { is_expected.not_to be_allowed_to(:destroy?) }
+ end
+ end
+end
diff --git a/spec/policies/person_policy_spec.rb b/spec/policies/person_policy_spec.rb
index 41a49d194..faf0ae846 100644
--- a/spec/policies/person_policy_spec.rb
+++ b/spec/policies/person_policy_spec.rb
@@ -75,7 +75,7 @@ def policy_for(record: nil, user:)
context "with owner" do
subject { policy_for(record: owned_person, user: owner_user) }
- it { is_expected.to be_allowed_to(:edit?) }
+ it { is_expected.not_to be_allowed_to(:edit?) }
end
context "with regular user who is not the owner" do
diff --git a/spec/requests/event_registrations_spec.rb b/spec/requests/event_registrations_spec.rb
index 222681718..da600744f 100644
--- a/spec/requests/event_registrations_spec.rb
+++ b/spec/requests/event_registrations_spec.rb
@@ -49,6 +49,46 @@
expect(data_rows).to include(expected_row)
end
+ context "registration form icon" do
+ let(:reg_form) { create(:form, :standalone, name: "Registration Form") }
+ let(:person) { existing_registration.registrant }
+
+ it "shows green icon when person submitted the current registration form" do
+ create(:event_form, event: event, form: reg_form, role: "registration")
+ create(:person_form, person: person, form: reg_form)
+
+ get event_registrations_path
+
+ expect(response.body).to include("fa-solid fa-file-lines")
+ end
+
+ it "shows gray icon when person has not submitted any form" do
+ create(:event_form, event: event, form: reg_form, role: "registration")
+
+ get event_registrations_path
+
+ expect(response.body).to include("fa-regular fa-file-lines")
+ end
+
+ it "shows green icon when person submitted a non-registration form for the event" do
+ scholarship_form = create(:form, :standalone, name: "Scholarship Form")
+ create(:event_form, event: event, form: reg_form, role: "registration")
+ create(:event_form, event: event, form: scholarship_form, role: "scholarship")
+ create(:person_form, person: person, form: scholarship_form)
+
+ get event_registrations_path
+
+ expect(response.body).to include("fa-solid fa-file-lines")
+ expect(response.body).not_to include("fa-regular fa-file-lines")
+ end
+
+ it "does not show any form icon when event has no forms" do
+ get event_registrations_path
+
+ expect(response.body).not_to include("fa-file-lines")
+ end
+ end
+
it "paginates results" do
additional = create_list(:event_registration, 3)
@@ -68,7 +108,7 @@
it "creates registration and redirects admin to confirm page" do
expect {
post event_registrations_path,
- params: { event_registration: { event_id: event.id, registrant_id: admin.person.id } }
+ params: { return_to: "manage", event_registration: { event_id: event.id, registrant_id: admin.person.id } }
}.to change(EventRegistration, :count).by(1)
registration = EventRegistration.last
@@ -233,7 +273,7 @@
}
}.not_to change(EventRegistration, :count)
- expect(response).to redirect_to(manage_event_path(event))
+ expect(response).to redirect_to(event_registrations_path)
expect(flash[:alert]).to be_present
end
end
diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb
index cd30ac332..eac574b37 100644
--- a/spec/requests/events_spec.rb
+++ b/spec/requests/events_spec.rb
@@ -242,4 +242,54 @@
end
end
end
+
+ describe "GET /events/:id/manage" do
+ let(:person) { create(:person) }
+ let!(:registration) { create(:event_registration, event: event, registrant: person) }
+
+ before { sign_in admin }
+
+ context "registration form icon" do
+ let(:reg_form) { create(:form, :standalone, name: "Registration Form") }
+
+ it "shows green icon when person submitted the current registration form" do
+ create(:event_form, event: event, form: reg_form, role: "registration")
+ create(:person_form, person: person, form: reg_form)
+
+ get manage_event_path(event)
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include('fa-solid fa-file-lines')
+ end
+
+ it "shows gray icon when person has not submitted any form" do
+ create(:event_form, event: event, form: reg_form, role: "registration")
+
+ get manage_event_path(event)
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include('fa-regular fa-file-lines')
+ end
+
+ it "shows green icon when person submitted a non-registration form for the event" do
+ scholarship_form = create(:form, :standalone, name: "Scholarship Form")
+ create(:event_form, event: event, form: reg_form, role: "registration")
+ create(:event_form, event: event, form: scholarship_form, role: "scholarship")
+ create(:person_form, person: person, form: scholarship_form)
+
+ get manage_event_path(event)
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include('fa-solid fa-file-lines')
+ expect(response.body).not_to include('fa-regular fa-file-lines')
+ end
+
+ it "does not show any form icon when event has no forms" do
+ get manage_event_path(event)
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).not_to include('fa-file-lines')
+ end
+ end
+ end
end
diff --git a/spec/requests/forms_spec.rb b/spec/requests/forms_spec.rb
new file mode 100644
index 000000000..7050cba1a
--- /dev/null
+++ b/spec/requests/forms_spec.rb
@@ -0,0 +1,194 @@
+require "rails_helper"
+
+RSpec.describe "/forms", type: :request do
+ let(:admin) { create(:user, :admin) }
+ let(:regular_user) { create(:user) }
+ let!(:form) { create(:form, :standalone, name: "Test Registration Form") }
+
+ describe "GET /index" do
+ context "as an admin" do
+ before { sign_in admin }
+
+ it "renders successfully" do
+ get forms_path
+ expect(response).to be_successful
+ end
+
+ it "shows forms in the body" do
+ get forms_path
+ expect(response.body).to include("Test Registration Form")
+ end
+ end
+
+ context "as a non-admin user" do
+ before { sign_in regular_user }
+
+ it "redirects with unauthorized" do
+ get forms_path
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
+ context "as a guest" do
+ it "redirects to sign in" do
+ get forms_path
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ describe "GET /show" do
+ before { sign_in admin }
+
+ it "renders a successful response" do
+ get form_url(form)
+ expect(response).to be_successful
+ expect(response.body).to include("Test Registration Form")
+ end
+ end
+
+ describe "GET /new" do
+ before { sign_in admin }
+
+ it "renders the builder selection page" do
+ get new_form_url
+ expect(response).to be_successful
+ expect(response.body).to include("Short Event Registration")
+ expect(response.body).to include("Extended Event Registration")
+ expect(response.body).to include("Scholarship Application")
+ expect(response.body).to include("Generic Form")
+ end
+ end
+
+ describe "GET /edit" do
+ before { sign_in admin }
+
+ it "renders a successful response" do
+ get edit_form_url(form)
+ expect(response).to be_successful
+ end
+ end
+
+ describe "POST /create" do
+ before { sign_in admin }
+
+ it "creates a form with the short registration builder" do
+ expect {
+ post forms_url, params: { builder_type: "short_registration" }
+ }.to change(Form, :count).by(1)
+
+ new_form = Form.last
+ expect(new_form.name).to eq("Short Event Registration")
+ expect(new_form.form_fields).to be_present
+ expect(response).to redirect_to(edit_form_url(new_form))
+ end
+
+ it "creates a form with the extended registration builder" do
+ expect {
+ post forms_url, params: { builder_type: "extended_registration" }
+ }.to change(Form, :count).by(1)
+
+ new_form = Form.last
+ expect(new_form.name).to eq("Extended Event Registration")
+ expect(response).to redirect_to(edit_form_url(new_form))
+ end
+
+ it "creates a form with the scholarship builder" do
+ expect {
+ post forms_url, params: { builder_type: "scholarship_application" }
+ }.to change(Form, :count).by(1)
+
+ new_form = Form.last
+ expect(new_form.name).to eq("Scholarship Application")
+ expect(new_form.scholarship_application?).to be true
+ expect(response).to redirect_to(edit_form_url(new_form))
+ end
+
+ it "creates a generic form with a custom name" do
+ expect {
+ post forms_url, params: { builder_type: "generic", form_name: "My Custom Form" }
+ }.to change(Form, :count).by(1)
+
+ new_form = Form.last
+ expect(new_form.name).to eq("My Custom Form")
+ expect(new_form.form_fields).to be_empty
+ expect(response).to redirect_to(edit_form_url(new_form))
+ end
+
+ it "creates a generic form with default name when blank" do
+ post forms_url, params: { builder_type: "generic", form_name: "" }
+ expect(Form.last.name).to eq("New Form")
+ end
+ end
+
+ describe "PATCH /update" do
+ before { sign_in admin }
+
+ it "updates the form name" do
+ patch form_url(form), params: { form: { name: "Updated Form Name" } }
+ form.reload
+ expect(form.name).to eq("Updated Form Name")
+ expect(response).to redirect_to(form_url(form))
+ end
+
+ it "updates a nested field question label" do
+ field = create(:form_field, form: form, question: "Original Question")
+ patch form_url(form), params: {
+ form: {
+ form_fields_attributes: {
+ "0" => { id: field.id, question: "Updated Question" }
+ }
+ }
+ }
+ field.reload
+ expect(field.question).to eq("Updated Question")
+ end
+
+ it "removes a field via _destroy" do
+ field = create(:form_field, form: form)
+ expect {
+ patch form_url(form), params: {
+ form: {
+ form_fields_attributes: {
+ "0" => { id: field.id, _destroy: "1" }
+ }
+ }
+ }
+ }.to change(FormField, :count).by(-1)
+ end
+ end
+
+ describe "DELETE /destroy" do
+ before { sign_in admin }
+
+ it "destroys the requested form" do
+ form_to_delete = create(:form, :standalone)
+ expect {
+ delete form_url(form_to_delete)
+ }.to change(Form, :count).by(-1)
+ end
+
+ it "redirects to the forms list" do
+ delete form_url(form)
+ expect(response).to redirect_to(forms_url)
+ end
+ end
+
+ describe "POST /add_questions" do
+ before { sign_in admin }
+
+ it "clones questions from another form" do
+ source_form = create(:form, :standalone, name: "Source Form")
+ source_field = create(:form_field, form: source_form, question: "Source Q", field_key: "source_q")
+
+ expect {
+ post add_questions_form_url(form), params: { source_field_ids: [ source_field.id ] }
+ }.to change(form.form_fields, :count).by(1)
+
+ cloned = form.form_fields.reorder(position: :asc).last
+ expect(cloned.question).to eq("Source Q")
+ expect(cloned.field_key).to eq("source_q")
+ expect(response).to redirect_to(edit_form_url(form))
+ end
+ end
+end
diff --git a/spec/requests/people_authorization_spec.rb b/spec/requests/people_authorization_spec.rb
index ebef4a307..a23238cb2 100644
--- a/spec/requests/people_authorization_spec.rb
+++ b/spec/requests/people_authorization_spec.rb
@@ -63,4 +63,89 @@
end
end
end
+
+ describe "GET /people/:id show page content" do
+ context "as an admin" do
+ before do
+ sign_in admin
+ get person_path(other_person)
+ end
+
+ it "shows the Edit link" do
+ expect(response.body).to include("Edit")
+ end
+
+ it "shows email with admin-only styling when profile_show_email is off" do
+ other_person.update!(profile_show_email: false)
+ get person_path(other_person)
+ email = other_person.user&.email || other_person.email
+ expect(response.body).to include("admin-only")
+ expect(response.body).to include(email) if email.present?
+ end
+
+ it "shows the Submitted content section" do
+ expect(response.body).to include("Submitted content")
+ end
+
+ it "does not show the Comments section" do
+ expect(response.body).not_to include("comment_form")
+ end
+ end
+
+ context "as the owner" do
+ before do
+ sign_in regular_user
+ get person_path(regular_user.person)
+ end
+
+ it "does not show the Edit link" do
+ expect(response.body).not_to include(edit_person_path(regular_user.person))
+ end
+
+ it "shows the Submitted content section" do
+ expect(response.body).to include("Submitted content")
+ end
+ end
+ end
+
+ describe "GET /people/:id/edit" do
+ context "as the owner" do
+ before { sign_in regular_user }
+
+ it "redirects to root" do
+ get edit_person_path(regular_user.person)
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
+ context "as an admin" do
+ before { sign_in admin }
+
+ it "renders successfully" do
+ get edit_person_path(other_person)
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+
+ describe "PATCH /people/:id" do
+ context "as the owner" do
+ before { sign_in regular_user }
+
+ it "redirects to root" do
+ patch person_path(regular_user.person), params: { person: { first_name: "Changed" } }
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
+ context "as an admin" do
+ before { sign_in admin }
+
+ it "updates the person" do
+ patch person_path(other_person), params: { person: { first_name: "Updated" } }
+ expect(response).to redirect_to(person_path(other_person))
+ expect(other_person.reload.first_name).to eq("Updated")
+ end
+ end
+ end
end
diff --git a/spec/requests/people_profile_flags_spec.rb b/spec/requests/people_profile_flags_spec.rb
new file mode 100644
index 000000000..8408ad693
--- /dev/null
+++ b/spec/requests/people_profile_flags_spec.rb
@@ -0,0 +1,166 @@
+require "rails_helper"
+
+RSpec.describe "Person profile flag visibility", type: :request do
+ let(:admin) { create(:user, :admin) }
+ let(:owner_user) { create(:user, :with_person) }
+ let(:person) { owner_user.person }
+
+ before do
+ person.update!(
+ pronouns: "they/them",
+ bio: "A passionate facilitator",
+ linked_in_url: "https://linkedin.com/in/test"
+ )
+ owner_user.update!(phone: "555-123-4567")
+ create(:affiliation, person: person, title: "Facilitator", start_date: Date.new(2020, 1, 1))
+ end
+
+ # Flags whose content is visible to any viewer with show access
+ {
+ profile_show_pronouns: "they/them",
+ profile_show_member_since: "Facilitator since",
+ profile_show_phone: "555-123-4567",
+ profile_show_affiliations: "Affiliations",
+ profile_show_sectors: "mb-3\">Sectors",
+ profile_show_bio: "mb-3\">Bio",
+ profile_show_workshops: "mb-3\">Workshops authored",
+ profile_show_workshop_variations: "Workshop variations authored",
+ profile_show_stories: "Stories authored/featured",
+ profile_show_events_registered: "Participation history"
+ }.each do |flag, marker|
+ describe "##{flag}" do
+ context "when false" do
+ before { person.update!(flag => false) }
+
+ it "hides content on own profile" do
+ sign_in owner_user
+ get person_path(person)
+ expect(response.body).not_to include(marker)
+ end
+
+ it "hides content when admin views profile" do
+ sign_in admin
+ get person_path(person)
+ expect(response.body).not_to include(marker)
+ end
+ end
+
+ context "when true" do
+ before { person.update!(flag => true) }
+
+ it "shows content on own profile" do
+ sign_in owner_user
+ get person_path(person)
+ expect(response.body).to include(marker)
+ end
+
+ it "shows content when admin views profile" do
+ sign_in admin
+ get person_path(person)
+ expect(response.body).to include(marker)
+ end
+ end
+ end
+ end
+
+ describe "#profile_show_social_media" do
+ context "when false" do
+ before { person.update!(profile_show_social_media: false) }
+
+ it "hides social media on own profile" do
+ sign_in owner_user
+ get person_path(person)
+ expect(response.body).not_to include("fa-linkedin-in")
+ end
+ end
+
+ context "when true" do
+ before { person.update!(profile_show_social_media: true) }
+
+ it "shows social media on own profile" do
+ sign_in owner_user
+ get person_path(person)
+ expect(response.body).to include("fa-linkedin-in")
+ end
+
+ it "shows social media when admin views profile" do
+ sign_in admin
+ get person_path(person)
+ expect(response.body).to include("fa-linkedin-in")
+ end
+ end
+ end
+
+ describe "#profile_show_email" do
+ let(:email) { person.user.email }
+
+ context "when false" do
+ before { person.update!(profile_show_email: false) }
+
+ it "hides email on own profile" do
+ sign_in owner_user
+ get person_path(person)
+ expect(response.body).not_to include(email)
+ end
+
+ it "shows email with admin-only styling when admin views profile" do
+ sign_in admin
+ get person_path(person)
+ expect(response.body).to include(email)
+ expect(response.body).to include("admin-only bg-blue-100")
+ end
+ end
+
+ context "when true" do
+ before { person.update!(profile_show_email: true) }
+
+ it "shows email on own profile" do
+ sign_in owner_user
+ get person_path(person)
+ expect(response.body).to include(email)
+ end
+
+ it "shows email when admin views profile" do
+ sign_in admin
+ get person_path(person)
+ expect(response.body).to include(email)
+ end
+ end
+ end
+
+ # Flags inside the "Submitted content" section (gated by show? policy)
+ {
+ profile_show_workshop_ideas: "Workshop ideas submitted",
+ profile_show_workshop_variation_ideas: "Workshop variation ideas submitted",
+ profile_show_story_ideas: "Story ideas submitted",
+ profile_show_workshop_logs: "Workshop logs submitted"
+ }.each do |flag, marker|
+ describe "##{flag}" do
+ context "when false" do
+ before { person.update!(flag => false) }
+
+ it "hides content on own profile" do
+ sign_in owner_user
+ get person_path(person)
+ expect(response.body).not_to include(marker)
+ end
+ end
+
+ context "when true" do
+ before { person.update!(flag => true) }
+
+ it "shows content on own profile" do
+ sign_in owner_user
+ get person_path(person)
+ expect(response.body).to include(marker)
+ end
+
+ it "shows content when admin views profile" do
+ sign_in admin
+ get person_path(person)
+ expect(response.body).to include(marker)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/users_spec.rb b/spec/requests/users_spec.rb
index 3b3f22dfc..767c353d3 100644
--- a/spec/requests/users_spec.rb
+++ b/spec/requests/users_spec.rb
@@ -286,6 +286,15 @@
delete user_url(user)
expect(response).to redirect_to(users_url)
end
+
+ it "does not destroy a user with created records and shows alert" do
+ create(:workshop, created_by: user)
+ expect {
+ delete user_url(user)
+ }.not_to change(User, :count)
+ expect(response).to redirect_to(user_url(user))
+ expect(flash[:alert]).to be_present
+ end
end
context "as regular_user" do
diff --git a/spec/requests/workshop_ideas_spec.rb b/spec/requests/workshop_ideas_spec.rb
index 49ceae05c..1bd7af479 100644
--- a/spec/requests/workshop_ideas_spec.rb
+++ b/spec/requests/workshop_ideas_spec.rb
@@ -78,6 +78,18 @@
expect(response).to redirect_to(workshop_idea_path(WorkshopIdea.last))
end
+
+ it "handles RecordNotUnique gracefully" do
+ allow_any_instance_of(WorkshopIdea).to receive(:save).and_raise(
+ ActiveRecord::RecordNotUnique.new("Duplicate entry")
+ )
+
+ expect {
+ post workshop_ideas_path, params: { workshop_idea: valid_attributes }
+ }.not_to change(WorkshopIdea, :count)
+
+ expect(response).to have_http_status(:unprocessable_content)
+ end
end
describe "PATCH /update" do
@@ -90,6 +102,19 @@
expect(idea.reload.title).to eq("Updated Title")
expect(response).to redirect_to(workshop_idea_path(idea))
end
+
+ it "handles RecordNotUnique gracefully" do
+ idea = create(:workshop_idea, valid_attributes)
+
+ allow_any_instance_of(WorkshopIdea).to receive(:update).and_raise(
+ ActiveRecord::RecordNotUnique.new("Duplicate entry")
+ )
+
+ patch workshop_idea_path(idea),
+ params: { workshop_idea: { title: "Updated Title" } }
+
+ expect(response).to have_http_status(:unprocessable_content)
+ end
end
describe "DELETE /destroy" do
diff --git a/spec/requests/workshop_logs_spec.rb b/spec/requests/workshop_logs_spec.rb
index 2f75182ff..b7c7166a2 100644
--- a/spec/requests/workshop_logs_spec.rb
+++ b/spec/requests/workshop_logs_spec.rb
@@ -25,6 +25,23 @@
}
end
+ let(:external_title_attributes) do
+ {
+ date: Date.current,
+ workshop_id: nil,
+ external_workshop_title: "Community Art Workshop",
+ organization_id: organization.id,
+ windows_type_id: windows_type.id,
+ created_by_id: user.id,
+ children_first_time: 1,
+ children_ongoing: 2,
+ teens_first_time: 0,
+ teens_ongoing: 0,
+ adults_first_time: 0,
+ adults_ongoing: 3
+ }
+ end
+
let(:invalid_attributes) do
{
date: nil,
@@ -88,6 +105,19 @@
expect(response).to have_http_status(:redirect)
end
+ it "creates a WorkshopLog with external_workshop_title and no workshop" do
+ expect {
+ post workshop_logs_path, params: {
+ workshop_log: external_title_attributes
+ }
+ }.to change(WorkshopLog, :count).by(1)
+
+ log = WorkshopLog.last
+ expect(log.workshop_id).to be_nil
+ expect(log.external_workshop_title).to eq("Community Art Workshop")
+ expect(response).to have_http_status(:redirect)
+ end
+
it "creates admin and submitter notifications and enqueues mail" do
expect {
post workshop_logs_path, params: {
diff --git a/spec/requests/workshop_variation_ideas_spec.rb b/spec/requests/workshop_variation_ideas_spec.rb
index 8df894035..14a8593f0 100644
--- a/spec/requests/workshop_variation_ideas_spec.rb
+++ b/spec/requests/workshop_variation_ideas_spec.rb
@@ -109,6 +109,20 @@
expect(response).to have_http_status(:unprocessable_content)
end
end
+
+ context "when RecordNotUnique is raised" do
+ it "handles it gracefully" do
+ allow_any_instance_of(WorkshopVariationIdea).to receive(:save).and_raise(
+ ActiveRecord::RecordNotUnique.new("Duplicate entry")
+ )
+
+ expect {
+ post workshop_variation_ideas_path, params: { workshop_variation_idea: valid_attributes }
+ }.not_to change(WorkshopVariationIdea, :count)
+
+ expect(response).to have_http_status(:unprocessable_content)
+ end
+ end
end
describe "PATCH /update" do
@@ -121,6 +135,19 @@
expect(idea.reload.name).to eq("Updated Name")
expect(response).to redirect_to(workshop_variation_idea_path(idea))
end
+
+ it "handles RecordNotUnique gracefully" do
+ idea = create(:workshop_variation_idea, valid_attributes)
+
+ allow_any_instance_of(WorkshopVariationIdea).to receive(:update).and_raise(
+ ActiveRecord::RecordNotUnique.new("Duplicate entry")
+ )
+
+ patch workshop_variation_idea_path(idea),
+ params: { workshop_variation_idea: { name: "Updated Name" } }
+
+ expect(response).to have_http_status(:unprocessable_content)
+ end
end
describe "DELETE /destroy" do
diff --git a/spec/requests/workshops_spec.rb b/spec/requests/workshops_spec.rb
index 3ac3061db..254923873 100644
--- a/spec/requests/workshops_spec.rb
+++ b/spec/requests/workshops_spec.rb
@@ -100,4 +100,45 @@
end
end
end
+
+ # --- RECORD NOT UNIQUE HANDLING -----------------------------------------------
+ describe "RecordNotUnique handling" do
+ let(:admin) { create(:user, :admin) }
+
+ before { sign_in admin }
+
+ describe "POST /create" do
+ it "handles RecordNotUnique gracefully" do
+ allow_any_instance_of(Workshop).to receive(:save).and_raise(
+ ActiveRecord::RecordNotUnique.new("Duplicate entry")
+ )
+
+ expect {
+ post workshops_url, params: {
+ workshop: { title: "Test Workshop", category_ids: [ "" ] }
+ }
+ }.not_to change(Workshop, :count)
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Unable to save the workshop")
+ end
+ end
+
+ describe "PATCH /update" do
+ it "handles RecordNotUnique gracefully" do
+ workshop = create(:workshop)
+
+ allow_any_instance_of(Workshop).to receive(:update).and_raise(
+ ActiveRecord::RecordNotUnique.new("Duplicate entry")
+ )
+
+ patch workshop_url(workshop), params: {
+ workshop: { title: "Updated Title", category_ids: [ "" ] }
+ }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Unable to update the workshop")
+ end
+ end
+ end
end
diff --git a/spec/services/extended_event_registration_form_builder_spec.rb b/spec/services/extended_event_registration_form_builder_spec.rb
new file mode 100644
index 000000000..9fd7f7e21
--- /dev/null
+++ b/spec/services/extended_event_registration_form_builder_spec.rb
@@ -0,0 +1,111 @@
+require "rails_helper"
+
+RSpec.describe ExtendedEventRegistrationFormBuilder do
+ let(:event) { create(:event) }
+
+ describe ".build!" do
+ subject(:form) { described_class.build!(event) }
+
+ it "creates a registration form linked to the event" do
+ expect(form.name).to eq("Extended Event Registration")
+ expect(event.registration_form).to eq(form)
+ end
+
+ it "assigns sequential positions starting at 1" do
+ field_count = form.form_fields.count
+ positions = form.form_fields.unscoped.where(form: form).order(:position).pluck(:position)
+ expect(positions).to eq((1..field_count).to_a)
+ end
+
+ it "inherits basic contact fields from BaseRegistrationFormBuilder" do
+ %w[first_name last_name primary_email confirm_email].each do |key|
+ field = form.form_fields.find_by(field_key: key)
+ expect(field).to be_present, "expected field_key '#{key}' to exist"
+ expect(field.field_group).to eq("contact")
+ end
+ end
+
+ it "adds extended contact fields beyond the basic set" do
+ extended_keys = %w[nickname pronouns primary_email_type secondary_email secondary_email_type
+ mailing_street mailing_address_type mailing_city mailing_state mailing_zip
+ phone phone_type agency_name agency_position]
+ extended_keys.each do |key|
+ field = form.form_fields.find_by(field_key: key)
+ expect(field).to be_present, "expected field_key '#{key}' to exist"
+ end
+ end
+
+ it "creates workshop_environments field with correct key" do
+ field = form.form_fields.find_by(field_key: "workshop_environments")
+ expect(field).to be_present
+ expect(field.answer_type).to eq("multiple_choice_checkbox")
+ end
+
+ it "inherits consent fields from BaseRegistrationFormBuilder" do
+ field = form.form_fields.find_by(field_key: "communication_consent")
+ expect(field).to be_present
+ expect(field.answer_type).to eq("multiple_choice_radio")
+ expect(field.is_required).to be true
+ expect(field.field_group).to eq("consent")
+ end
+
+ it "inherits scholarship fields from BaseRegistrationFormBuilder" do
+ field = form.form_fields.find_by(field_key: "scholarship_eligibility")
+ expect(field).to be_present
+ expect(field.field_group).to eq("scholarship")
+ end
+
+ it "creates payment fields" do
+ attendees = form.form_fields.find_by(field_key: "number_of_attendees")
+ expect(attendees.answer_datatype).to eq("number_integer")
+ expect(attendees.is_required).to be true
+
+ payment = form.form_fields.find_by(field_key: "payment_method")
+ expect(payment.answer_type).to eq("multiple_choice_radio")
+ end
+
+ it "creates all expected field groups" do
+ groups = form.form_fields.pluck(:field_group).uniq.sort
+ expect(groups).to contain_exactly("background", "consent", "contact", "payment", "professional", "qualitative", "scholarship")
+ end
+ end
+
+ describe ".build! without contact fields" do
+ subject(:form) { described_class.build!(event, include_contact_fields: false) }
+
+ it "omits contact fields" do
+ expect(form.form_fields.find_by(field_key: "first_name")).to be_nil
+ expect(form.form_fields.find_by(field_key: "primary_email")).to be_nil
+ end
+
+ it "still includes consent fields" do
+ expect(form.form_fields.find_by(field_key: "communication_consent")).to be_present
+ end
+
+ it "still includes scholarship fields" do
+ expect(form.form_fields.find_by(field_key: "scholarship_eligibility")).to be_present
+ end
+ end
+
+ describe ".build_standalone!" do
+ it "creates a form without linking to an event" do
+ form = described_class.build_standalone!
+
+ expect(form.name).to eq("Extended Event Registration")
+ expect(form.event_forms).to be_empty
+ end
+ end
+
+ describe ".copy!" do
+ it "duplicates a form for a new event" do
+ source_form = described_class.build!(event)
+ new_event = create(:event)
+
+ copied = described_class.copy!(from_form: source_form, to_event: new_event)
+
+ expect(copied.id).not_to eq(source_form.id)
+ expect(copied.form_fields.count).to eq(source_form.form_fields.count)
+ expect(new_event.registration_form).to eq(copied)
+ end
+ end
+end
diff --git a/spec/services/short_event_registration_form_builder_spec.rb b/spec/services/short_event_registration_form_builder_spec.rb
new file mode 100644
index 000000000..dda53dbee
--- /dev/null
+++ b/spec/services/short_event_registration_form_builder_spec.rb
@@ -0,0 +1,61 @@
+require "rails_helper"
+
+RSpec.describe ShortEventRegistrationFormBuilder do
+ let(:event) { create(:event) }
+
+ describe ".build!" do
+ subject(:form) { described_class.build!(event) }
+
+ it "creates a registration form linked to the event" do
+ expect(form.name).to eq("Short Event Registration")
+ expect(event.registration_form).to eq(form)
+ end
+
+ it "assigns sequential positions starting at 1" do
+ field_count = form.form_fields.count
+ positions = form.form_fields.unscoped.where(form: form).order(:position).pluck(:position)
+ expect(positions).to eq((1..field_count).to_a)
+ end
+
+ it "inherits basic contact fields from BaseRegistrationFormBuilder" do
+ %w[first_name last_name primary_email confirm_email].each do |key|
+ field = form.form_fields.find_by(field_key: key)
+ expect(field).to be_present, "expected field_key '#{key}' to exist"
+ expect(field.field_group).to eq("contact")
+ end
+ end
+
+ it "inherits consent fields from BaseRegistrationFormBuilder" do
+ field = form.form_fields.find_by(field_key: "communication_consent")
+ expect(field).to be_present
+ expect(field.answer_type).to eq("multiple_choice_radio")
+ expect(field.is_required).to be true
+ expect(field.field_group).to eq("consent")
+ end
+
+ it "inherits scholarship fields from BaseRegistrationFormBuilder" do
+ field = form.form_fields.find_by(field_key: "scholarship_eligibility")
+ expect(field).to be_present
+ expect(field.field_group).to eq("scholarship")
+ end
+
+ it "creates short-specific qualitative fields" do
+ referral = form.form_fields.find_by(field_key: "referral_source")
+ expect(referral.answer_type).to eq("multiple_choice_checkbox")
+ expect(referral.is_required).to be true
+
+ interest = form.form_fields.find_by(field_key: "training_interest")
+ expect(interest.answer_type).to eq("multiple_choice_checkbox")
+ expect(interest.is_required).to be true
+ end
+ end
+
+ describe ".build_standalone!" do
+ it "creates a form without linking to an event" do
+ form = described_class.build_standalone!
+
+ expect(form.name).to eq("Short Event Registration")
+ expect(form.event_forms).to be_empty
+ end
+ end
+end
diff --git a/spec/system/event_registration_edit_spec.rb b/spec/system/event_registration_edit_spec.rb
new file mode 100644
index 000000000..59a14ff42
--- /dev/null
+++ b/spec/system/event_registration_edit_spec.rb
@@ -0,0 +1,22 @@
+require "rails_helper"
+
+RSpec.describe "Event registration edit page", type: :system do
+ let(:admin) { create(:user, :with_person, super_user: true) }
+ let(:event) { create(:event, :published, title: "Test Event") }
+ let!(:registration) { create(:event_registration, event: event, registrant: admin.person) }
+
+ describe "delete button" do
+ it "deletes the registration" do
+ sign_in(admin)
+ visit edit_event_registration_path(registration)
+
+ accept_confirm("Are you sure you want to delete?") do
+ click_on "Delete"
+ end
+
+ expect(page).to have_current_path(event_registrations_path)
+ expect(page).to have_text("Registration deleted")
+ expect(EventRegistration.exists?(registration.id)).to be false
+ end
+ end
+end
diff --git a/spec/system/navbar_avatar_updates_spec.rb b/spec/system/navbar_avatar_updates_spec.rb
index 8316952db..67640c044 100644
--- a/spec/system/navbar_avatar_updates_spec.rb
+++ b/spec/system/navbar_avatar_updates_spec.rb
@@ -1,7 +1,7 @@
require "rails_helper"
RSpec.describe "Navbar avatar behavior", type: :system do
- let(:user) { create(:user) }
+ let(:user) { create(:user, :admin) }
let!(:person) { create(:person, user: user) }
before do
diff --git a/spec/system/person_views_workshop_log_spec.rb b/spec/system/person_views_workshop_log_spec.rb
index e68d3777b..3de119e7c 100644
--- a/spec/system/person_views_workshop_log_spec.rb
+++ b/spec/system/person_views_workshop_log_spec.rb
@@ -57,6 +57,67 @@
end
end
+ describe "workshop log with external title and no workshop" do
+ let(:user) { create(:user) }
+ let!(:workshop_log) do
+ create(:workshop_log,
+ created_by: user,
+ organization: organization,
+ owner: nil,
+ workshop: nil,
+ windows_type: windows_type,
+ external_workshop_title: "Community Mural Project",
+ date: 1.day.ago)
+ end
+
+ before { sign_in user }
+
+ it "displays the external title in the heading" do
+ visit workshop_log_path(workshop_log)
+
+ expect(page).to have_css("h1", text: "Community Mural Project")
+ end
+
+ it "displays the external title next to the Workshop label" do
+ visit workshop_log_path(workshop_log)
+
+ workshop_div = find("span", text: "Workshop:").ancestor("div", match: :first)
+ expect(workshop_div).to have_text("Community Mural Project")
+ end
+ end
+
+ describe "workshop log with both workshop and external title" do
+ let(:user) { create(:user) }
+ let(:person) { create(:person, user: user) }
+ let!(:affiliation) { create(:affiliation, person: person, organization: organization) }
+ let!(:workshop_log) do
+ create(:workshop_log,
+ created_by: user,
+ organization: organization,
+ owner: workshop,
+ workshop: workshop,
+ windows_type: windows_type,
+ external_workshop_title: "Guest-led Session",
+ date: 1.day.ago)
+ end
+
+ before { sign_in user }
+
+ it "displays the workshop name in the heading" do
+ visit workshop_log_path(workshop_log)
+
+ expect(page).to have_css("h1", text: "Healing Through Art")
+ end
+
+ it "displays both the workshop chip and external title" do
+ visit workshop_log_path(workshop_log)
+
+ workshop_div = find("span", text: "Workshop:").ancestor("div", match: :first)
+ expect(workshop_div).to have_text("Healing Through Art")
+ expect(workshop_div).to have_text("Guest-led Session")
+ end
+ end
+
describe "as an admin" do
let(:admin) { create(:user, :admin) }
let(:person) { create(:person, user: admin) }
diff --git a/spec/system/workshops_spec.rb b/spec/system/workshops_spec.rb
index 544d3989d..5ee3b95e7 100644
--- a/spec/system/workshops_spec.rb
+++ b/spec/system/workshops_spec.rb
@@ -122,7 +122,6 @@
select adult_window.short_name, from: 'workshop_windows_type_id'
find("#workshop_published", visible: :all).check
find('#body-button').click
- fill_in 'workshop_full_name', with: 'Jane Doe'
click_on 'Submit'
diff --git a/spec/views/tutorials/show.html.erb_spec.rb b/spec/views/tutorials/show.html.erb_spec.rb
index 54f9806d5..af95e6dd2 100644
--- a/spec/views/tutorials/show.html.erb_spec.rb
+++ b/spec/views/tutorials/show.html.erb_spec.rb
@@ -9,6 +9,7 @@
assign(:tutorial, Tutorial.create!(
title: "Title",
body: "MyText",
+ rhino_body: "MyText",
featured: false,
published: false,
position: 2,
<%= f.input :created_by_id,
@@ -74,7 +76,7 @@
label: "Author credit preference",
hint: "Controls how the author's name is displayed" %>
-
+
<%= f.input :workshop_idea_id,
as: :select,
label: "Workshop Idea parent",
@@ -84,6 +86,24 @@
hint: "If this workshop was created from a Workshop Idea, select it here.",
input_html: { value: f.object.workshop_idea_id,
class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
+
+
+
+ <%= f.input :month, as: :select,
+ collection: Date::MONTHNAMES.compact.each_with_index.map { |m, i| [m, i + 1] },
+ selected: f.object.month || Date.today.month,
+ wrapper: false, label: false,
+ input_html: { class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
+ <%= f.input :year,
+ as: :integer,
+ wrapper: false,
+ label: false,
+ input_html: {
+ value: f.object.year || Date.today.year,
+ class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+ } %>
+
+
@@ -228,28 +248,6 @@
<%= rhino_editor(f, :extra_field) %>
<% end %>
- <% if allowed_to?(:manage?, Workshop) %>
-
- <% if params[:admin] == 'true' %>
-
-
-
-
- <% end %>
<% end %>
diff --git a/app/views/workshops/edit.html.erb b/app/views/workshops/edit.html.erb
index 68e8bf5c8..1794ed784 100644
--- a/app/views/workshops/edit.html.erb
+++ b/app/views/workshops/edit.html.erb
@@ -16,11 +16,5 @@
<%= render "form", workshop: @workshop %>
<%= render "shared/audit_info", resource: @workshop %>
- <%= f.input :month, as: :select,
- collection: Date::MONTHNAMES.compact.each_with_index.map { |m, i| [m, i + 1] },
- selected: f.object.month || Date.today.month,
- wrapper: false, label: false,
- input_html: { class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
-
- <%= f.input :year,
- as: :integer,
- wrapper: false,
- label: false,
- input_html: {
- value: f.object.year || Date.today.year,
- class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
- } %>
-
- <%= render "assets/form", owner: @workshop %>
- <%= turbo_frame_tag "editor_assets_lazy", src: edit_workshop_path(@workshop) do %>
- <%= render "shared/loading" %>
- <% end %>
- <% end %>
|