diff --git a/app/assets/javascripts/invitations.js b/app/assets/javascripts/invitations.js
index 7a496b8f3..fd57836ee 100644
--- a/app/assets/javascripts/invitations.js
+++ b/app/assets/javascripts/invitations.js
@@ -1,14 +1,22 @@
$(document).ready(function() {
$(document).on("ajax:success", "#invitations [data-remote]", function(e) {
var xhr = e.detail[2];
- var $invitations = $("#invitations");
+ var $link = $(e.currentTarget);
- $invitations.html(xhr.responseText);
+ if ($link.hasClass('verify_attendance')) {
+ var $row = $link.closest('.row.attendee');
+ var rowId = $row.attr('id');
+ $row.replaceWith(xhr.responseText);
+ $(".row.attendee[id='" + rowId + "']").find('[data-bs-toggle="tooltip"]').tooltip();
+ } else {
+ var $invitations = $("#invitations");
+ $invitations.html(xhr.responseText);
- $invitations.find('select').chosen(function () {
- allow_single_deselect: true
- no_results_text: 'No results matched'
- });
+ $invitations.find('select').chosen({
+ allow_single_deselect: true,
+ no_results_text: 'No results matched'
+ });
+ }
});
$(document).on('change','#workshop_invitations ',function() {
diff --git a/app/controllers/admin/invitations_controller.rb b/app/controllers/admin/invitations_controller.rb
index 28143b15b..c65d06924 100644
--- a/app/controllers/admin/invitations_controller.rb
+++ b/app/controllers/admin/invitations_controller.rb
@@ -9,8 +9,12 @@ def update
message = update_attendance(attending: params[:attending], attended: params[:attended])
if request.xhr?
- set_admin_workshop_data
- render partial: 'admin/workshops/invitation_management'
+ if params[:attended].present?
+ render partial: 'admin/workshop/attendance_row', locals: { invitation: InvitationPresenter.new(@invitation), workshop: @workshop }
+ else
+ set_admin_workshop_data
+ render partial: 'admin/workshops/invitation_management'
+ end
else
redirect_back fallback_location: root_path, notice: message
end
@@ -19,7 +23,8 @@ def update
private
def update_attendance(attending:, attended:)
- return update_to_attended if attended
+ return update_to_attended if attended.to_s == 'true'
+ return update_to_unattended if attended.to_s == 'false'
result = attending.eql?('true') ? update_to_attending : update_to_not_attending
message, error = result.values_at(:message, :error)
@@ -34,8 +39,10 @@ def update_attendance(attending:, attended:)
def update_to_attended
@invitation.update(attended: true)
+ end
- "You have verified #{@invitation.member.full_name}’s attendace."
+ def update_to_unattended
+ @invitation.update(attended: nil)
end
def update_to_attending
diff --git a/app/models/waiting_list.rb b/app/models/waiting_list.rb
index 92fe00d6e..7c3ee7abe 100644
--- a/app/models/waiting_list.rb
+++ b/app/models/waiting_list.rb
@@ -6,7 +6,7 @@ class WaitingList < ApplicationRecord
scope :by_workshop, ->(workshop) { joins(:invitation).where('workshop_invitations.workshop_id = ?', workshop.id) }
scope :where_role, ->(role) { where('workshop_invitations.role = ?', role) }
- scope :with_notes_and_their_authors, -> { includes(member: { member_notes: :author }) }
+ scope :with_notes_and_their_authors, -> { includes(member: [{ member_notes: :author }, :attendance_warnings]) }
def self.add(invitation, auto_rsvp = true)
find_or_create_by(invitation: invitation) do |waiting_list|
diff --git a/app/models/workshop_invitation.rb b/app/models/workshop_invitation.rb
index 59d20959b..d4dd6596d 100644
--- a/app/models/workshop_invitation.rb
+++ b/app/models/workshop_invitation.rb
@@ -23,7 +23,7 @@ class WorkshopInvitation < ApplicationRecord
scope :last_six_months, -> { joins(:workshop).where(workshops: { date_and_time: 6.months.ago...Time.zone.now }) }
scope :not_reminded, -> { where(reminded_at: nil) }
scope :on_waiting_list, -> { joins(:waiting_list) }
- scope :with_notes_and_their_authors, -> { includes(member: { member_notes: :author }) }
+ scope :with_notes_and_their_authors, -> { includes(member: [{ member_notes: :author }, :attendance_warnings]).includes(:overrider) }
after_save :clear_member_cache, if: :saved_change_to_attending?
diff --git a/app/views/admin/workshop/_attendance_row.html.haml b/app/views/admin/workshop/_attendance_row.html.haml
new file mode 100644
index 000000000..49206684b
--- /dev/null
+++ b/app/views/admin/workshop/_attendance_row.html.haml
@@ -0,0 +1,63 @@
+-# locals: (invitation:, workshop:)
+.row.attendee.mt-3{ id: "attendee-row-#{invitation.id}" }
+ .col-1
+ - if (workshop.date_and_time - 30.minutes).past?
+ - if invitation.attended.nil?
+ = link_to admin_workshop_invitation_path(workshop, invitation, attended: true),
+ class: 'verify_attendance', title: 'Verify attendance', method: :put, remote: true,
+ data: { disable_with: "" } do
+ %i.far.fa-square
+ - else
+ = link_to admin_workshop_invitation_path(workshop, invitation, attended: false),
+ class: 'verify_attendance', title: 'Unverify attendance', method: :put, remote: true,
+ data: { disable_with: "" } do
+ %i.far.fa-check-square
+ - else
+ = link_to admin_workshop_invitation_path(workshop, invitation, attending: false), class: 'cancel_attendance float-right', title: 'Cancel RSVP', data: { confirm: "Are you sure you want to remove #{invitation.member.full_name} from the list of attendees?" }, method: :put, remote: true do
+ %i.far.fa-minus-square
+ .col-5
+ - if invitation.member.newbie?
+ %span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'New attendee.' }
+ %i.fas.fa-paw
+ - elsif invitation.member.flag_to_organisers?
+ %span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'Multiple no shows and attendance warnings in the last six months' }
+ %i.fas.fa-exclamation-circle
+
+ - if invitation.member.recent_notes.any?
+ %span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'Note recently added'}
+ %i.fas.fa-comment
+
+ = link_to admin_member_path(invitation.member) do
+ - if invitation.member.full_name.blank?
+ = invitation.member.email
+ - else
+ = invitation.member.full_name
+ - if invitation.member.dietary_restrictions.present?
+ %p
+ - invitation.member.displayed_dietary_restrictions.each do |dr|
+ %span.badge.bg-secondary.text-wrap.text-break.mb-1.text-start= dr
+ .col-6
+ - if invitation.tutorial?
+ %p.mb-1 Tutorial: #{invitation.tutorial}
+ - if invitation.note?
+ %p.mb-1 Note: #{invitation.note}
+ - if invitation.rsvp_time.present?
+ %span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'Time of RSVP'}
+ %p.mb-1.small
+ %i.fas.fa-history
+ = l(invitation.rsvp_time)
+ - if invitation.automated_rsvp?
+ - 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
+ %i.far.fa-clock
+ = l(invitation.reminded_at)
diff --git a/app/views/admin/workshop/_attendances.html.haml b/app/views/admin/workshop/_attendances.html.haml
index 13e79fe6e..d43ce1d53 100644
--- a/app/views/admin/workshop/_attendances.html.haml
+++ b/app/views/admin/workshop/_attendances.html.haml
@@ -1,62 +1,4 @@
.row
.col.attendances
- invitations.each do |invitation|
- .row.attendee.mt-3
- .col-1
- - if (invitation.parent.date_and_time - 30.minutes).past?
- - if invitation.attended.nil?
- = link_to admin_workshop_invitation_path(@workshop, invitation, attended: true),
- class: 'verify_attendance', title: 'Verify attendance', method: :put, remote: true,
- data: { disable_with: "" } do
- %i.far.fa-square
- - else
- %i.far.fa-check-square
- - else
- = link_to admin_workshop_invitation_path(@workshop, invitation, attending: false), class: 'cancel_attendance float-right', title: 'Cancel RSVP', data: { confirm: "Are you sure you want to remove #{invitation.member.full_name} from the list of attendees?" }, method: :put, remote: true do
- %i.far.fa-minus-square
- .col-5
- - if invitation.member.newbie?
- %span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'New attendee.' }
- %i.fas.fa-paw
- - elsif invitation.member.flag_to_organisers?
- %span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'Multiple no shows and attendance warnings in the last six months' }
- %i.fas.fa-exclamation-circle
-
- - if invitation.member.recent_notes.any?
- %span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'Note recently added'}
- %i.fas.fa-comment
-
- = link_to admin_member_path(invitation.member) do
- - if invitation.member.full_name.blank?
- = invitation.member.email
- - else
- = invitation.member.full_name
- - if invitation.member.dietary_restrictions.present?
- %p
- - invitation.member.displayed_dietary_restrictions.each do |dr|
- %span.badge.bg-secondary.text-wrap.text-break.mb-1.text-start= dr
- .col-6
- - if invitation.tutorial?
- %p.mb-1 Tutorial: #{invitation.tutorial}
- - if invitation.note?
- %p.mb-1 Note: #{invitation.note}
- - if invitation.rsvp_time.present?
- %span{'data-bs-toggle': 'tooltip', 'data-bs-placement': 'bottom', title: 'Time of RSVP'}
- %p.mb-1.small
- %i.fas.fa-history
- = l(invitation.rsvp_time)
- - if invitation.automated_rsvp?
- - 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
- %i.far.fa-clock
- = l(invitation.reminded_at)
+ = render partial: 'admin/workshop/attendance_row', locals: { invitation: invitation, workshop: @workshop }
diff --git a/spec/controllers/admin/invitations_controller_spec.rb b/spec/controllers/admin/invitations_controller_spec.rb
index efd1c88d5..2e50b5fd0 100644
--- a/spec/controllers/admin/invitations_controller_spec.rb
+++ b/spec/controllers/admin/invitations_controller_spec.rb
@@ -43,4 +43,34 @@
expect(invitation.last_overridden_by_id).to be admin.id
end
end
+
+ describe "PUT #update with attended param" do
+ let(:workshop) { Fabricate(:workshop, date_and_time: Time.zone.now - 1.day) }
+ let(:invitation) { Fabricate(:workshop_invitation, workshop: workshop, attending: true) }
+ let(:admin) { Fabricate(:chapter_organiser) }
+
+ before do
+ admin.add_role(:organiser, workshop.chapter)
+ login admin
+ request.env["HTTP_REFERER"] = "/admin/workshop/#{workshop.id}"
+ end
+
+ it "renders the attendance row partial via XHR when verifying" do
+ put :update, params: { id: invitation.token, workshop_id: workshop.id, attended: true }, xhr: true
+
+ expect(response).to have_http_status(:success)
+ expect(response.media_type).to eq('text/html')
+ expect(invitation.reload.attended).to be true
+ end
+
+ it "renders the attendance row partial via XHR when unverifying" do
+ invitation.update!(attended: true)
+
+ put :update, params: { id: invitation.token, workshop_id: workshop.id, attended: false }, xhr: true
+
+ expect(response).to have_http_status(:success)
+ expect(response.media_type).to eq('text/html')
+ expect(invitation.reload.attended).to be_nil
+ end
+ end
end
diff --git a/spec/controllers/admin/workshops_controller_spec.rb b/spec/controllers/admin/workshops_controller_spec.rb
index 66c338cbe..f580b75d6 100644
--- a/spec/controllers/admin/workshops_controller_spec.rb
+++ b/spec/controllers/admin/workshops_controller_spec.rb
@@ -6,6 +6,15 @@
login_as_organiser(admin, workshop.chapter)
end
+ describe 'GET #show' do
+ it 'loads the workshop attendance page with attendees' do
+ Fabricate(:workshop_invitation, workshop: workshop, attending: true)
+ get :show, params: { id: workshop.id }
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+
describe 'DELETE #destroy' do
context 'workshop invitations have been sent' do
before do
diff --git a/spec/features/admin/manage_workshop_attendances_spec.rb b/spec/features/admin/manage_workshop_attendances_spec.rb
index 9e2192e14..e0e26f2ff 100644
--- a/spec/features/admin/manage_workshop_attendances_spec.rb
+++ b/spec/features/admin/manage_workshop_attendances_spec.rb
@@ -20,6 +20,27 @@
expect(page).to have_css('.fa-check-square')
end
+
+ scenario 'verifies and unverifies attendance with targeted row replacement', js: true do
+ second_invitation = Fabricate(:workshop_invitation, workshop: workshop, attending: true)
+
+ visit admin_workshop_path(workshop)
+
+ # Capture the ID of the second row before clicking
+ second_row_id = "attendee-row-#{second_invitation.id}"
+ expect(page).to have_selector("##{second_row_id}")
+
+ # Verify first attendee
+ first('.verify_attendance').click
+ expect(page).to have_css('.fa-check-square', count: 1, wait: 5)
+
+ # Unverify the same attendee
+ first('.verify_attendance').click
+ expect(page).to have_css('.fa-check-square', count: 0, wait: 5)
+
+ # The unclicked row should still exist with its original ID, proving targeted replacement
+ expect(page).to have_selector("##{second_row_id}")
+ end
end
scenario 'can remove a member from the attendee list' do