diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 8d24ffdf6..36b217456 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -33,7 +33,7 @@ from app.logic.certification import getCertRequirements, updateCertRequirements from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRepeatingEventsData, deleteEventAndAllFollowing, deleteAllEventsInSeries, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewSeriesId, inviteCohortsToEvent, updateEventCohorts -from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp +from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp, getTargetList from app.logic.minor import getMinorInterest from app.logic.fileHandler import FileHandler from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog @@ -195,7 +195,32 @@ def createEvent(templateid, programid): def rsvpLogDisplay(eventId): event = Event.get_by_id(eventId) if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)): - allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc()) + # Existing RSVP-specific log entries + event_logs = list(EventRsvpLog.select(EventRsvpLog, User) + .join(User, on=(EventRsvpLog.createdBy == User.username)) + .where(EventRsvpLog.event_id == eventId)) + + # Include invited users from EventRsvp so the log display reflects invitations too + invited_rsvps = EventRsvp.select(EventRsvp, User).join(User).where(EventRsvp.event == eventId) + + from collections import namedtuple + LogEntry = namedtuple('LogEntry', ['createdOn', 'createdBy', 'rsvpLogContent']) + + allLogs = [] + allLogs.extend(event_logs) + + # Only add invitation logs for non-RSVP events, as for RSVP events, EventRsvp represents RSVPs, not invitations + if not event.isRsvpRequired: + for rsvp in invited_rsvps: + # Provide an explicit invitation action for EventRsvp records + allLogs.append(LogEntry( + createdOn=rsvp.rsvpTime, + createdBy=rsvp.user, + rsvpLogContent=f"Added {rsvp.user.fullName} to {getTargetList(event)}" + )) + + allLogs.sort(key=lambda entry: entry.createdOn, reverse=True) + return render_template("/events/rsvpLog.html", event = event, allLogs = allLogs) diff --git a/app/controllers/admin/volunteers.py b/app/controllers/admin/volunteers.py index 2dbe81479..1c2fd0c75 100644 --- a/app/controllers/admin/volunteers.py +++ b/app/controllers/admin/volunteers.py @@ -113,13 +113,15 @@ def volunteerDetailsPage(eventID): .where(EventParticipant.event==event)) - waitlistUser = list(set([obj for obj in eventRsvpData if obj.rsvpWaitlist])) - rsvpUser = list(set([obj for obj in eventRsvpData if not obj.rsvpWaitlist ])) - + attendedUser = list(set([obj for obj in eventParticipantData if not obj.rsvpWaitlist])) + attendedUserIds = {obj.user.id for obj in attendedUser} + waitlistUser = [obj for obj in eventRsvpData if obj.rsvpWaitlist and obj.user.id not in attendedUserIds] + rsvpUser = [obj for obj in eventRsvpData if not obj.rsvpWaitlist and obj.user.id not in attendedUserIds] + return render_template("/events/volunteerDetails.html", waitlistUser = waitlistUser, - attendedUser= eventParticipantData, - rsvpUser= rsvpUser, + attendedUser = attendedUser, + rsvpUser = rsvpUser, event = event) diff --git a/app/logic/participants.py b/app/logic/participants.py index 630ea0aea..ebf131882 100644 --- a/app/logic/participants.py +++ b/app/logic/participants.py @@ -53,13 +53,30 @@ def addBnumberAsParticipant(bnumber, eventId): userStatus = "already signed in" else: + # Non-RSVP and RSVP event handling userStatus = "success" - # We are not using addPersonToEvent to do this because - # that function checks if the event is in the past, but - # someone could start signing people up via the kiosk - # before an event has started - totalHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) - EventParticipant.create (user=kioskUser, event=event, hoursEarned=totalHours) + if event.isRsvpRequired: + # RSVP event: standard logic (RSVP before event, attend after) + if event.isPastStart: + totalHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) + EventParticipant.create(user=kioskUser, event=event, hoursEarned=totalHours) + else: + if not checkUserRsvp(kioskUser, event): + currentRsvp = getEventRsvpCountsForTerm(event.term) + waitlist = currentRsvp[event.id] >= event.rsvpLimit if event.rsvpLimit is not None else False + EventRsvp.create(user=kioskUser, event=event, rsvpWaitlist=waitlist) + targetList = getTargetList(event, waitlist) + try: + if g.current_user.username == kioskUser.username: + createRsvpLog(event.id, f"{kioskUser.fullName} joined {targetList}.") + else: + createRsvpLog(event.id, f"Added {kioskUser.fullName} to {targetList}.") + except Exception: + pass + else: + # Non-RSVP event: scanner entry ALWAYS marks as attended regardless of timing + totalHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) + EventParticipant.create(user=kioskUser, event=event, hoursEarned=totalHours) return kioskUser, userStatus @@ -69,6 +86,9 @@ def checkUserRsvp(user, event): def checkUserVolunteer(user, event): return EventParticipant.select().where(EventParticipant.user == user, EventParticipant.event == event).exists() +def getTargetList(event, waitlist=False): + return "the waitlist" if waitlist else "the Invited list" if not event.isRsvpRequired else "the RSVP list" + def addPersonToEvent(user, event): """ Add a user to an event. @@ -80,22 +100,34 @@ def addPersonToEvent(user, event): try: volunteerExists = checkUserVolunteer(user, event) rsvpExists = checkUserRsvp(user, event) - if event.isPastStart: - if not volunteerExists: - # We duplicate these two lines in addBnumberAsParticipant - eventHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) - EventParticipant.create(user = user, event = event, hoursEarned = eventHours) + + if event.isRsvpRequired: + # RSVP event logic + if event.isPastStart: + if not volunteerExists: + eventHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) + EventParticipant.create(user = user, event = event, hoursEarned = eventHours) + else: + if not rsvpExists: + currentRsvp = getEventRsvpCountsForTerm(event.term) + waitlist = currentRsvp[event.id] >= event.rsvpLimit if event.rsvpLimit is not None else 0 + EventRsvp.create(user = user, event = event, rsvpWaitlist = waitlist) + targetList = "the waitlist" if waitlist else "the RSVP list" + if g.current_user.username == user.username: + createRsvpLog(event.id, f"{user.fullName} joined {targetList}.") + else: + createRsvpLog(event.id, f"Added {user.fullName} to {targetList}.") else: - if not rsvpExists: - currentRsvp = getEventRsvpCountsForTerm(event.term) - waitlist = currentRsvp[event.id] >= event.rsvpLimit if event.rsvpLimit is not None else 0 - EventRsvp.create(user = user, event = event, rsvpWaitlist = waitlist) - - targetList = "the waitlist" if waitlist else "the RSVP list" - if g.current_user.username == user.username: - createRsvpLog(event.id, f"{user.fullName} joined {targetList}.") - else: - createRsvpLog(event.id, f"Added {user.fullName} to {targetList}.") + # Non-RSVP event logic + if event.isPastStart: + # After event: create EventParticipant (attended) + if not volunteerExists: + eventHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) + EventParticipant.create(user = user, event = event, hoursEarned = eventHours) + else: + # Before event: create EventRsvp (invited status) + if not rsvpExists: + EventRsvp.create(user = user, event = event, rsvpWaitlist = False) if volunteerExists or rsvpExists: return "already in" @@ -193,8 +225,8 @@ def sortParticipantsByStatus(event): # if rsvp is required for the event, grab all volunteers that are in the waitlist eventWaitlistData = [volunteer for volunteer in (eventParticipants + eventRsvpData) if volunteer.rsvpWaitlist and event.isRsvpRequired] - # put the rest of the users that are not on the waitlist into the volunteer data - eventVolunteerData = [volunteer for volunteer in eventNonAttendedData if volunteer not in eventWaitlistData] + # put all participants and non-waitlisted RSVPs into the volunteer data + eventVolunteerData = [volunteer for volunteer in (eventParticipants + eventNonAttendedData) if volunteer not in eventWaitlistData] eventNonAttendedData = [] return eventNonAttendedData, eventWaitlistData, eventVolunteerData, eventParticipants \ No newline at end of file diff --git a/app/models/eventRsvp.py b/app/models/eventRsvp.py index 8afdd47c2..d492a0e4c 100644 --- a/app/models/eventRsvp.py +++ b/app/models/eventRsvp.py @@ -9,6 +9,10 @@ class EventRsvp(baseModel): rsvpTime = DateTimeField(default=datetime.now) rsvpWaitlist = BooleanField(default=False) + @property + def rsvp(self): + # EventRsvp always represents an RSVP record, including invited participants. + return True class Meta: indexes = ( (('user', 'event'), True), ) diff --git a/app/static/js/volunteerDetails.js b/app/static/js/volunteerDetails.js index 1b6192a23..1b5aafdf7 100644 --- a/app/static/js/volunteerDetails.js +++ b/app/static/js/volunteerDetails.js @@ -28,6 +28,7 @@ $(document).ready(function () { const status = data[3].toLowerCase(); if (status === 'attended' && !$('#attendedSelect').is(':checked')) return false; if (status === 'rsvp' && !$('#rsvpSelect').is(':checked')) return false; + if (status === 'invited' && !$('#invitedSelect').is(':checked')) return false; if (status === 'waitlist' && !$('#waitlistSelect').is(':checked')) return false; return true; }); diff --git a/app/templates/events/manageVolunteers.html b/app/templates/events/manageVolunteers.html index aa66235fe..6b6c98aae 100644 --- a/app/templates/events/manageVolunteers.html +++ b/app/templates/events/manageVolunteers.html @@ -239,7 +239,11 @@