diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb new file mode 100644 index 000000000..e1ef8f0fa --- /dev/null +++ b/app/controllers/event_registrations_controller.rb @@ -0,0 +1,63 @@ +class EventRegistrationsController < ApplicationController + + def create + @event_registration = EventRegistration.new(event_registration_params) + if @event_registration.save + redirect_to @event_registration.event, notice: 'Successfully registered for the event.' + else + redirect_to @event_registration.event, alert: "Registration failed: #{@event_registration.errors.full_messages.join(', ')}" + end + end + + def bulk_create + event_ids = Array(params[:event_ids]).map(&:to_i).uniq + if event_ids.blank? + redirect_to events_path, alert: "Please select at least one event." + return + end + + attendee_attrs = { + first_name: current_user.first_name || current_user.email.split('@').first, + last_name: current_user.last_name || 'User', + email: current_user.email + } + + created = 0 + errors = [] + + Event.transaction do + event_ids.each do |event_id| + existing_registration = EventRegistration.where( + event_id: event_id, + email: attendee_attrs[:email] + ).first + + if existing_registration + errors << "Event '#{Event.find(event_id).title}': You are already registered for this event." + next + end + + reg = EventRegistration.new(attendee_attrs.merge(event_id: event_id)) + unless reg.save + errors << "Event '#{Event.find(event_id).title}': #{reg.errors.full_messages.to_sentence}" + else + created += 1 + end + end + + raise ActiveRecord::Rollback if errors.any? + end + + if errors.any? + redirect_to events_path, alert: errors.join("; ") + else + redirect_to events_path, notice: "Successfully registered for #{created} event#{'s' if created != 1}." + end + end + + private + + def event_registration_params + params.require(:event_registration).permit(:event_id, :first_name, :last_name, :email) + end +end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb new file mode 100644 index 000000000..cef223c84 --- /dev/null +++ b/app/controllers/events_controller.rb @@ -0,0 +1,67 @@ +class EventsController < ApplicationController + before_action :set_event, only: %i[ show edit update destroy ] + before_action :authorize_admin!, only: %i[ new edit update destroy ] + + def index + @events = Event.all + end + + def show + end + + def new + @event = Event.new + end + + def edit + end + + def create + @event = Event.new(event_params) + + respond_to do |format| + if @event.save + format.html { redirect_to @event, notice: "Event was successfully created." } + format.json { render :show, status: :created, location: @event } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @event.errors, status: :unprocessable_entity } + end + end + end + + def update + respond_to do |format| + if @event.update(event_params) + format.html { redirect_to @event, notice: "Event was successfully updated." } + format.json { render :show, status: :ok, location: @event } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @event.errors, status: :unprocessable_entity } + end + end + end + + def destroy + @event.destroy + + respond_to do |format| + format.html { redirect_to events_path, status: :see_other, notice: "Event was successfully destroyed." } + format.json { head :no_content } + end + end + + private + + def set_event + @event = Event.find(params[:id]) + end + + def event_params + params.require(:event).permit(:title, :description, :start_date, :end_date, :registration_close_date, :publicly_visible) + end + + def authorize_admin! + redirect_to events_path, alert: "You are not authorized to perform this action." unless current_admin + end +end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 000000000..e3de81f83 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,6 @@ +class Event < ApplicationRecord + has_many :event_registrations, dependent: :destroy + + validates_presence_of :title, :start_date, :end_date + validates_inclusion_of :publicly_visible, in: [true, false] +end diff --git a/app/models/event_registration.rb b/app/models/event_registration.rb new file mode 100644 index 000000000..4be81b66d --- /dev/null +++ b/app/models/event_registration.rb @@ -0,0 +1,7 @@ +class EventRegistration < ApplicationRecord + belongs_to :event + + validates_presence_of :first_name, :last_name, :email, :event_id + validates_uniqueness_of :email, scope: :event_id, message: 'is already registered for this event', case_sensitive: false + validates_format_of :email, with: URI::MailTo::EMAIL_REGEXP +end diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb new file mode 100644 index 000000000..569e115a7 --- /dev/null +++ b/app/views/events/_form.html.erb @@ -0,0 +1,29 @@ +<%= form_for(@event) do |f| %> + <%= render 'shared/errors', resource: @event if @event.errors.any? %> + +
<%= notice %>
+ +| + | Title | +Start Date | +End Date | +Actions | +
|---|---|---|---|---|
| + <%= check_box_tag "event_ids[]", event.id, false, class: "event-checkbox" %> + | +<%= event.title %> | +<%= event.start_date.strftime("%B %d, %Y") if event.start_date %> | +<%= event.end_date.strftime("%B %d, %Y") if event.end_date %> | ++ <%= link_to 'Show', event %> | + <%= link_to 'Edit', edit_event_path(event) %> | + <%= link_to 'Destroy', event, method: :delete, data: { confirm: 'Are you sure?' } %> + | +
<%= notice %>
+ ++ Title: + <%= @event.title %> +
+ ++ Description: + <%= @event.description %> +
+ ++ Start Date: + <%= @event.start_date.strftime("%B %d, %Y %I:%M %p") if @event.start_date.present? %> +
+ ++ End Date: + <%= @event.end_date.strftime("%B %d, %Y %I:%M %p") if @event.end_date.present? %> +
+ ++ Registration Close Date: + <%= @event.registration_close_date.strftime("%B %d, %Y %I:%M %p") if @event.registration_close_date.present? %> +
+ ++ Publicly Visible: + <%= @event.publicly_visible? ? 'Yes' : 'No' %> +
+ +<%= link_to 'Edit', edit_event_path(@event) %> | +<%= link_to 'Back', events_path %> diff --git a/config/routes.rb b/config/routes.rb index 298a2b0a2..2fcbae7c5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,6 +45,12 @@ resources :workshop_log_creation_wizard resources :workshop_logs, only: [:show, :edit, :new, :create, :update] + resources :events + resources :event_registrations, only: [:create] do + collection do + post :bulk_create + end + end resources :resources get 'stories', to: 'resources#stories' diff --git a/db/migrate/20250912133056_create_events.rb b/db/migrate/20250912133056_create_events.rb new file mode 100644 index 000000000..c7abfb42e --- /dev/null +++ b/db/migrate/20250912133056_create_events.rb @@ -0,0 +1,12 @@ +class CreateEvents < ActiveRecord::Migration[6.1] + def change + create_table :events do |t| + t.string "title" + t.text "description" + t.datetime "start_date" + t.datetime "end_date" + t.datetime "registration_close_date" + t.timestamps + end + end +end diff --git a/db/migrate/20250912155207_create_event_registrations.rb b/db/migrate/20250912155207_create_event_registrations.rb new file mode 100644 index 000000000..dec9b0d37 --- /dev/null +++ b/db/migrate/20250912155207_create_event_registrations.rb @@ -0,0 +1,12 @@ +class CreateEventRegistrations < ActiveRecord::Migration[6.1] + def change + create_table :event_registrations do |t| + t.string :first_name + t.string :last_name + t.string :email + t.references :event, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20250912173152_add_publicly_visible_to_events.rb b/db/migrate/20250912173152_add_publicly_visible_to_events.rb new file mode 100644 index 000000000..ac68bf972 --- /dev/null +++ b/db/migrate/20250912173152_add_publicly_visible_to_events.rb @@ -0,0 +1,5 @@ +class AddPubliclyVisibleToEvents < ActiveRecord::Migration[6.1] + def change + add_column :events, :publicly_visible, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index f591eaf1e..283924a3a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2,17 +2,17 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2025_09_12_144532) do +ActiveRecord::Schema.define(version: 2025_09_12_173152) do - create_table "admins", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "admins", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "first_name", default: "", null: false @@ -31,7 +31,7 @@ t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true end - create_table "age_ranges", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "age_ranges", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -39,14 +39,14 @@ t.index ["windows_type_id"], name: "index_age_ranges_on_windows_type_id" end - create_table "answer_options", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "answer_options", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.integer "order" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "attachments", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "attachments", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "owner_id" t.string "owner_type" t.datetime "created_at", null: false @@ -57,22 +57,22 @@ t.datetime "file_updated_at" end - create_table "banners", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "banners", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.text "content" t.boolean "show" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "bookmark_annotations", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "bookmark_annotations", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "bookmark_id" - t.text "annotation", limit: 16777215 + t.text "annotation", size: :medium t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["bookmark_id"], name: "index_bookmark_annotations_on_bookmark_id" end - create_table "bookmarks", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "bookmarks", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "user_id" t.string "bookmarkable_type" t.integer "bookmarkable_id" @@ -81,7 +81,7 @@ t.index ["user_id"], name: "index_bookmarks_on_user_id" end - create_table "categories", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "categories", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "metadatum_id" t.string "name" t.integer "legacy_id" @@ -91,7 +91,7 @@ t.index ["metadatum_id"], name: "index_categories_on_metadatum_id" end - create_table "categorizable_items", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "categorizable_items", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "categorizable_id" t.string "categorizable_type" t.integer "category_id" @@ -102,7 +102,7 @@ t.index ["category_id"], name: "index_categorizable_items_on_category_id" end - create_table "ckeditor_assets", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "ckeditor_assets", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "data_file_name", null: false t.string "data_content_type" t.integer "data_file_size" @@ -118,7 +118,28 @@ t.index ["assetable_type", "type", "assetable_id"], name: "idx_ckeditor_assetable_type" end - create_table "facilitators", force: :cascade do |t| + create_table "event_registrations", charset: "utf8mb3", force: :cascade do |t| + t.string "first_name" + t.string "last_name" + t.string "email" + t.bigint "event_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["event_id"], name: "index_event_registrations_on_event_id" + end + + create_table "events", charset: "utf8mb3", force: :cascade do |t| + t.string "title" + t.text "description" + t.datetime "start_date" + t.datetime "end_date" + t.datetime "registration_close_date" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.boolean "publicly_visible", default: false, null: false + end + + create_table "facilitators", charset: "utf8mb3", force: :cascade do |t| t.string "first_name", null: false t.string "last_name", null: false t.string "email", null: false @@ -126,16 +147,16 @@ t.datetime "updated_at", precision: 6, null: false end - create_table "faqs", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "faqs", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "question" - t.text "answer", limit: 16777215 + t.text "answer", size: :medium t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "inactive" t.integer "ordering" end - create_table "footers", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "footers", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "phone" t.string "children_program" t.string "adult_program" @@ -144,17 +165,17 @@ t.datetime "updated_at", null: false end - create_table "form_builders", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "form_builders", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.integer "owner_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "description", limit: 16777215 + t.text "description", size: :medium t.integer "windows_type_id" t.index ["windows_type_id"], name: "index_form_builders_on_windows_type_id" end - create_table "form_field_answer_options", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "form_field_answer_options", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "form_field_id" t.integer "answer_option_id" t.datetime "created_at", null: false @@ -163,7 +184,7 @@ t.index ["form_field_id"], name: "index_form_field_answer_options_on_form_field_id" end - create_table "form_fields", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "form_fields", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "form_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -178,7 +199,7 @@ t.index ["form_id"], name: "index_form_fields_on_form_id" end - create_table "forms", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "forms", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "owner_type" t.integer "owner_id" t.datetime "created_at", null: false @@ -187,7 +208,7 @@ t.index ["form_builder_id"], name: "index_forms_on_form_builder_id" end - create_table "images", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "images", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "owner_id" t.string "owner_type" t.datetime "created_at", null: false @@ -200,7 +221,7 @@ t.index ["owner_id"], name: "index_images_on_owner_id" end - create_table "locations", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "locations", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "city" t.string "state" t.string "country" @@ -208,7 +229,7 @@ t.datetime "updated_at", null: false end - create_table "media_files", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "media_files", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "file_file_name" t.string "file_content_type" t.integer "file_file_size" @@ -217,7 +238,7 @@ t.integer "workshop_log_id" end - create_table "metadata", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "metadata", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.string "legacy_id" t.datetime "created_at", null: false @@ -225,7 +246,7 @@ t.boolean "published", default: false end - create_table "monthly_reports", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "monthly_reports", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "month" t.integer "project_id" t.integer "project_user_id" @@ -234,11 +255,11 @@ t.boolean "mail_evaluations" t.string "num_ongoing_participants" t.string "num_new_participants" - t.text "most_effective", limit: 16777215 - t.text "most_challenging", limit: 16777215 - t.text "goals_reached", limit: 16777215 - t.text "goals", limit: 16777215 - t.text "comments", limit: 16777215 + t.text "most_effective", size: :medium + t.text "most_challenging", size: :medium + t.text "goals_reached", size: :medium + t.text "goals", size: :medium + t.text "comments", size: :medium t.boolean "call_requested" t.string "best_call_time" t.string "phone" @@ -248,7 +269,7 @@ t.index ["project_user_id"], name: "index_monthly_reports_on_project_user_id" end - create_table "notifications", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "notifications", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "notification_type" @@ -256,26 +277,26 @@ t.integer "noticeable_id" end - create_table "permissions", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "permissions", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "security_cat" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "legacy_id" end - create_table "project_obligations", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "project_obligations", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "project_statuses", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "project_statuses", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "project_users", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "project_users", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "agency_id" t.integer "user_id" t.integer "position" @@ -288,7 +309,7 @@ t.index ["user_id"], name: "index_project_users_on_user_id" end - create_table "projects", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "projects", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.integer "location_id" t.datetime "created_at", null: false @@ -298,8 +319,8 @@ t.date "start_date" t.date "end_date" t.string "locality" - t.text "description", limit: 16777215 - t.text "notes", limit: 16777215 + t.text "description", size: :medium + t.text "notes", size: :medium t.string "filemaker_code" t.boolean "inactive", default: false t.integer "legacy_id" @@ -310,7 +331,7 @@ t.index ["windows_type_id"], name: "index_projects_on_windows_type_id" end - create_table "quotable_item_quotes", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "quotable_item_quotes", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "quotable_type" t.integer "quotable_id" t.integer "legacy_id" @@ -320,8 +341,8 @@ t.index ["quote_id"], name: "index_quotable_item_quotes_on_quote_id" end - create_table "quotes", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| - t.text "quote", limit: 16777215 + create_table "quotes", id: :integer, charset: "utf8mb3", force: :cascade do |t| + t.text "quote", size: :medium t.boolean "inactive", default: true t.integer "legacy_id" t.boolean "legacy", default: false @@ -334,10 +355,10 @@ t.index ["workshop_id"], name: "index_quotes_on_workshop_id" end - create_table "report_form_field_answers", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "report_form_field_answers", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "report_id" t.integer "form_field_id" - t.text "answer", limit: 16777215 + t.text "answer", size: :medium t.datetime "created_at" t.datetime "updated_at" t.integer "answer_option_id" @@ -346,7 +367,7 @@ t.index ["report_id"], name: "index_report_form_field_answers_on_report_id" end - create_table "reports", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "reports", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "type" t.integer "owner_id" t.string "owner_type" @@ -376,11 +397,11 @@ t.index ["windows_type_id"], name: "index_reports_on_windows_type_id" end - create_table "resources", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "resources", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "title" t.string "author" t.integer "user_id" - t.text "text", limit: 16777215 + t.text "text", size: :medium t.boolean "featured", default: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -401,7 +422,7 @@ t.index ["workshop_id"], name: "index_resources_on_workshop_id" end - create_table "sectorable_items", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "sectorable_items", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "sectorable_id" t.string "sectorable_type" t.integer "sector_id" @@ -411,24 +432,24 @@ t.index ["sector_id"], name: "index_sectorable_items_on_sector_id" end - create_table "sectors", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "sectors", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "published", default: false end - create_table "user_form_form_fields", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "user_form_form_fields", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "form_field_id" t.integer "user_form_id" - t.text "text", limit: 16777215 + t.text "text", size: :medium t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["form_field_id"], name: "index_user_form_form_fields_on_form_field_id" t.index ["user_form_id"], name: "index_user_form_form_fields_on_user_form_id" end - create_table "user_forms", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "user_forms", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "user_id" t.integer "form_id" t.datetime "created_at", null: false @@ -437,7 +458,7 @@ t.index ["user_id"], name: "index_user_forms_on_user_id" end - create_table "user_permissions", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "user_permissions", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "user_id" t.integer "permission_id" t.datetime "created_at", null: false @@ -446,7 +467,7 @@ t.index ["user_id"], name: "index_user_permissions_on_user_id" end - create_table "users", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "users", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "first_name", default: "" @@ -469,8 +490,8 @@ t.string "zip" t.date "birthday" t.string "subscribecode" - t.text "comment", limit: 16777215 - t.text "notes", limit: 16777215 + t.text "comment", size: :medium + t.text "notes", size: :medium t.boolean "legacy", default: false t.boolean "inactive", default: false t.boolean "confirmed", default: true @@ -495,7 +516,7 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end - create_table "windows_types", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "windows_types", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -503,7 +524,7 @@ t.string "short_name" end - create_table "workshop_age_ranges", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "workshop_age_ranges", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "workshop_id" t.integer "age_range_id" t.datetime "created_at", null: false @@ -512,20 +533,20 @@ t.index ["workshop_id"], name: "index_workshop_age_ranges_on_workshop_id" end - create_table "workshop_logs", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "workshop_logs", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "workshop_id" t.integer "user_id" t.date "date" t.integer "rating", default: 0 - t.text "reaction", limit: 16777215 - t.text "successes", limit: 16777215 - t.text "challenges", limit: 16777215 - t.text "suggestions", limit: 16777215 - t.text "questions", limit: 16777215 + t.text "reaction", size: :medium + t.text "successes", size: :medium + t.text "challenges", size: :medium + t.text "suggestions", size: :medium + t.text "questions", size: :medium t.boolean "lead_similar" - t.text "similarities", limit: 16777215 - t.text "differences", limit: 16777215 - t.text "comments", limit: 16777215 + t.text "similarities", size: :medium + t.text "differences", size: :medium + t.text "comments", size: :medium t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "project_id" @@ -537,7 +558,7 @@ t.index ["workshop_id"], name: "index_workshop_logs_on_workshop_id" end - create_table "workshop_resources", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "workshop_resources", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "workshop_id" t.integer "resource_id" t.datetime "created_at", null: false @@ -546,11 +567,11 @@ t.index ["workshop_id"], name: "index_workshop_resources_on_workshop_id" end - create_table "workshop_variations", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "workshop_variations", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "workshop_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "code", limit: 16777215 + t.text "code", size: :medium t.boolean "inactive", default: true t.integer "ordering" t.string "name" @@ -559,27 +580,27 @@ t.index ["workshop_id"], name: "index_workshop_variations_on_workshop_id" end - create_table "workshops", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t| + create_table "workshops", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "title" t.string "full_name" t.string "author_location" t.integer "month" t.integer "year" - t.text "objective", limit: 16777215 - t.text "materials", limit: 16777215 - t.text "timeframe", limit: 16777215 - t.text "age_range", limit: 16777215 - t.text "setup", limit: 16777215 - t.text "instructions", limit: 16777215 - t.text "warm_up", limit: 16777215 - t.text "creation", limit: 16777215 - t.text "closing", limit: 16777215 - t.text "misc_instructions", limit: 16777215 - t.text "project", limit: 16777215 - t.text "description", limit: 16777215 - t.text "notes", limit: 16777215 - t.text "timestamps", limit: 16777215 - t.text "tips", limit: 16777215 + t.text "objective", size: :medium + t.text "materials", size: :medium + t.text "timeframe", size: :medium + t.text "age_range", size: :medium + t.text "setup", size: :medium + t.text "instructions", size: :medium + t.text "warm_up", size: :medium + t.text "creation", size: :medium + t.text "closing", size: :medium + t.text "misc_instructions", size: :medium + t.text "project", size: :medium + t.text "description", size: :medium + t.text "notes", size: :medium + t.text "timestamps", size: :medium + t.text "tips", size: :medium t.string "pub_issue" t.string "misc1" t.string "misc2" @@ -595,36 +616,36 @@ t.integer "windows_type_id" t.integer "user_id" t.integer "led_count", default: 0 - t.text "objective_spanish", limit: 16777215 - t.text "materials_spanish", limit: 16777215 - t.text "timeframe_spanish", limit: 16777215 - t.text "age_range_spanish", limit: 16777215 - t.text "setup_spanish", limit: 16777215 - t.text "instructions_spanish", limit: 16777215 - t.text "project_spanish", limit: 16777215 - t.text "warm_up_spanish", limit: 16777215 - t.text "creation_spanish", limit: 16777215 - t.text "closing_spanish", limit: 16777215 - t.text "misc_instructions_spanish", limit: 16777215 - t.text "description_spanish", limit: 16777215 - t.text "notes_spanish", limit: 16777215 - t.text "tips_spanish", limit: 16777215 + t.text "objective_spanish", size: :medium + t.text "materials_spanish", size: :medium + t.text "timeframe_spanish", size: :medium + t.text "age_range_spanish", size: :medium + t.text "setup_spanish", size: :medium + t.text "instructions_spanish", size: :medium + t.text "project_spanish", size: :medium + t.text "warm_up_spanish", size: :medium + t.text "creation_spanish", size: :medium + t.text "closing_spanish", size: :medium + t.text "misc_instructions_spanish", size: :medium + t.text "description_spanish", size: :medium + t.text "notes_spanish", size: :medium + t.text "tips_spanish", size: :medium t.string "thumbnail_file_name" t.string "thumbnail_content_type" t.integer "thumbnail_file_size" t.datetime "thumbnail_updated_at" - t.text "optional_materials", limit: 16777215 - t.text "optional_materials_spanish", limit: 16777215 - t.text "introduction", limit: 16777215 - t.text "introduction_spanish", limit: 16777215 - t.text "demonstration", limit: 16777215 - t.text "demonstration_spanish", limit: 16777215 - t.text "opening_circle", limit: 16777215 - t.text "opening_circle_spanish", limit: 16777215 - t.text "visualization", limit: 16777215 - t.text "visualization_spanish", limit: 16777215 - t.text "misc1_spanish", limit: 16777215 - t.text "misc2_spanish", limit: 16777215 + t.text "optional_materials", size: :medium + t.text "optional_materials_spanish", size: :medium + t.text "introduction", size: :medium + t.text "introduction_spanish", size: :medium + t.text "demonstration", size: :medium + t.text "demonstration_spanish", size: :medium + t.text "opening_circle", size: :medium + t.text "opening_circle_spanish", size: :medium + t.text "visualization", size: :medium + t.text "visualization_spanish", size: :medium + t.text "misc1_spanish", size: :medium + t.text "misc2_spanish", size: :medium t.integer "time_intro" t.integer "time_demonstration" t.integer "time_warm_up" @@ -647,6 +668,7 @@ add_foreign_key "bookmark_annotations", "bookmarks" add_foreign_key "bookmarks", "users" add_foreign_key "categories", "metadata" + add_foreign_key "event_registrations", "events" add_foreign_key "form_builders", "windows_types" add_foreign_key "form_field_answer_options", "answer_options" add_foreign_key "form_field_answer_options", "form_fields" diff --git a/spec/factories/event_registrations.rb b/spec/factories/event_registrations.rb new file mode 100644 index 000000000..2ef2dcbde --- /dev/null +++ b/spec/factories/event_registrations.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :event_registration do + first_name { "John" } + last_name { "Doe" } + email { "john.doe@example.com" } + association :event + end +end diff --git a/spec/factories/events.rb b/spec/factories/events.rb new file mode 100644 index 000000000..5ed795ce2 --- /dev/null +++ b/spec/factories/events.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :event do + title { "sample title" } + description { "sample description" } + start_date { 1.day.from_now } + end_date { 2.days.from_now } + registration_close_date { 3.days.ago } + publicly_visible { true } + end +end diff --git a/spec/models/event_registration_spec.rb b/spec/models/event_registration_spec.rb new file mode 100644 index 000000000..83fe597c8 --- /dev/null +++ b/spec/models/event_registration_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +RSpec.describe EventRegistration, type: :model do + subject { build(:event_registration) } + + describe 'associations' do + it { should belong_to(:event) } + end + + describe 'validations' do + it { should validate_presence_of(:event_id) } + it { should validate_presence_of(:first_name) } + it { should validate_presence_of(:last_name) } + it { should validate_presence_of(:email) } + it { should validate_uniqueness_of(:email).scoped_to(:event_id).with_message('is already registered for this event').case_insensitive } + it { should allow_value('user@example.com').for(:email) } + it { should_not allow_value('invalid_email').for(:email) } + end +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb new file mode 100644 index 000000000..73d025b44 --- /dev/null +++ b/spec/models/event_spec.rb @@ -0,0 +1,9 @@ +require 'rails_helper' + +RSpec.describe Event, type: :model do + describe 'validations' do + it { should validate_presence_of(:title) } + it { should validate_presence_of(:start_date) } + it { should validate_presence_of(:end_date) } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index cd74f7d39..734183c80 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -46,7 +46,7 @@ # examples within a transaction, remove the following line or assign false # instead of true. config.use_transactional_fixtures = true - + config.include Devise::Test::IntegrationHelpers, type: :request # You can uncomment this line to turn off ActiveRecord support entirely. # config.use_active_record = false diff --git a/spec/requests/event_registrations_spec.rb b/spec/requests/event_registrations_spec.rb new file mode 100644 index 000000000..043333d3f --- /dev/null +++ b/spec/requests/event_registrations_spec.rb @@ -0,0 +1,294 @@ +require 'rails_helper' + +RSpec.describe "EventRegistrations", type: :request do + let(:user) { create(:user, first_name: "John", last_name: "Doe", email: "john.doe@example.com") } + let(:event) { create(:event, title: "Test Event") } + let(:valid_attributes) do + { + event_id: event.id, + first_name: "Jane", + last_name: "Smith", + email: "jane.smith@example.com" + } + end + let(:invalid_attributes) do + { + event_id: event.id, + first_name: "", + last_name: "", + email: "invalid-email" + } + end + + before do + sign_in user + end + + describe "POST /event_registrations" do + context "with valid parameters" do + it "creates a new EventRegistration" do + expect { + post event_registrations_path, params: { event_registration: valid_attributes } + }.to change(EventRegistration, :count).by(1) + end + + it "redirects to the event page with success notice" do + post event_registrations_path, params: { event_registration: valid_attributes } + expect(response).to redirect_to(event_path(event)) + expect(flash[:notice]).to eq('Successfully registered for the event.') + end + + it "creates registration with correct attributes" do + post event_registrations_path, params: { event_registration: valid_attributes } + + registration = EventRegistration.last + expect(registration.event_id).to eq(event.id) + expect(registration.first_name).to eq("Jane") + expect(registration.last_name).to eq("Smith") + expect(registration.email).to eq("jane.smith@example.com") + end + end + + context "with invalid parameters" do + it "does not create a new EventRegistration" do + expect { + post event_registrations_path, params: { event_registration: invalid_attributes } + }.to change(EventRegistration, :count).by(0) + end + + it "redirects to the event page with error" do + post event_registrations_path, params: { event_registration: invalid_attributes } + expect(response).to redirect_to(event_path(event)) + follow_redirect! + expect(response.body).to include("Registration failed") + end + end + + context "when user is not authenticated" do + before { sign_out user } + + it "redirects to sign in page" do + post event_registrations_path, params: { event_registration: valid_attributes } + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when trying to register for the same event twice" do + before do + create(:event_registration, event: event, email: valid_attributes[:email]) + end + + it "does not create a duplicate registration" do + expect { + post event_registrations_path, params: { event_registration: valid_attributes } + }.to change(EventRegistration, :count).by(0) + end + + it "redirects to the event page with error" do + post event_registrations_path, params: { event_registration: valid_attributes } + expect(response).to redirect_to(event_path(event)) + follow_redirect! + expect(response.body).to include("Registration failed") + end + end + end + + describe "POST /event_registrations/bulk_create" do + let(:event1) { create(:event, title: "Event 1") } + let(:event2) { create(:event, title: "Event 2") } + let(:event3) { create(:event, title: "Event 3") } + + context "with valid event IDs" do + it "creates multiple EventRegistrations" do + expect { + post bulk_create_event_registrations_path, params: { event_ids: [event1.id, event2.id] } + }.to change(EventRegistration, :count).by(2) + end + + it "redirects to events page with success notice" do + post bulk_create_event_registrations_path, params: { event_ids: [event1.id, event2.id] } + expect(response).to redirect_to(events_path) + expect(flash[:notice]).to eq("Successfully registered for 2 events.") + end + + it "creates registrations with user information" do + post bulk_create_event_registrations_path, params: { event_ids: [event1.id] } + + registration = EventRegistration.last + expect(registration.event_id).to eq(event1.id) + expect(registration.first_name).to eq("John") + expect(registration.last_name).to eq("Doe") + expect(registration.email).to eq("john.doe@example.com") + end + + it "handles single event registration" do + post bulk_create_event_registrations_path, params: { event_ids: [event1.id] } + expect(response).to redirect_to(events_path) + expect(flash[:notice]).to eq("Successfully registered for 1 event.") + end + + it "removes duplicate event IDs" do + expect { + post bulk_create_event_registrations_path, params: { event_ids: [event1.id, event1.id, event2.id] } + }.to change(EventRegistration, :count).by(2) + end + end + + context "with no event IDs" do + it "redirects with alert when event_ids is nil" do + post bulk_create_event_registrations_path, params: {} + expect(response).to redirect_to(events_path) + expect(flash[:alert]).to eq("Please select at least one event.") + end + end + + context "when user is already registered for some events" do + before do + create(:event_registration, event: event1, email: user.email) + end + + it "does not create any registrations due to rollback" do + expect { + post bulk_create_event_registrations_path, params: { event_ids: [event1.id, event2.id] } + }.to change(EventRegistration, :count).by(0) + end + + it "redirects with alert about already registered events" do + post bulk_create_event_registrations_path, params: { event_ids: [event1.id, event2.id] } + expect(response).to redirect_to(events_path) + expect(flash[:alert]).to include("Event 'Event 1': You are already registered for this event.") + end + end + + context "when user has no first_name or last_name" do + let(:user_without_names) { create(:user, first_name: nil, last_name: nil, email: "test@example.com") } + + before { sign_in user_without_names } + + it "uses email prefix as first name and 'User' as last name" do + post bulk_create_event_registrations_path, params: { event_ids: [event1.id] } + + registration = EventRegistration.last + expect(registration.first_name).to eq("test") + expect(registration.last_name).to eq("User") + expect(registration.email).to eq("test@example.com") + end + end + + context "when user has only first_name" do + let(:user_with_first_name) { create(:user, first_name: "Alice", last_name: nil, email: "alice@example.com") } + + before { sign_in user_with_first_name } + + it "uses provided first name and 'User' as last name" do + post bulk_create_event_registrations_path, params: { event_ids: [event1.id] } + + registration = EventRegistration.last + expect(registration.first_name).to eq("Alice") + expect(registration.last_name).to eq("User") + expect(registration.email).to eq("alice@example.com") + end + end + + context "when registration validation fails" do + before do + allow_any_instance_of(EventRegistration).to receive(:save).and_return(false) + allow_any_instance_of(EventRegistration).to receive(:errors).and_return( + double(full_messages: ["Email is invalid"]) + ) + end + + it "redirects with error message" do + post bulk_create_event_registrations_path, params: { event_ids: [event1.id] } + expect(response).to redirect_to(events_path) + expect(flash[:alert]).to include("Email is invalid") + end + + it "does not create any registrations when validation fails" do + expect { + post bulk_create_event_registrations_path, params: { event_ids: [event1.id, event2.id] } + }.to change(EventRegistration, :count).by(0) + end + end + + context "when user is not authenticated" do + before { sign_out user } + + it "redirects to sign in page" do + post bulk_create_event_registrations_path, params: { event_ids: [event1.id] } + expect(response).to redirect_to(new_user_session_path) + end + end + + context "with mixed valid and invalid scenarios" do + before do + create(:event_registration, event: event1, email: user.email) + allow_any_instance_of(EventRegistration).to receive(:save).and_return(false) + allow_any_instance_of(EventRegistration).to receive(:errors).and_return( + double(full_messages: ["Some validation error"]) + ) + end + + it "handles mixed scenarios correctly" do + expect { + post bulk_create_event_registrations_path, params: { event_ids: [event1.id, event2.id, event3.id] } + }.to change(EventRegistration, :count).by(0) + + expect(response).to redirect_to(events_path) + expect(flash[:alert]).to include("Event 'Event 1': You are already registered for this event.") + expect(flash[:alert]).to include("Some validation error") + end + end + + context "with string event IDs" do + it "converts string IDs to integers" do + expect { + post bulk_create_event_registrations_path, params: { event_ids: [event1.id.to_s, event2.id.to_s] } + }.to change(EventRegistration, :count).by(2) + end + end + + context "with non-existent event IDs" do + it "handles non-existent event IDs gracefully" do + expect { + post bulk_create_event_registrations_path, params: { event_ids: [99999, event1.id] } + }.to change(EventRegistration, :count).by(0) + + expect(response).to have_http_status(:not_found) + end + end + end + + describe "parameter filtering" do + it "permits only allowed parameters for create action" do + expect_any_instance_of(ActionController::Parameters).to receive(:permit) + .with(:event_id, :first_name, :last_name, :email) + .and_call_original + + post event_registrations_path, params: { + event_registration: valid_attributes.merge(unauthorized_param: "hack") + } + end + end + + describe "transaction handling" do + let(:event1) { create(:event, title: "Event 1") } + let(:event2) { create(:event, title: "Event 2") } + + context "when bulk_create encounters errors" do + before do + create(:event_registration, event: event1, email: user.email) + allow_any_instance_of(EventRegistration).to receive(:save).and_return(false) + allow_any_instance_of(EventRegistration).to receive(:errors).and_return( + double(full_messages: ["Validation failed"]) + ) + end + + it "rolls back all changes when any registration fails" do + expect { + post bulk_create_event_registrations_path, params: { event_ids: [event1.id, event2.id] } + }.to change(EventRegistration, :count).by(0) + end + end + end +end \ No newline at end of file diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb new file mode 100644 index 000000000..749f01b39 --- /dev/null +++ b/spec/requests/events_spec.rb @@ -0,0 +1,195 @@ +require 'rails_helper' + +RSpec.describe "/events", type: :request do + before do + create(:permission, :adult) + create(:permission, :children) + create(:permission, :combined) + end + + let(:valid_attributes) { + { + "title": "sample title", + "description": "sample description", + "start_date": 1.day.from_now, + "end_date": 2.days.from_now, + "registration_close_date": 3.days.ago, + "publicly_visible": true + } + } + + let(:invalid_attributes) { + { + "title": "", + "description": "", + "start_date": "", + "end_date": "", + "registration_close_date": "" + } + } + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:event) { Event.create!(valid_attributes) } + + describe "GET /index" do + it "renders a successful response" do + sign_in user + get events_url + + expect(response).to be_successful + end + end + + describe "GET /show" do + it "renders a successful response" do + sign_in user + + allow_any_instance_of(ApplicationController). + to receive(:current_admin).and_return(true) + get event_url(event) + + expect(response).to be_successful + end + end + + describe "GET /new" do + it "renders a successful response" do + sign_in user + allow_any_instance_of(ApplicationController). + to receive(:current_admin).and_return(true) + + get new_event_url + + expect(response).to be_successful + end + end + + describe "GET /edit" do + describe 'when signed in as an admin' do + it "renders a successful response" do + sign_in user + + allow_any_instance_of(ApplicationController). + to receive(:current_admin).and_return(true) + get edit_event_url(event) + + expect(response).to be_successful + end + end + + describe "when not signed in as an admin" do + it "returns unauthorized" do + sign_in user + + get edit_event_url(event) + + expect(response).to have_http_status(:found) # 302 redirect + expect(response).to redirect_to(events_path) + end + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new Event" do + sign_in user + expect { + post events_url, params: { event: valid_attributes } + }.to change(Event, :count).by(1) + end + + it "redirects to the created event" do + sign_in user + post events_url, params: { event: valid_attributes } + expect(response).to redirect_to(event_url(Event.last)) + end + end + + context "with invalid parameters" do + it "does not create a new Event" do + sign_in user + expect { + post events_url, params: { event: invalid_attributes } + }.to change(Event, :count).by(0) + end + + it "renders a response with validation errors (i.e. to display the 'new' template)" do + sign_in user + post events_url, params: { event: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + context "when signed in as admin" do + it "updates the requested event" do + sign_in user + allow_any_instance_of(ApplicationController). + to receive(:current_admin).and_return(true) + patch event_url(event), params: { event: new_attributes } + event.reload + + expect(event.title).to eq(new_attributes[:title]) + end + + it "redirects to the event" do + sign_in user + allow_any_instance_of(ApplicationController). + to receive(:current_admin).and_return(true) + + patch event_url(event), params: { event: new_attributes } + event.reload + + expect(response).to redirect_to(event_url(event)) + end + end + + context "when not signed in as an admin" do + it "returns unauthorized" do + sign_in user + + patch event_url(event), params: { event: new_attributes } + + expect(response).to have_http_status(:found) + expect(response).to redirect_to(events_path) + end + end + end + + context "with invalid parameters" do + it "renders a response with validation errors (i.e. to display the 'edit' template)" do + event = Event.create!(valid_attributes) + allow_any_instance_of(ApplicationController). + to receive(:current_admin).and_return(true) + patch event_url(event), params: { event: invalid_attributes } + expect(response).to_not be_successful + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested event" do + event = Event.create!(valid_attributes) + sign_in user + allow_any_instance_of(ApplicationController). + to receive(:current_admin).and_return(true) + expect { + delete event_url(event) + }.to change(Event, :count).by(-1) + end + + it "redirects to the events list" do + sign_in user + allow_any_instance_of(ApplicationController). + to receive(:current_admin).and_return(true) + delete event_url(event) + expect(response).to redirect_to(events_url) + end + end +end diff --git a/spec/views/events/_form.html.erb_spec.rb b/spec/views/events/_form.html.erb_spec.rb new file mode 100644 index 000000000..b5c99f210 --- /dev/null +++ b/spec/views/events/_form.html.erb_spec.rb @@ -0,0 +1,124 @@ +require 'rails_helper' + +RSpec.describe "events/_form", type: :view do + let(:event) { Event.new } + + before do + assign(:event, event) + end + + it "renders all form fields" do + render + + expect(rendered).to have_selector("form") + expect(rendered).to have_selector("input[type='text'][name='event[title]']") + expect(rendered).to have_selector("textarea[name='event[description]']") + expect(rendered).to have_selector("input[type='datetime-local'][name='event[start_date]']") + expect(rendered).to have_selector("input[type='datetime-local'][name='event[end_date]']") + expect(rendered).to have_selector("input[type='datetime-local'][name='event[registration_close_date]']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]']") + end + + it "renders all form labels" do + render + + expect(rendered).to have_selector("label", text: "Title") + expect(rendered).to have_selector("label", text: "Description") + expect(rendered).to have_selector("label", text: "Start date") + expect(rendered).to have_selector("label", text: "End date") + expect(rendered).to have_selector("label", text: "Registration close date") + expect(rendered).to have_selector("label", text: "Publicly visible") + end + + it "applies correct CSS classes" do + render + + expect(rendered).to have_selector(".form-group") + expect(rendered).to have_selector("input.form-control") + expect(rendered).to have_selector("textarea.form-control") + expect(rendered).to have_selector("label.bold") + end + + it "renders submit button" do + render + + expect(rendered).to have_selector("button[type='submit']") + expect(rendered).to have_selector(".btn.btn-primary") + end + + context "when event has existing data" do + let(:event) do + create(:event, + title: "Existing Event", + description: "Existing description", + start_date: DateTime.new(2024, 1, 15, 10, 0), + end_date: DateTime.new(2024, 1, 15, 16, 0), + registration_close_date: DateTime.new(2024, 1, 10, 23, 59), + publicly_visible: true) + end + + it "populates form fields with existing data" do + render + + expect(rendered).to have_selector("input[value='Existing Event']") + expect(rendered).to have_selector("textarea", text: "Existing description") + expect(rendered).to have_selector("input[type='checkbox'][checked='checked']") + end + + it "populates datetime fields with properly formatted values" do + render + + expect(rendered).to have_selector("input[type='datetime-local'][value='2024-01-15T10:00']") + expect(rendered).to have_selector("input[type='datetime-local'][value='2024-01-15T16:00']") + expect(rendered).to have_selector("input[type='datetime-local'][value='2024-01-10T23:59']") + end + end + + context "when event has validation errors" do + let(:event) do + event = Event.new + event.errors.add(:title, "can't be blank") + event.errors.add(:start_date, "can't be blank") + event.errors.add(:end_date, "can't be blank") + event + end + + it "renders error messages" do + render + + expect(rendered).to have_content("Title can't be blank") + expect(rendered).to have_content("Start date can't be blank") + expect(rendered).to have_content("End date can't be blank") + end + end + + context "when event has no errors" do + it "does not render error messages" do + render + + expect(rendered).not_to have_selector(".error") + expect(rendered).not_to have_selector(".field_with_errors") + end + end + + context "when publicly_visible is false" do + let(:event) { create(:event, publicly_visible: false) } + + it "renders unchecked checkbox" do + render + + expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]']") + expect(rendered).not_to have_selector("input[type='checkbox'][checked='checked']") + end + end + + context "when publicly_visible is true" do + let(:event) { create(:event, publicly_visible: true) } + + it "renders checked checkbox" do + render + + expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]'][checked='checked']") + end + end +end diff --git a/spec/views/events/edit.html.erb_spec.rb b/spec/views/events/edit.html.erb_spec.rb new file mode 100644 index 000000000..b3920dc09 --- /dev/null +++ b/spec/views/events/edit.html.erb_spec.rb @@ -0,0 +1,75 @@ +require 'rails_helper' + +RSpec.describe "events/edit", type: :view do + let(:event) do + create(:event, + title: "Original Title", + description: "Original description", + start_date: DateTime.new(2024, 1, 15, 10, 0), + end_date: DateTime.new(2024, 1, 15, 16, 0), + registration_close_date: DateTime.new(2024, 1, 10, 23, 59), + publicly_visible: true) + end + + before do + assign(:event, event) + end + + it "renders the editing event heading" do + render + + expect(rendered).to have_selector("h1", text: "Editing Event") + end + + it "renders the form partial with event data" do + render + + expect(rendered).to have_selector("form") + expect(rendered).to have_selector("input[type='text'][name='event[title]'][value='Original Title']") + expect(rendered).to have_selector("textarea[name='event[description]']", text: "Original description") + expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]'][checked='checked']") + end + + it "renders action links" do + render + + expect(rendered).to have_link("Show", href: event_path(event)) + expect(rendered).to have_link("Back", href: events_path) + end + + it "renders submit button" do + render + + expect(rendered).to have_selector("button[type='submit']") + end + + context "when event has errors" do + let(:event) do + event = create(:event, title: "Test Event") + event.errors.add(:title, "can't be blank") + event.errors.add(:start_date, "can't be blank") + event + end + + it "renders the form with errors" do + render + + expect(rendered).to have_selector("form") + end + end + + context "when event is not publicly visible" do + let(:event) do + create(:event, + title: "Private Event", + publicly_visible: false) + end + + it "renders unchecked checkbox" do + render + + expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]']") + expect(rendered).not_to have_selector("input[type='checkbox'][name='event[publicly_visible]'][checked='checked']") + end + end +end \ No newline at end of file diff --git a/spec/views/events/index.html.erb_spec.rb b/spec/views/events/index.html.erb_spec.rb new file mode 100644 index 000000000..6dd9edad2 --- /dev/null +++ b/spec/views/events/index.html.erb_spec.rb @@ -0,0 +1,103 @@ +require 'rails_helper' + +RSpec.describe "events/index", type: :view do + before do + create(:permission, :adult) + create(:permission, :children) + create(:permission, :combined) + end + + let(:user) { create(:user) } + let(:event1) { create(:event, title: "Event 1", start_date: 1.day.from_now, end_date: 2.days.from_now) } + let(:event2) { create(:event, title: "Event 2", start_date: 3.days.from_now, end_date: 4.days.from_now) } + let(:events) { [event1, event2] } + + before do + assign(:events, events) + allow(view).to receive(:current_user).and_return(user) + end + + it "renders the events table with proper headers" do + render + + expect(rendered).to have_selector("th", text: "") + expect(rendered).to have_selector("th", text: "Title") + expect(rendered).to have_selector("th", text: "Start Date") + expect(rendered).to have_selector("th", text: "End Date") + expect(rendered).to have_selector("th", text: "Actions") + end + + it "renders each event with checkbox and details" do + render + + events.each do |event| + expect(rendered).to have_selector("input[type='checkbox'][value='#{event.id}']") + expect(rendered).to have_content(event.title) + expect(rendered).to have_content(event.start_date.strftime("%B %d, %Y")) + expect(rendered).to have_content(event.end_date.strftime("%B %d, %Y")) + end + end + + it "renders action links for each event" do + render + + events.each do |event| + expect(rendered).to have_link("Show", href: event_path(event)) + expect(rendered).to have_link("Edit", href: edit_event_path(event)) + expect(rendered).to have_link("Destroy", href: event_path(event)) + end + end + + it "renders the bulk registration form" do + render + + expect(rendered).to have_selector("form[action='#{bulk_create_event_registrations_path}'][method='post']") + expect(rendered).to have_selector("input[type='submit'][value='Register for Selected Events']") + expect(rendered).to have_selector("input[type='submit'][disabled='disabled']") + end + + it "renders the New Event link" do + render + + expect(rendered).to have_link("New Event", href: new_event_path) + end + + it "includes JavaScript for checkbox handling" do + render + + expect(rendered).to have_content("event-checkbox") + expect(rendered).to have_content("register-button") + expect(rendered).to have_content("addEventListener") + end + + it "displays notice if present" do + flash[:notice] = "Test notice" + render + + expect(rendered).to have_selector("p#notice", text: "Test notice") + end + + context "when no events exist" do + let(:events) { [] } + + it "renders empty table" do + render + + expect(rendered).to have_selector("table") + expect(rendered).to have_selector("th", text: "Title") + expect(rendered).not_to have_selector("input[type='checkbox']") + end + end + + context "when events have minimal data" do + let(:event_with_minimal_data) { create(:event, title: "Minimal Event", description: nil) } + let(:events) { [event_with_minimal_data] } + + it "handles minimal data gracefully" do + render + + expect(rendered).to have_content("Minimal Event") + expect(rendered).to have_content(event_with_minimal_data.start_date.strftime("%B %d, %Y")) + end + end +end \ No newline at end of file diff --git a/spec/views/events/new.html.erb_spec.rb b/spec/views/events/new.html.erb_spec.rb new file mode 100644 index 000000000..a681c7c28 --- /dev/null +++ b/spec/views/events/new.html.erb_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +RSpec.describe "events/new", type: :view do + let(:event) { Event.new } + + before do + assign(:event, event) + end + + it "renders the new event heading" do + render + + expect(rendered).to have_selector("h1", text: "New Event") + end + + it "renders the form partial" do + render + + expect(rendered).to have_selector("form") + expect(rendered).to have_selector("input[type='text'][name='event[title]']") + expect(rendered).to have_selector("textarea[name='event[description]']") + expect(rendered).to have_selector("input[type='datetime-local'][name='event[start_date]']") + expect(rendered).to have_selector("input[type='datetime-local'][name='event[end_date]']") + expect(rendered).to have_selector("input[type='datetime-local'][name='event[registration_close_date]']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]']") + end + + it "renders the Back link" do + render + + expect(rendered).to have_link("Back", href: events_path) + end + + it "renders submit button" do + render + + expect(rendered).to have_selector("button[type='submit']") + end + + context "when event has errors" do + let(:event) do + event = Event.new + event.errors.add(:title, "can't be blank") + event.errors.add(:start_date, "can't be blank") + event + end + + it "renders error messages" do + render + + expect(rendered).to have_selector(".form-group") + end + end +end \ No newline at end of file diff --git a/spec/views/events/show.html.erb_spec.rb b/spec/views/events/show.html.erb_spec.rb new file mode 100644 index 000000000..cb991ee21 --- /dev/null +++ b/spec/views/events/show.html.erb_spec.rb @@ -0,0 +1,90 @@ +require 'rails_helper' + +RSpec.describe "events/show", type: :view do + let(:event) do + create(:event, + title: "Test Event", + description: "This is a test event description", + start_date: DateTime.new(2024, 1, 15, 10, 0), + end_date: DateTime.new(2024, 1, 15, 16, 0), + registration_close_date: DateTime.new(2024, 1, 10, 23, 59)) + end + + before do + assign(:event, event) + end + + it "renders the event title" do + render + + expect(rendered).to have_selector("h1", text: "Event Details") + expect(rendered).to have_content("Test Event") + end + + it "renders all event details" do + render + + expect(rendered).to have_content("Title:") + expect(rendered).to have_content("Test Event") + + expect(rendered).to have_content("Description:") + expect(rendered).to have_content("This is a test event description") + + expect(rendered).to have_content("Start Date:") + expect(rendered).to have_content("January 15, 2024 10:00 AM") + + expect(rendered).to have_content("End Date:") + expect(rendered).to have_content("January 15, 2024 04:00 PM") + + expect(rendered).to have_content("Registration Close Date:") + expect(rendered).to have_content("January 10, 2024 11:59 PM") + end + + it "renders action links" do + render + + expect(rendered).to have_link("Edit", href: edit_event_path(event)) + expect(rendered).to have_link("Back", href: events_path) + end + + it "displays notice if present" do + flash[:notice] = "Event created successfully" + render + + expect(rendered).to have_selector("p#notice", text: "Event created successfully") + end + + context "when event has minimal data" do + let(:event) do + create(:event, + title: "Minimal Event", + description: "Event with minimal data", + registration_close_date: nil) + end + + it "handles minimal data gracefully" do + render + + expect(rendered).to have_content("Minimal Event") + expect(rendered).to have_content("Event with minimal data") + expect(rendered).to have_content(event.start_date.strftime("%B %d, %Y %I:%M %p")) + end + end + + context "when event has empty description" do + let(:event) do + create(:event, + title: "Event with Empty Description", + description: "", + start_date: 1.day.from_now, + end_date: 2.days.from_now) + end + + it "renders without description content" do + render + + expect(rendered).to have_content("Event with Empty Description") + expect(rendered).to have_content("Description:") + end + end +end \ No newline at end of file