Coverage for app/logic/participants.py: 67%
143 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-09-04 14:24 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-09-04 14:24 +0000
1from flask import g
2from peewee import fn, JOIN
3from playhouse.shortcuts import model_to_dict
4from datetime import date, datetime
5from app.logic.users import isEligibleForProgram
6from app.models.user import User
7from app.models.event import Event
8from app.models.term import Term
9from app.models.eventRsvp import EventRsvp
10from app.models.program import Program
11from app.models.programBan import ProgramBan
12from app.models.eventParticipant import EventParticipant
13from app.models.backgroundCheck import BackgroundCheck
14from app.logic.volunteers import getEventLengthInHours
15from app.logic.events import getEventRsvpCountsForTerm
16from app.logic.createLogs import createRsvpLog
17from collections import defaultdict
18from app import app
22def addBnumberAsParticipant(bnumber, eventId):
23 """
24 Accepts scan input and signs in the user. If user exists or is already
25 signed in will return user and login status
26 """
27 try:
28 kioskUser = User.get(User.bnumber == bnumber)
29 except Exception as e:
30 print(e)
31 return None, "does not exist"
33 event = Event.get_by_id(eventId)
34 if (ProgramBan.select().where(ProgramBan.user == kioskUser, ProgramBan.program == event.program, ProgramBan.endDate > datetime.now(), ProgramBan.unbanNote == None).exists()):
35 userStatus = "banned"
37 elif checkUserVolunteer(kioskUser, event):
38 userStatus = "already signed in"
40 else:
41 userStatus = "success"
42 # We are not using addPersonToEvent to do this because
43 # that function checks if the event is in the past, but
44 # someone could start signing people up via the kiosk
45 # before an event has started
46 totalHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate)
47 EventParticipant.create (user=kioskUser, event=event, hoursEarned=totalHours)
49 return kioskUser, userStatus
51def checkUserRsvp(user, event):
52 return EventRsvp.select().where(EventRsvp.user==user, EventRsvp.event == event).exists()
54def checkUserVolunteer(user, event):
55 return EventParticipant.select().where(EventParticipant.user == user, EventParticipant.event == event).exists()
57def addPersonToEvent(user, event):
58 """
59 Add a user to an event.
60 If the event is in the past, add the user as a volunteer (EventParticipant) including hours worked.
61 If the event is in the future, rsvp for the user (EventRsvp)
63 Returns True if the operation was successful, false otherwise
64 """
65 try:
66 volunteerExists = checkUserVolunteer(user, event)
67 rsvpExists = checkUserRsvp(user, event)
68 if event.isPastStart:
69 if not volunteerExists:
70 # We duplicate these two lines in addBnumberAsParticipant
71 eventHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate)
72 EventParticipant.create(user = user, event = event, hoursEarned = eventHours)
73 else:
74 if not rsvpExists:
75 currentRsvp = getEventRsvpCountsForTerm(event.term)
76 waitlist = currentRsvp[event.id] >= event.rsvpLimit if event.rsvpLimit is not None else 0
77 EventRsvp.create(user = user, event = event, rsvpWaitlist = waitlist)
79 targetList = "the waitlist" if waitlist else "the RSVP list"
80 if g.current_user.username == user.username:
81 createRsvpLog(event.id, f"{user.fullName} joined {targetList}.")
82 else:
83 createRsvpLog(event.id, f"Added {user.fullName} to {targetList}.")
85 if volunteerExists or rsvpExists:
86 return "already in"
87 except Exception as e:
88 print(e)
89 return False
91 return True
93def unattendedRequiredEvents(program, user):
94 # Check for events that are prerequisite for program
95 requiredEvents = (Event.select(Event)
96 .where(Event.isTraining == True, Event.program == program))
98 if requiredEvents:
99 attendedRequiredEventsList = []
100 for event in requiredEvents:
101 attendedRequirement = (EventParticipant.select()
102 .join(User)
103 .where(EventParticipant.user == User.username, EventParticipant.event == event))
104 if not attendedRequirement:
105 attendedRequiredEventsList.append(event.name)
106 if attendedRequiredEventsList is not None:
107 return attendedRequiredEventsList
108 else:
109 return []
112def getEventParticipants(event):
113 eventParticipants = (EventParticipant.select(EventParticipant, User)
114 .join(User)
115 .where(EventParticipant.event == event))
117 return [p for p in eventParticipants]
119def getParticipationStatusForTrainings(program, userList, term, includeFutureEvents = True):
120 """
121 This function returns a dictionary of all trainings for a program and
122 whether the current user participated in them.
124 :returns: trainings for program and if the user participated
125 """
126 isRelevantTraining = ((Event.isAllVolunteerTraining | Event.isCeltsTraining | ((Event.isTraining) & (Event.program == program))) &
127 (Event.term.academicYear == term.academicYear))
128 programTrainings = (Event.select(Event, Term, EventParticipant, EventRsvp)
129 .join(EventParticipant, JOIN.LEFT_OUTER).switch()
130 .join(EventRsvp, JOIN.LEFT_OUTER).switch()
131 .join(Term)
132 .where(isRelevantTraining, (Event.isCanceled != True)).order_by(Event.startDate))
133 if not includeFutureEvents:
134 programTrainings = programTrainings.where(
135 (Event.startDate < datetime.now().date()) |
136 ((Event.startDate == datetime.now().date()) & (Event.timeStart <= datetime.now().time())))
137 # Create a dictionary where the keys are trainings and values are a set of those who attended
138 trainingData = defaultdict(set)
139 for training in programTrainings:
140 try:
141 if training.isPastStart:
142 trainingData[training].add(training.eventparticipant.user_id)
143 else: # The training has yet to happen
144 trainingData[training].add(training.eventrsvp.user_id)
145 except AttributeError:
146 pass
147 # Create a dictionary binding usernames to a list of [training, hasAttended] pairs. The tuples consist of the training (event object) and whether or not they attended it (bool)
149 # Necessarily complex algorithm to merge the attendances of trainings which have the same name
150 # Structure of userParticipationStatus for a single user:
151 # {user.username: {training1.name: [EventObject, hasAttended], training2.name: [EventObject, hasAttended]}, ...}
152 userParticipationStatus = {user.username: {} for user in userList}
153 for training, attendeeList in trainingData.items():
154 for user in userList:
155 if training.name not in userParticipationStatus[user.username] or user.username in attendeeList:
156 userParticipationStatus[user.username][training.name] = [training, user.username in attendeeList]
157 if includeFutureEvents:
158 return {user.username: list(userParticipationStatus[user.username].values()) for user in userList}
159 else:
160 return {user: list(userParticipationStatus[user.username].values()) for user in userList}
162def getTrainingsForInterestedParticipants(programID, interestedUsers):
163 """
164 Takes in a programID and a list of interested users, and returns all of the trainings and background checks they have completed.
165 Returns a nested dictionary which looks like the following:
166 {'userID1': {'userObj': <User>,
167 'allVolunteer': True,
168 'programSpecific': False,
169 'bgCheck': '0/22/2026',
170 'eligible': True,
171 'star': False
172 },
173 'userID2': {'userObj': <User>,
174 'allVolunteer': True,
175 'programSpecific': True,
176 'bgCheck': '0/22/2026',
177 'eligible': True,
178 'star': True
179 }
180 }
182 Gracefully handles multiple trainings of the same type (e.g., two All Volunteers Trainings)
183 """
184 trainedUsers = getParticipationStatusForTrainings(programID, interestedUsers, g.current_term, includeFutureEvents = False)
185 now = datetime.now()
186 bannedUsers = list(User
187 .select(User.username)
188 .join(ProgramBan)
189 .where(ProgramBan.program == programID,
190 ProgramBan.endDate > now,
191 ProgramBan.unbanNote == None,
192 User.username << [user.username for user in interestedUsers]))
193 bgCheckSubmitted = (User.select(User.username, BackgroundCheck.dateCompleted)
194 .join(BackgroundCheck)
195 .where(BackgroundCheck.user == User.username,
196 BackgroundCheck.deletionDate.is_null())
197 .distinct())
198 trainedAndInterested = {}
199 for interestedUser in interestedUsers:
200 if interestedUser in trainedUsers:
201 trainedAndInterested[interestedUser.username] = {}
202 trainedAndInterested[interestedUser.username]["userObj"] = interestedUser
203 trainedAndInterested[interestedUser.username]['allVolunteer'] = False
204 trainedAndInterested[interestedUser.username]['programSpecific'] = False
205 trainedAndInterested[interestedUser.username]['bgCheck'] = "Not submitted"
206 trainedAndInterested[interestedUser.username]["eligible"] = True
207 trainedAndInterested[interestedUser.username]["star"] = False
209 # Go through the trainings
210 for event in trainedUsers[interestedUser]:
211 if not event[1]: # they didn't attend this training
212 continue
213 elif event[0].isAllVolunteerTraining or event[0].isCeltsTraining: # They attended AVT or ACT
214 trainedAndInterested[interestedUser.username]["allVolunteer"] = True
215 elif event[0].isTraining: # They attended the Program-specific training
216 trainedAndInterested[interestedUser.username]["programSpecific"] = True
217 # They are banned
218 if interestedUser in bannedUsers:
219 trainedAndInterested[interestedUser.username]["eligible"] = False
220 # They submitted their background check
221 if interestedUser in bgCheckSubmitted:
222 trainedAndInterested[interestedUser.username]['bgCheck'] = "Submitted"
224 # NOTE: Handbook signature already tracked inside the user object
226 # Give them a star if they have met all the requirements
227 if ( trainedAndInterested[interestedUser.username]["allVolunteer"] and
228 trainedAndInterested[interestedUser.username]["programSpecific"] and
229 trainedAndInterested[interestedUser.username]["eligible"] and
230 trainedAndInterested[interestedUser.username]['bgCheck'] == "Submitted" and
231 trainedAndInterested[interestedUser.username]['userObj'].lastHandbookSignature is not None and
232 trainedAndInterested[interestedUser.username]['userObj'].signatureTerm.academicYear == g.current_term.academicYear):
233 trainedAndInterested[interestedUser.username]["star"] = True
235 return trainedAndInterested
237def getParticipantsForProgramForAY(program, academicYear):
238 participants = (User.select()
239 .join(EventParticipant)
240 .join(Event)
241 .join(Program)
242 .switch(Event)
243 .join(Term)
244 .where(Program.id == program,
245 Term.academicYear == academicYear,
246 User.hasGraduated == False,
247 EventParticipant.hoursEarned > 0)
248 .distinct()
249 )
250 return participants
253def sortParticipantsByStatus(event):
254 """
255 Takes in an event object, queries all participants, and then filters those
256 participants by their attendee status.
258 return: a list of participants who didn't attend, a list of participants who are waitlisted,
259 a list of participants who attended, and a list of all participants who have some status for the
260 event.
261 """
262 eventParticipants = getEventParticipants(event)
264 # get all RSVPs for event and filter out those that did not attend into separate list
265 eventRsvpData = list(EventRsvp.select(EventRsvp, User).join(User).where(EventRsvp.event==event).order_by(EventRsvp.rsvpTime))
266 eventNonAttendedData = [rsvp for rsvp in eventRsvpData if rsvp.user not in eventParticipants]
268 if event.isPastStart:
269 eventVolunteerData = eventParticipants
271 # if the event date has passed disregard the waitlist
272 eventWaitlistData = []
273 else:
274 # if rsvp is required for the event, grab all volunteers that are in the waitlist
275 eventWaitlistData = [volunteer for volunteer in (eventParticipants + eventRsvpData) if volunteer.rsvpWaitlist and event.isRsvpRequired]
277 # put the rest of the users that are not on the waitlist into the volunteer data
278 eventVolunteerData = [volunteer for volunteer in eventNonAttendedData if volunteer not in eventWaitlistData]
279 eventNonAttendedData = []
281 return eventNonAttendedData, eventWaitlistData, eventVolunteerData, eventParticipants
283def hasGoneToTraining(participant, term):
284 """
285 Taken in a User object, and returns which training (specifically, All volunteers training or All CELTS labor training) they attended for this term.
286 This is necessary for delivering the correct handbook to the student for signing.
288 return: A single event object of, in this order of precedence:
289 1) the All Celts training, if they attended,
290 2) the All Volunteers training, if they attended,
291 3) None
292 """
293 attended = (EventParticipant.select()
294 .join(User)
295 .switch(EventParticipant)
296 .join(Event)
297 .join(Term)
298 .where(User.username == participant.username,
299 Term.id == term.id,
300 Event.isAllVolunteerTraining | Event.isCeltsTraining)
301 .order_by(Event.isCeltsTraining)
302 )
303 if not attended:
304 return None
305 attended = attended[-1] if len(attended) > 1 else attended[0]
306 return attended.event