diff --git a/.gitignore b/.gitignore index 71bac6175..11e86f128 100644 --- a/.gitignore +++ b/.gitignore @@ -59,5 +59,5 @@ vendor/bundle yarn-debug.log* .yarn-integrity -# Git worktrees -.worktrees +# Worktrees +.worktrees/ diff --git a/app/controllers/concerns/invitation_controller_concerns.rb b/app/controllers/concerns/invitation_controller_concerns.rb deleted file mode 100644 index 62c8cfc58..000000000 --- a/app/controllers/concerns/invitation_controller_concerns.rb +++ /dev/null @@ -1,73 +0,0 @@ -module InvitationControllerConcerns - extend ActiveSupport::Concern - - included do - include WorkshopInvitationConcerns - include InstanceMethods - end - - module InstanceMethods - def accept - user = current_user || @invitation.member - workshop = @invitation.workshop - return back_with_message(t('messages.already_rsvped')) if @invitation.attending? - - @invitation.assign_attributes(invitation_params.merge!(attending: true, rsvp_time: Time.zone.now)) - return back_with_message(@invitation.errors.full_messages) unless @invitation.valid? - - if user.has_existing_RSVP_on(workshop.date_and_time) - return back_with_message(t('messages.invitations.rsvped_to_other_workshop')) - end - - return back_with_message(t('messages.already_invited')) if attending_or_waitlisted?(workshop, user) - - @workshop = WorkshopPresenter.decorate(@invitation.workshop) - if available_spaces?(@workshop, @invitation) - @invitation.update(invitation_params.merge!(attending: true, rsvp_time: Time.zone.now)) - @workshop.send_attending_email(@invitation) - - back_with_message(t('messages.accepted_invitation', name: @invitation.member.name)) - else - back_with_message(t('messages.no_available_seats')) - end - end - - def reject - @workshop = WorkshopPresenter.decorate(@invitation.workshop) - if @invitation.workshop.date_and_time - 3.5.hours >= Time.zone.now - - if @invitation.attending.eql? false - redirect_back(fallback_location: invitation_path(@invitation), - notice: t('messages.not_attending_already')) - else - @invitation.update_attribute(:attending, false) - - next_spot = WaitingList.next_spot(@invitation.workshop, @invitation.role) - - if next_spot.present? - invitation = next_spot.invitation - next_spot.destroy - invitation.update(attending: true, rsvp_time: Time.zone.now, automated_rsvp: true) - @workshop.send_attending_email(invitation, true) - end - - redirect_back( - fallback_location: invitation_path(@invitation), - notice: t('messages.rejected_invitation', name: @invitation.member.name) - ) - end - else - redirect_back( - fallback_location: invitation_path(@invitation), - notice: 'You can only change your RSVP status up to 3.5 hours before the workshop' - ) - end - end - - private - - def attending_or_waitlisted?(workshop, user) - workshop.attendee?(user) || workshop.waitlisted?(user) - end - end -end diff --git a/app/controllers/invitation_controller.rb b/app/controllers/invitation_controller.rb deleted file mode 100644 index 31acf029a..000000000 --- a/app/controllers/invitation_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -class InvitationController < ApplicationController - include InvitationControllerConcerns - - def show - @announcements = @invitation.member.announcements.active - @tutorial_titles = Tutorial.all_titles - - @workshop = WorkshopPresenter.decorate(@invitation.workshop) - - render plain: @workshop.attendees_csv if request.format.csv? - end - - def update - @invitation.assign_attributes(invitation_params.merge!(attending: true, rsvp_time: Time.zone.now)) - return back_with_message(@invitation.errors.full_messages) unless @invitation.valid? - - @invitation.update(invitation_params) - back_with_message(t('messages.invitations.updated_details')) - end - - private - - def invitation_params - if params.key?(:workshop_invitation) - params.expect(workshop_invitation: [:tutorial, :note]) - else - {} - end - end - - def available_spaces?(workshop, invitation) - (invitation.role.eql?('Student') && workshop.student_spaces?) || - (invitation.role.eql?('Coach') && workshop.coach_spaces?) - end - - def token - params[:id] - end -end diff --git a/app/controllers/workshop_invitation_controller.rb b/app/controllers/workshop_invitation_controller.rb new file mode 100644 index 000000000..5bd1d28b3 --- /dev/null +++ b/app/controllers/workshop_invitation_controller.rb @@ -0,0 +1,106 @@ +class WorkshopInvitationController < ApplicationController + include WorkshopInvitationConcerns + + # NOTE: This controller handles workshop invitations (WorkshopInvitation model). + # It provides accept/reject RSVP actions for workshop attendees via token-based links. + # Routes: /invitation/:token (legacy) and /workshop_invitation/:token + + def show + @announcements = @invitation.member.announcements.active + @tutorial_titles = Tutorial.all_titles + + @workshop = WorkshopPresenter.decorate(@invitation.workshop) + + render plain: @workshop.attendees_csv if request.format.csv? + end + + def update + @invitation.assign_attributes(invitation_params.merge!(attending: true, rsvp_time: Time.zone.now)) + return back_with_message(@invitation.errors.full_messages) unless @invitation.valid? + + @invitation.update(invitation_params) + back_with_message(t('messages.invitations.updated_details')) + end + + # Inline accept from InvitationControllerConcerns + def accept + user = current_user || @invitation.member + workshop = @invitation.workshop + return back_with_message(t('messages.already_rsvped')) if @invitation.attending? + + @invitation.assign_attributes(invitation_params.merge!(attending: true, rsvp_time: Time.zone.now)) + return back_with_message(@invitation.errors.full_messages) unless @invitation.valid? + + if user.has_existing_RSVP_on(workshop.date_and_time) + return back_with_message(t('messages.invitations.rsvped_to_other_workshop')) + end + + return back_with_message(t('messages.already_invited')) if attending_or_waitlisted?(workshop, user) + + @workshop = WorkshopPresenter.decorate(@invitation.workshop) + if available_spaces?(@workshop, @invitation) + @invitation.update(invitation_params.merge!(attending: true, rsvp_time: Time.zone.now)) + @workshop.send_attending_email(@invitation) + + back_with_message(t('messages.accepted_invitation', name: @invitation.member.name)) + else + back_with_message(t('messages.no_available_seats')) + end + end + + # Inline reject from InvitationControllerConcerns + def reject + @workshop = WorkshopPresenter.decorate(@invitation.workshop) + if @invitation.workshop.date_and_time - 3.5.hours >= Time.zone.now + if @invitation.attending.eql? false + redirect_back(fallback_location: invitation_path(@invitation), + notice: t('messages.not_attending_already')) + else + @invitation.update_attribute(:attending, false) + + next_spot = WaitingList.next_spot(@invitation.workshop, @invitation.role) + + if next_spot.present? + invitation = next_spot.invitation + next_spot.destroy + invitation.update(attending: true, rsvp_time: Time.zone.now, automated_rsvp: true) + @workshop.send_attending_email(invitation, true) + end + + redirect_back( + fallback_location: invitation_path(@invitation), + notice: t('messages.rejected_invitation', name: @invitation.member.name) + ) + end + else + redirect_back( + fallback_location: invitation_path(@invitation), + notice: 'You can only change your RSVP status up to 3.5 hours before the workshop' + ) + end + end + + private + + def invitation_params + if params.key?(:workshop_invitation) + params.require(:workshop_invitation).permit(:tutorial, :note) + else + {} + end + end + + def available_spaces?(workshop, invitation) + (invitation.role.eql?('Student') && workshop.student_spaces?) || + (invitation.role.eql?('Coach') && workshop.coach_spaces?) + end + + # Inline from InvitationControllerConcerns + def attending_or_waitlisted?(workshop, user) + workshop.attendee?(user) || workshop.waitlisted?(user) + end + + def token + params[:id] + end +end diff --git a/app/views/invitation/_coach.html.haml b/app/views/workshop_invitation/_coach.html.haml similarity index 100% rename from app/views/invitation/_coach.html.haml rename to app/views/workshop_invitation/_coach.html.haml diff --git a/app/views/invitation/_student.html.haml b/app/views/workshop_invitation/_student.html.haml similarity index 100% rename from app/views/invitation/_student.html.haml rename to app/views/workshop_invitation/_student.html.haml diff --git a/app/views/invitation/_waiting_list.html.haml b/app/views/workshop_invitation/_waiting_list.html.haml similarity index 100% rename from app/views/invitation/_waiting_list.html.haml rename to app/views/workshop_invitation/_waiting_list.html.haml diff --git a/app/views/invitation/show.html.haml b/app/views/workshop_invitation/show.html.haml similarity index 97% rename from app/views/invitation/show.html.haml rename to app/views/workshop_invitation/show.html.haml index e2a6ef797..dbba2f595 100644 --- a/app/views/invitation/show.html.haml +++ b/app/views/workshop_invitation/show.html.haml @@ -89,7 +89,7 @@ = f.input :note, required: false, input_html: { rows: 3, maxlength: 100 } = f.button :button, 'Attend', class: 'btn btn-primary w-100 mb-0' - else - = render partial: 'invitation/waiting_list', locals: { invitation: @invitation } + = render partial: 'workshop_invitation/waiting_list', locals: { invitation: @invitation } - else - if @workshop.student_spaces? = simple_form_for @invitation, url: :accept_invitation, method: :post do |f| @@ -97,7 +97,7 @@ = f.input :note, required: false, input_html: { rows: 3, maxlength: 100 }, hint: 'Anything else we should know?', placeholder: 'e.g. I need help understanding selectors' = f.button :button, 'Attend', class: 'btn btn-primary w-100 mb-0' - else - = render partial: 'invitation/waiting_list', locals: { invitation: @invitation } + = render partial: 'workshop_invitation/waiting_list', locals: { invitation: @invitation } .card-footer.bg-transparent Read our #{link_to "attendance policy", attendance_policy_path}. diff --git a/config/routes.rb b/config/routes.rb index 006b92990..345a2d9c2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,7 +32,19 @@ get 'unsubscribe/:token' => 'members#unsubscribe', as: :unsubscribe - resources :invitation, only: [:show, :update] do + # Old route - kept for backwards compatibility (existing invitation links in emails) + resources :invitation, only: %i[show update], controller: 'workshop_invitation' do + member do + post 'accept' + get 'accept' + get 'reject' + end + + resource :waiting_list, only: %i[create destroy] + end + + # New route - cleaner URLs + resources :workshop_invitation, only: %i[show update] do member do post 'accept' get 'accept'