Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fe07fd0
Updated RSVP before and After Status
Meatchema Feb 23, 2026
1a5fecc
re-implemented the attended status for the non-rsvp events
Meatchema Feb 23, 2026
1fcf75e
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Mar 6, 2026
5c0226b
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Mar 9, 2026
129b188
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Mar 12, 2026
da9b9f5
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Mar 24, 2026
1137e06
Added Invited status into checkedbox javascript to allow for filtered…
Meatchema Mar 24, 2026
3ce5b3b
Merge branch 'development' of https://github.com/BCStudentSoftwareDev…
JohnCox2211 Mar 25, 2026
ea0d701
Add @propriety for RSVP and changed conditional to allow invited stat…
Meatchema Mar 30, 2026
03dfcf0
Added Conditional to allow accordion title of RSVP and Waitlist to ch…
Meatchema Mar 31, 2026
90aefc6
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Apr 2, 2026
7d89e1d
updated allLogs section to include invited user to create logs for bo…
Meatchema Apr 2, 2026
5ad6ee5
refined the way that the event message is passed rather than having t…
Meatchema Apr 2, 2026
afe5827
Updated Verbage of logs back to original verbage
Meatchema Apr 6, 2026
e4438dd
Changed ordering of table conditionals to prioritize the identificati…
Meatchema Apr 7, 2026
cc523f8
Added Marked message
Meatchema Apr 9, 2026
7a026e4
fixed format
Meatchema Apr 9, 2026
7b48463
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Apr 13, 2026
519291f
Added ability to created attended Logs after check boxing attended
Meatchema Apr 13, 2026
b3b88a2
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Apr 15, 2026
f38aaaf
Merge branch 'development' of https://github.com/BCStudentSoftwareDev…
Meatchema Apr 15, 2026
51043d0
Merge branch 'issue-1663-change-rsvp-language' of https://github.com/…
Meatchema Apr 15, 2026
01aff6c
removed old version of html status selector
Meatchema Apr 15, 2026
462c27e
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Apr 22, 2026
73d22ff
implemented a test for getTargetList condition function
Meatchema Apr 24, 2026
3b6e59c
Merge branch 'development' into issue-1663-change-rsvp-language
Meatchema Apr 29, 2026
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
29 changes: 27 additions & 2 deletions app/controllers/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions app/controllers/admin/volunteers.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +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)


Expand Down
86 changes: 63 additions & 23 deletions app/logic/participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to write a test for the new function

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have now implemented a test for the getTargetList function:

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To test this new function, this is the command:
pytest tests/code/test_participants.py::test_getTargetList

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your test looks good!

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.
Expand All @@ -80,22 +100,42 @@ 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)
try:
createRsvpLog(event.id, f"Marked {user.fullName} as attended.")
except Exception:
pass
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)
try:
createRsvpLog(event.id, f"Marked {user.fullName} as attended.")
except Exception:
pass
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"
Expand Down Expand Up @@ -193,8 +233,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
6 changes: 5 additions & 1 deletion app/logic/volunteers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from app.models.backgroundCheck import BackgroundCheck
from app.models.programManager import ProgramManager
from datetime import datetime, date
from app.logic.createLogs import createActivityLog
from app.logic.createLogs import createActivityLog, createRsvpLog

def getEventLengthInHours(startTime, endTime, eventDate):
"""
Expand Down Expand Up @@ -46,6 +46,10 @@ def updateEventParticipants(participantData):
.execute())
else:
EventParticipant.create(user=userObject, event=event, hoursEarned=hoursEarned)
try:
createRsvpLog(event.id, f"Marked {userObject.fullName} as attended.")
except Exception:
pass
else:
((EventParticipant.delete()
.where(EventParticipant.user==userObject.username, EventParticipant.event==event.id))
Expand Down
4 changes: 4 additions & 0 deletions app/models/eventRsvp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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), )
1 change: 1 addition & 0 deletions app/static/js/volunteerDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
8 changes: 6 additions & 2 deletions app/templates/events/manageVolunteers.html
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,11 @@ <h2 class="accordion-header">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed fs-4" type="button" data-bs-toggle="collapse" data-bs-target="#non-attended-collapse">
RSVP and Waitlist
{% if event.isRsvpRequired %}
RSVP and Waitlist
{% else %}
Invited and Waitlist
{% endif %}
</button>
</h2>
<div class="accordion-collapse collapse" id="non-attended-collapse">
Expand Down Expand Up @@ -280,7 +284,7 @@ <h2 class="accordion-header">
</td>
<td>{{participant.user.email}}</td>
<td>{{participant.user.phoneNumber}}</td>
<td>{{ 'Waitlist' if participant.rsvpWaitlist else 'RSVP' }}</td>
<td>{{ 'Waitlist' if participant.rsvpWaitlist else ('RSVP' if event.isRsvpRequired else 'Invited') }}</td>
<td>
<input
class="form-control number-only form-control input-sm"
Expand Down
44 changes: 22 additions & 22 deletions app/templates/events/volunteerDetails.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,31 +59,28 @@ <h4 class="nameSelect" nowrap><b>{{ participant.user.firstName }} {{ participant
{% macro printParticipants(type, attended, rsvp, waitlist) %}
{% set seen = [] %}
{% set combinedParticipants = attended + rsvp + waitlist %}
{% for p in combinedParticipants %}
{% set username = p.user.username %}
{% if username not in seen %}
{% set _ = seen.append(username) %}
{% set status = none %}
{% if p in attended %}
{% set status = 'attended'%}
{% elif p in rsvp %}
{% set status = 'rsvp'%}
{% elif p in waitlist %}
{% set status = 'waitlist'%}
{% endif %}
{% if status %}
{% if type == 'card' %}
{{ createCard(p, status) }}
{% elif type == 'table' %}
{{ createTable(p, status) }}
{% endif %}
{% endif %}
{% endif %}
{% for p in combinedParticipants | unique %}
{% if p in attended %}
{{createTable(p, 'attended') if type == 'table' else createCard(p, 'attended') }}
{% elif p in rsvp and not event.isRsvpRequired %}
{{createTable(p, 'invited') if type == 'table' else createCard(p, 'invited') }}
{% elif p in rsvp and event.isRsvpRequired %}
{{createTable(p, 'rsvp') if type == 'table' else createCard(p, 'rsvp') }}
{% elif p in waitlist %}
{{createTable(p, 'waitlisted') if type == 'table' else createCard(p, 'waitlisted') }}
{% endif %}

{% endfor %}
{% endmacro %}

{% macro createCheckbox(checkboxName) %}
{% set labelText = "RSVP" if checkboxName == 'rsvp' else 'Waitlist' if checkboxName == 'waitlist' else 'Attended' %}
{%- set labelMap = {
'rsvp': 'RSVP',
'waitlist': 'Waitlisted',
'attended': 'Attended',
'invited': 'Invited'
} -%}
{% set labelText = labelMap.get(checkboxName, 'Invited') %}
<input class="displayCheckbox noprint" type="checkbox" name="selected_items" id="{{checkboxName}}Select" checked>
<label for="{{checkboxName}}Select">{{labelText}}</label><br>
{% endmacro %}
Expand All @@ -105,7 +102,7 @@ <h3 style="text-align: center; width:180%;">
<div class="row">
<div class="col">
<label><b>Volunteer Groups:</b></label><br>
{% if rsvpUser %}
{% if rsvpUser and event.isRsvpRequired %}
{{createCheckbox('rsvp')}}
{% endif %}
{% if waitlistUser %}
Expand All @@ -114,6 +111,9 @@ <h3 style="text-align: center; width:180%;">
{% if attendedUser %}
{{createCheckbox('attended')}}
{% endif %}
{% if rsvpUser and not event.isRsvpRequired %}
{{createCheckbox('invited')}}
{% endif %}
</div>
<div class="col">
<label><b>Included Information:</b></label><br>
Expand Down
24 changes: 23 additions & 1 deletion tests/code/test_participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from app.models.program import Program
from app.models.eventParticipant import EventParticipant
from app.logic.volunteers import getEventLengthInHours, updateEventParticipants
from app.logic.participants import unattendedRequiredEvents, addBnumberAsParticipant, getEventParticipants, trainedParticipants, getParticipationStatusForTrainings, checkUserRsvp, checkUserVolunteer, addPersonToEvent, sortParticipantsByStatus
from app.logic.participants import getTargetList, unattendedRequiredEvents, addBnumberAsParticipant, getEventParticipants, trainedParticipants, getParticipationStatusForTrainings, checkUserRsvp, checkUserVolunteer, addPersonToEvent, sortParticipantsByStatus
from app.models.eventRsvp import EventRsvp


Expand Down Expand Up @@ -144,6 +144,28 @@ def test_addPersonToEvent():

transaction.rollback()

@pytest.mark.integration
def test_getTargetList():
with mainDB.atomic() as transaction:
current_term = Term.get(Term.isCurrentTerm == True)

# Test case 1: waitlist=True should always return "the waitlist"
event_rsvp = Event.create(term=current_term, program=9, isRsvpRequired=True)
assert getTargetList(event_rsvp, waitlist=True) == "the waitlist"

event_invited = Event.create(term=current_term, program=9, isRsvpRequired=False)
assert getTargetList(event_invited, waitlist=True) == "the waitlist"

# Test case 2: waitlist=False and isRsvpRequired=False should return "the Invited list"
assert event_invited.isRsvpRequired == False # Ensure the event has RSVP not required
assert getTargetList(event_invited, waitlist=False) == "the Invited list"

# Test case 3: waitlist=False and isRsvpRequired=True should return "the RSVP list"
assert event_rsvp.isRsvpRequired == True # Ensure the event has RSVP required
assert getTargetList(event_rsvp, waitlist=False) == "the RSVP list"

transaction.rollback()

@pytest.mark.integration
def test_updateEventParticipants():
with mainDB.atomic() as transaction:
Expand Down
Loading