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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, with: :render_not_found
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found

rescue_from ActiveRecord::RecordInvalid do |exception|
Rails.logger.error("Validation failed: #{exception.message}")
redirect_back(fallback_location: root_path,
alert: 'Unable to complete action. Please check your input and try again.')
end

rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from Pundit::AuthorizationNotPerformedError, with: :user_not_authorized

Expand Down
4 changes: 2 additions & 2 deletions app/controllers/concerns/admin/sponsor_concerns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ def destroy_sponsor
def host
set_sponsor
@workshop_sponsor = WorkshopSponsor.find_or_create_by(workshop: @workshop, sponsor: @sponsor)
@workshop_sponsor.update_attribute(:host, true)
@workshop_sponsor.set_as_host!
flash[:notice] = 'Host set successfully'

redirect_back fallback_location: root_path
end

def destroy_host
@workshop.workshop_sponsors.find_by(host: true).update_attribute(:host, false)
@workshop.workshop_sponsors.find_by(host: true)&.remove_as_host!

redirect_back fallback_location: root_path
end
Expand Down
39 changes: 24 additions & 15 deletions app/controllers/concerns/invitation_controller_concerns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,25 @@ def accept
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)
begin
WorkshopInvitation.transaction do
@invitation.decline!
promote_from_waitlist(@invitation.workshop, @invitation.role)
end
redirect_back(
fallback_location: invitation_path(@invitation),
notice: t('messages.rejected_invitation', name: @invitation.member.name)
)
rescue ActiveRecord::RecordInvalid
redirect_back(
fallback_location: invitation_path(@invitation),
alert: 'Unable to process cancellation. Please try again.'
)
end

redirect_back(
fallback_location: invitation_path(@invitation),
notice: t('messages.rejected_invitation', name: @invitation.member.name)
)
end
else
redirect_back(
Expand All @@ -66,6 +65,16 @@ def reject

private

def promote_from_waitlist(workshop, role)
next_spot = WaitingList.next_spot(workshop, role)
return unless next_spot

invitation = next_spot.invitation
next_spot.destroy!
invitation.accept!(rsvp_time: Time.zone.now, automated_rsvp: true)
WorkshopPresenter.decorate(workshop).send_attending_email(invitation, true)
end

def attending_or_waitlisted?(workshop, user)
workshop.attendee?(user) || workshop.waitlisted?(user)
end
Expand Down
8 changes: 4 additions & 4 deletions app/controllers/invitations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ def attend
return redirect_back fallback_location: root_path, notice: t('messages.already_rsvped') if @invitation.attending?

if @invitation.student_spaces? || @invitation.coach_spaces?
@invitation.update_attribute(:attending, true)
@invitation.accept!

notice = t('messages.invitations.spot_confirmed', event: @invitation.event.name)

unless event.confirmation_required || event.surveys_required
@invitation.update_attribute(:verified, true)
@invitation.verify!
EventInvitationMailer.attending(@invitation.event, @invitation.member, @invitation).deliver_now
end
notice = t('messages.invitations.spot_not_confirmed') if event.surveys_required
Expand All @@ -54,7 +54,7 @@ def reject
)
end

@invitation.update_attribute(:attending, false)
@invitation.decline!
redirect_back(
fallback_location: root_path,
notice: t('messages.rejected_invitation', name: @invitation.member.name)
Expand All @@ -79,7 +79,7 @@ def rsvp_meeting
def cancel_meeting
@invitation = MeetingInvitation.find_by(token: params[:token])

@invitation.update_attribute(:attending, false)
@invitation.decline!

redirect_back fallback_location: root_path, notice: t('messages.invitations.meeting.cancel')
end
Expand Down
12 changes: 12 additions & 0 deletions app/models/invitation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,16 @@ def coach_spaces?
def to_param
token
end

def accept!(verified: false)
update!(attending: true, verified: verified)
end

def verify!
update!(verified: true)
end

def decline!
update!(attending: false)
end
end
8 changes: 8 additions & 0 deletions app/models/meeting_invitation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@ class MeetingInvitation < ApplicationRecord
scope :attended, -> { where(attended: true) }

alias event meeting

def accept!
update!(attending: true)
end

def decline!
update!(attending: false)
end
end
8 changes: 8 additions & 0 deletions app/models/workshop_invitation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,12 @@ def student_attending?
def not_attending?
attending == false
end

def accept!(rsvp_time: Time.zone.now, automated_rsvp: false)
update!(attending: true, rsvp_time: rsvp_time, automated_rsvp: automated_rsvp)
end

def decline!
update!(attending: false)
end
end
8 changes: 8 additions & 0 deletions app/models/workshop_sponsor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,12 @@ class WorkshopSponsor < ApplicationRecord
belongs_to :workshop

validates :sponsor_id, uniqueness: { scope: :workshop_id, message: :already_sponsoring }

def set_as_host!
update!(host: true)
end

def remove_as_host!
update!(host: false)
end
end
26 changes: 13 additions & 13 deletions spec/controllers/admin/invitations_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,43 @@
let(:workshop) { invitation.workshop }
let(:admin) { Fabricate(:chapter_organiser) }

describe "PUT #update" do
describe 'PUT #update' do
before do
admin.add_role(:organiser, workshop.chapter)

login admin
request.env["HTTP_REFERER"] = "/admin/member/3"
request.env['HTTP_REFERER'] = '/admin/member/3'

expect(invitation.attending).to be_nil
end

it "Successfuly updates an invitation" do
put :update, params: { id: invitation.token, workshop_id: workshop.id, attending: "true" }
it 'Successfuly updates an invitation' do
put :update, params: { id: invitation.token, workshop_id: workshop.id, attending: 'true' }

expect(invitation.reload.attending).to be true
expect(flash[:notice]).to match("You have added")
expect(flash[:notice]).to match('You have added')
end

# While similar to the previous test, this specifically tests that organisers
# have the ability to manually add a student to the workshop that has not
# selected a tutorial. This is helpful for when a student shows up for a
# workshop they have not have a spot — this happens from time to time.
it "Successfuly adds a user as attenting, even without a tutorial" do
invitation.update_attribute(:tutorial, nil)
it 'Successfuly adds a user as attenting, even without a tutorial' do
invitation.update_column(:tutorial, nil)
expect(invitation.automated_rsvp).to be_nil

put :update, params: { id: invitation.token, workshop_id: workshop.id, attending: "true" }
put :update, params: { id: invitation.token, workshop_id: workshop.id, attending: 'true' }
invitation.reload

expect(invitation.attending).to be true
expect(invitation.automated_rsvp).to be true
expect(flash[:notice]).to match("You have added")
expect(flash[:notice]).to match('You have added')
end

it "Records the organiser ID that overrides an invitations" do
put :update, params: { id: invitation.token, workshop_id: workshop.id, attending: "true" }
it 'Records the organiser ID that overrides an invitations' do
put :update, params: { id: invitation.token, workshop_id: workshop.id, attending: 'true' }
invitation.reload

expect(invitation.last_overridden_by_id).to be admin.id
end
end
Expand Down
10 changes: 5 additions & 5 deletions spec/features/accepting_invitation_spec.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
RSpec.feature 'Accepting a workshop invitation', type: :feature do
context '#workshop' do
describe '#workshop' do
let(:member) { Fabricate(:member) }
let(:invitation) { Fabricate(:workshop_invitation, member: member, tutorial: tutorial.title) }
let(:invitation_route) { invitation_path(invitation) }
let(:accept_invitation_route) { accept_invitation_path(invitation) }
let(:reject_invitation_route) { reject_invitation_path(invitation) }
let(:set_no_available_slots) { invitation.workshop.host.update_attribute(:seats, 0) }
let(:set_no_available_slots) { invitation.workshop.host.update_column(:seats, 0) }
let!(:tutorial) { Fabricate(:tutorial) }

it_behaves_like 'invitation route'

context 'amend invitation details' do
context 'a student' do
scenario 'cannot accept an invitation without a tutorial' do
scenario 'cannot accept an invitation without a tutorial' do
invitation.update(attending: nil, tutorial: nil)
visit invitation_route

Expand All @@ -22,7 +22,7 @@
end

scenario 'with an accepted invitation can edit the tutorial' do
invitation.update_attribute(:attending, true)
invitation.accept!
visit invitation_route

select tutorial.title, from: :workshop_invitation_tutorial
Expand Down Expand Up @@ -66,7 +66,7 @@
click_on 'Update note'

expect(page).to have_field('workshop_invitation_note', with: note)
expect(page).to have_content("Invitation details successfully updated.")
expect(page).to have_content('Invitation details successfully updated.')
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions spec/features/coach_accepting_invitation_spec.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
RSpec.feature 'a Coach can', type: :feature do
context '#workshop' do
describe '#workshop' do
let(:member) { Fabricate(:member) }
let(:invitation) { Fabricate(:coach_workshop_invitation, member: member) }
let(:invitation_route) { invitation_path(invitation) }
let(:reject_invitation_route) { reject_invitation_path(invitation) }
let(:accept_invitation_route) { accept_invitation_path(invitation) }

let(:set_no_available_slots) { invitation.workshop.host.update_attribute(:seats, 0) }
let(:set_no_available_slots) { invitation.workshop.host.update_column(:seats, 0) }

before(:each) do
before do
login(member)
end

Expand Down
56 changes: 54 additions & 2 deletions spec/models/invitation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,71 @@
it { is_expected.to validate_inclusion_of(:role).in_array(%w[Student Coach]) }
end

context '#student_spaces?' do
describe '#student_spaces?' do
it 'checks if there are any available spaces for students at the event' do
student_invitation = Fabricate(:invitation)

expect(student_invitation.student_spaces?).to eq(true)
end
end

context '#coach_spaces?' do
describe '#coach_spaces?' do
it 'checks if there are any available spaces for coaches at the event' do
coach_invitation = Fabricate(:coach_invitation)

expect(coach_invitation.coach_spaces?).to eq(true)
end
end

describe '#accept!' do
let(:invitation) { Fabricate(:invitation, attending: false) }

it 'sets attending to true' do
invitation.accept!
expect(invitation.reload.attending).to be true
end

it 'does not verify by default' do
invitation.accept!
expect(invitation.reload.verified).to be false
end

it 'allows immediate verification' do
invitation.accept!(verified: true)
expect(invitation.reload.verified).to be true
end

it 'raises RecordInvalid on validation failure' do
allow(invitation).to receive(:valid?).and_return(false)
expect { invitation.accept! }.to raise_error(ActiveRecord::RecordInvalid)
end
end

describe '#verify!' do
let(:invitation) { Fabricate(:invitation, verified: false) }

it 'sets verified to true' do
invitation.verify!
expect(invitation.reload.verified).to be true
end

it 'raises RecordInvalid on validation failure' do
allow(invitation).to receive(:valid?).and_return(false)
expect { invitation.verify! }.to raise_error(ActiveRecord::RecordInvalid)
end
end

describe '#decline!' do
let(:invitation) { Fabricate(:invitation, attending: true) }

it 'sets attending to false' do
invitation.decline!
expect(invitation.reload.attending).to be false
end

it 'raises RecordInvalid on validation failure' do
allow(invitation).to receive(:valid?).and_return(false)
expect { invitation.decline! }.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
28 changes: 28 additions & 0 deletions spec/models/meeting_invitation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,32 @@
it { is_expected.to validate_presence_of(:member) }
it { is_expected.to validate_uniqueness_of(:member_id).scoped_to(:meeting_id) }
end

describe '#accept!' do
let(:invitation) { Fabricate(:meeting_invitation, attending: false) }

it 'sets attending to true' do
invitation.accept!
expect(invitation.reload.attending).to be true
end

it 'raises RecordInvalid on validation failure' do
allow(invitation).to receive(:valid?).and_return(false)
expect { invitation.accept! }.to raise_error(ActiveRecord::RecordInvalid)
end
end

describe '#decline!' do
let(:invitation) { Fabricate(:meeting_invitation, attending: true) }

it 'sets attending to false' do
invitation.decline!
expect(invitation.reload.attending).to be false
end

it 'raises RecordInvalid on validation failure' do
allow(invitation).to receive(:valid?).and_return(false)
expect { invitation.decline! }.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
Loading