Skip to content
Merged
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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ gem 'public_activity'

group :development do
gem 'better_errors'
gem 'binding_of_caller'
gem 'letter_opener'
gem 'web-console', '>= 4.1.0'
# Display performance information such as SQL time and flame graphs for each request in your browser.
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ GEM
rack (>= 0.9.0)
rouge (>= 1.0.0)
bindex (0.8.1)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
bootsnap (1.17.0)
msgpack (~> 1.2)
bootstrap (5.3.2)
Expand Down Expand Up @@ -139,6 +141,7 @@ GEM
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.3)
debug_inspector (1.2.0)
delayed_job (4.1.11)
activesupport (>= 3.0, < 8.0)
delayed_job_active_record (4.1.8)
Expand Down Expand Up @@ -488,6 +491,7 @@ PLATFORMS
DEPENDENCIES
acts-as-taggable-on
better_errors
binding_of_caller
bootsnap
bootstrap (~> 5)
bullet
Expand Down
9 changes: 7 additions & 2 deletions app/controllers/admin/invitations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ def update_to_attended
end

def update_to_attending
update_successful = @invitation.update(attending: true, rsvp_time: Time.zone.now, automated_rsvp: true)
update_successful = @invitation.update(
attending: true,
rsvp_time: Time.zone.now,
automated_rsvp: true,
last_overridden_by_id: current_user.id
)

{
message: update_successful ? attending_successful : attending_failed,
Expand All @@ -59,7 +64,7 @@ def attending_failed
end

def update_to_not_attending
@invitation.update(attending: false)
@invitation.update!(attending: false, last_overridden_by_id: current_user.id)

{
message: "You have removed #{@invitation.member.full_name} from the workshop.",
Expand Down
11 changes: 10 additions & 1 deletion app/controllers/admin/workshops_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class Admin::WorkshopsController < Admin::ApplicationController
include Admin::WorkshopConcerns

before_action :set_workshop_by_id, only: %i[show edit destroy update]
before_action :set_and_decorate_workshop, only: %i[attendees_checklist attendees_emails send_invites]
before_action :set_and_decorate_workshop, only: %i[attendees_checklist attendees_emails send_invites changes]

WORKSHOP_DELETION_TIME_FRAME_SINCE_CREATION = 4.hours

Expand Down Expand Up @@ -105,6 +105,15 @@ def destroy
end
end

def changes
invitations = @workshop.invitations
.where.not(attending: nil)
.includes(:member).order('members.name')

@coach_invitations = invitations.to_coaches
@student_invitations = invitations.to_students
end

private

def workshop_params
Expand Down
15 changes: 9 additions & 6 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ class ApplicationController < ActionController::Base
include Pundit::Authorization
include Pagy::Backend

rescue_from Exception do |ex|
Rollbar.error(ex)
Rails.logger.fatal(ex)
respond_to do |format|
format.html { render 'errors/error', layout: false, status: :internal_server_error }
format.all { head :internal_server_error }
if Rails.env.production?
rescue_from Exception do |ex|
Rollbar.error(ex)
Rails.logger.fatal(ex)

respond_to do |format|
format.html { render 'errors/error', layout: false, status: :internal_server_error }
format.all { head :internal_server_error }
end
end
end

Expand Down
6 changes: 5 additions & 1 deletion app/models/member.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ def banned_permanently?
bans.permanent.present?
end

def name_and_surname
[name, surname].compact.join ' '
end

def full_name
pronoun = pronouns.present? ? "(#{pronouns})" : nil
[name, surname, pronoun].compact.join ' '
[name_and_surname, pronoun].compact.join ' '
end

def student?
Expand Down
10 changes: 9 additions & 1 deletion app/models/workshop_invitation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ class WorkshopInvitation < ApplicationRecord
include InvitationConcerns

belongs_to :workshop
belongs_to :member
belongs_to :overrider, foreign_key: :last_overridden_by_id, class_name: 'Member', inverse_of: false
has_one :waiting_list, foreign_key: :invitation_id

validates :workshop, :member, presence: true
validates :member_id, uniqueness: { scope: %i[workshop_id role] }
validates :role, inclusion: { in: %w[Student Coach], allow_nil: true }
validates :tutorial, presence: true, if: :student_attending?
validates :tutorial, presence: true, if: lambda {
student_attending? && !automated_rsvp
}
validates :tutorial, presence: true, on: :waitinglist, if: :student_attending?

scope :year, ->(year) { joins(:workshop).where('EXTRACT(year FROM workshops.date_and_time) = ?', year) }
Expand Down Expand Up @@ -39,4 +43,8 @@ def parent
def student_attending?
for_student? && attending.present?
end

def not_attending?
attending == false
end
end
12 changes: 9 additions & 3 deletions app/views/admin/workshop/_attendances.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,15 @@
%i.fas.fa-history
= l(invitation.rsvp_time)
- if invitation.automated_rsvp?
%span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'Waiting list or admin addition'}
%p.mb-1.small
%i.fas.fa-magic
- tooltip_args = { 'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom' }
- if invitation.last_overridden_by_id?
%span{ tooltip_args, title: "Addition by #{invitation.overrider.full_name}" }
%p.mb-1.small
%i.fas.fa-hat-wizard
- else
%span{ tooltip_args, title: 'Waiting list or admin addition' }
%p.mb-1.small
%i.fas.fa-magic
- if invitation.reminded_at.present?
%span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'Reminder emailed at'}
%p.mb-1.small
Expand Down
15 changes: 15 additions & 0 deletions app/views/admin/workshops/_activity.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
%tr
%th{ scope: "col" }
= link_to invitation.member.name_and_surname,
admin_member_path(invitation.member_id)
%td
- if invitation.not_attending?
%span.text-muted No
- else
Yes
%td
- if invitation.last_overridden_by_id?
= link_to invitation.overrider.full_name,
admin_member_path(invitation.last_overridden_by_id)
- else
%span.text-muted No
2 changes: 2 additions & 0 deletions app/views/admin/workshops/_invitation_management.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<strong>#{@attending_students.count}</strong> are attending as students and <strong>#{@attending_coaches.count}</strong> as coaches.
- if @student_waiting_list.any? or @coach_waiting_list.any?
There is also a waiting list of <strong>#{@student_waiting_list.count}</strong> students and <strong>#{@coach_waiting_list.count}</strong> coaches.
%br
= link_to 'See all invitations’ statuses', admin_workshop_changes_path(@workshop)

= simple_form_for :workshop, url: admin_workshop_invitations_path(@workshop, attending: true), remote: true, method: :put do |f|
.row.mb-4
Expand Down
30 changes: 30 additions & 0 deletions app/views/admin/workshops/changes.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.container
%p
This page is useful to see all the invitations acted upon, meaning any people
that RSVPed attending or not attending.
%p
You can also see if an invitation has been overridden by an organiser,
usually to manually add/remove people from workshops.

%table.table.table-striped.students-table
%thead
%tr
%th{ scope: "col" } Student
%th{ scope: "col" } Attending?
%th{ scope: "col" } Overriden by organiser?
%tbody
- @student_invitations.each do |invitation|
= render partial: 'admin/workshops/activity',
locals: { invitation: invitation }

.container
%table.table.table-striped.coaches-table
%thead
%tr
%th{ scope: "col" } Coach
%th{ scope: "col" } Attending?
%th{ scope: "col" } Overriden by organiser?
%tbody
- @coach_invitations.each do |invitation|
= render partial: 'admin/workshops/activity',
locals: { invitation: invitation }
5 changes: 3 additions & 2 deletions app/views/invitations/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
Group: #{invitation.role.pluralize}
%br
.text-muted #{humanize_date(invitation.parent.date_and_time, with_time: true)}
- if invitation.is_a? WorkshopInvitation
= link_to attendance_status(invitation), invitation_path(invitation), class: 'btn btn-sm btn-primary', role: 'button'
- if invitation.is_a? InvitationPresenter
= link_to invitation.attendance_status, invitation_path(invitation),
class: 'btn btn-sm btn-primary', role: 'button'

- elsif @upcoming_workshop
%p You have no invitations. If you just signed up you should receive one soon.
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
get 'attendees_checklist'
get 'attendees_emails'
get 'send_invites'
get 'changes'

resource :invitations, only: [:update]
resources :invitations, only: [:update]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddLastOverriddenByIdToWorkshopInvitations < ActiveRecord::Migration[7.0]
def change
add_column :workshop_invitations, :last_overridden_by_id, :integer, default: nil
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_08_03_225601) do
ActiveRecord::Schema[7.0].define(version: 2023_12_30_162506) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

Expand Down Expand Up @@ -526,6 +526,7 @@
t.datetime "rsvp_time", precision: nil
t.boolean "automated_rsvp"
t.text "tutorial"
t.integer "last_overridden_by_id"
t.index ["member_id"], name: "index_workshop_invitations_on_member_id"
t.index ["token"], name: "index_workshop_invitations_on_token", unique: true
t.index ["workshop_id"], name: "index_workshop_invitations_on_workshop_id"
Expand Down
22 changes: 17 additions & 5 deletions spec/controllers/admin/invitations_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,27 @@
expect(flash[:notice]).to match("You have added")
end

it "Warns the user about failed updates" do
# Trigger an error when trying to update the `attending` attribute
# 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)
expect(invitation.automated_rsvp).to be_nil

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")
end

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

# State didn't change and we have an error message explaining why
expect(invitation.reload.attending).to be_nil
expect(flash[:notice]).to match("Tutorial must be selected.")
expect(invitation.last_overridden_by_id).to be admin.id
end
end
end
37 changes: 35 additions & 2 deletions spec/features/admin/manage_workshop_attendances_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

expect(page).to have_content('2 are attending as students')
expect(page).to have_content(I18n.l(other_invitation.reload.rsvp_time))
expect(page).to have_selector('i.fa-magic')
expect(page).to have_selector('i.fa-hat-wizard')
end

scenario 'can rsvp an invited student to the workshop', js: true do
Expand All @@ -56,7 +56,7 @@
expect(page).to have_content('2 are attending as students')

expect(page).to have_content(I18n.l(other_invitation.reload.rsvp_time))
expect(page).to have_css('.fa-magic')
expect(page).to have_css('.fa-hat-wizard')
end

scenario 'can view the tutorial and note set by an attendee' do
Expand All @@ -67,5 +67,38 @@
expect(page).to have_content(invitation.note)
expect(page).to have_content(invitation.tutorial)
end

context '#changes' do
before do
# Workshop invitations without `attending` status
Fabricate(:workshop_invitation, workshop: workshop, role: 'Coach')
Fabricate(:workshop_invitation, workshop: workshop, role: 'Student')

# Not attending
Fabricate(:workshop_invitation, workshop: workshop, role: 'Coach', attending: false)
Fabricate(:workshop_invitation, workshop: workshop, role: 'Student', attending: false)

# Attending, with a student having been manually added/confirmed by an organiser
Fabricate(:attending_workshop_invitation, workshop: workshop, role: 'Coach')
Fabricate(:attending_workshop_invitation, workshop: workshop, role: 'Student')
overridden = Fabricate(:attending_workshop_invitation, workshop: workshop, role: 'Student')
overridden.update(last_overridden_by_id: member.id)
end

scenario 'can verify if a invitation has been overridden by an organiser' do
visit admin_workshop_changes_path(workshop)

expect(page).to have_css(
'.coaches-table tbody tr',
count: workshop.invitations.to_coaches.where.not(attending: nil).count
)
expect(page).to have_css(
'.students-table tbody tr',
count: workshop.invitations.to_students.where.not(attending: nil).count
)

expect(page).to have_link(member.name_and_surname, href: admin_member_path(member.id))
end
end
end
end