Coverage for app/logic/participants.py: 67%
142 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-08-24 19:35 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-08-24 19:35 +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, returnStr = 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))
134 # Create a dictionary where the keys are trainings and values are a set of those who attended
135 trainingData = defaultdict(set)
136 for training in programTrainings:
137 try:
138 if training.isPastStart:
139 trainingData[training].add(training.eventparticipant.user_id)
140 else: # The training has yet to happen
141 trainingData[training].add(training.eventrsvp.user_id)
142 except AttributeError:
143 pass
144 # 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)
146 # Necessarily complex algorithm to merge the attendances of trainings which have the same name
147 # Structure of userParticipationStatus for a single user:
148 # {user.username: {training1.name: [EventObject, hasAttended], training2.name: [EventObject, hasAttended]}, ...}
149 userParticipationStatus = {user.username: {} for user in userList}
150 for training, attendeeList in trainingData.items():
151 for user in userList:
152 if training.name not in userParticipationStatus[user.username] or user.username in attendeeList:
153 userParticipationStatus[user.username][training.name] = [training, user.username in attendeeList]
154 if returnStr:
155 return {user.username: list(userParticipationStatus[user.username].values()) for user in userList}
156 else:
157 return {user: list(userParticipationStatus[user.username].values()) for user in userList}
159def getTrainingsForInterestedParticipants(programID, interestedUsers):
160 """
161 Takes in a programID and a list of interested users, and returns all of the trainings and background checks they have completed.
162 Returns a nested dictionary which looks like the following:
163 {'userID1': {'userObj': <User>,
164 'allVolunteer': True,
165 'programSpecific': False,
166 'bgCheck': '0/22/2026',
167 'eligible': True,
168 'star': False
169 },
170 'userID2': {'userObj': <User>,
171 'allVolunteer': True,
172 'programSpecific': True,
173 'bgCheck': '0/22/2026',
174 'eligible': True,
175 'star': True
176 }
177 }
179 Gracefully handles multiple trainings of the same type (e.g., two All Volunteers Trainings)
180 """
181 trainedUsers = getParticipationStatusForTrainings(programID, interestedUsers, g.current_term, returnStr = False)
182 now = datetime.now()
183 bannedUsers = list(User
184 .select(User.username)
185 .join(ProgramBan)
186 .where(ProgramBan.program == programID,
187 ProgramBan.endDate > now,
188 ProgramBan.unbanNote == None,
189 User.username << [user.username for user in interestedUsers]))
190 bgCheckSubmitted = (User.select(User.username, BackgroundCheck.dateCompleted)
191 .join(BackgroundCheck)
192 .where(BackgroundCheck.user == User.username,
193 BackgroundCheck.deletionDate.is_null())
194 .distinct())
195 trainedAndInterested = {}
196 for interestedUser in interestedUsers:
197 if interestedUser in trainedUsers:
198 trainedAndInterested[interestedUser.username] = {}
199 trainedAndInterested[interestedUser.username]["userObj"] = interestedUser
200 trainedAndInterested[interestedUser.username]['allVolunteer'] = False
201 trainedAndInterested[interestedUser.username]['programSpecific'] = False
202 trainedAndInterested[interestedUser.username]['bgCheck'] = "Not submitted"
203 trainedAndInterested[interestedUser.username]["eligible"] = True
204 trainedAndInterested[interestedUser.username]["star"] = False
206 # Go through the trainings
207 for event in trainedUsers[interestedUser]:
208 if not event[1]: # they didn't attend this training
209 continue
210 elif event[0].isAllVolunteerTraining: # They attended AVT
211 trainedAndInterested[interestedUser.username]["allVolunteer"] = True
212 elif event[0].isTraining: # They attended the Program-specific training
213 trainedAndInterested[interestedUser.username]["programSpecific"] = True
214 # They are banned
215 if interestedUser in bannedUsers:
216 trainedAndInterested[interestedUser.username]["eligible"] = False
217 # They submitted their background check
218 if interestedUser in bgCheckSubmitted:
219 trainedAndInterested[interestedUser.username]['bgCheck'] = "Submitted"
221 # NOTE: Handbook signature already tracked inside the user object
223 # Give them a star if they have met all the requirements
224 if ( trainedAndInterested[interestedUser.username]["allVolunteer"] and
225 trainedAndInterested[interestedUser.username]["programSpecific"] and
226 trainedAndInterested[interestedUser.username]["eligible"] and
227 trainedAndInterested[interestedUser.username]['bgCheck'] == "Submitted" and
228 trainedAndInterested[interestedUser.username]['userObj'].lastHandbookSignature is not None and
229 trainedAndInterested[interestedUser.username]['userObj'].signatureTerm.academicYear == g.current_term.academicYear):
230 trainedAndInterested[interestedUser.username]["star"] = True
232 return trainedAndInterested
234def getParticipantsForProgramForAY(program, academicYear):
235 participants = (User.select()
236 .join(EventParticipant)
237 .join(Event)
238 .join(Program)
239 .switch(Event)
240 .join(Term)
241 .where(Program.id == program,
242 Term.academicYear == academicYear,
243 User.hasGraduated == False,
244 EventParticipant.hoursEarned > 0)
245 .distinct()
246 )
247 return participants
250def sortParticipantsByStatus(event):
251 """
252 Takes in an event object, queries all participants, and then filters those
253 participants by their attendee status.
255 return: a list of participants who didn't attend, a list of participants who are waitlisted,
256 a list of participants who attended, and a list of all participants who have some status for the
257 event.
258 """
259 eventParticipants = getEventParticipants(event)
261 # get all RSVPs for event and filter out those that did not attend into separate list
262 eventRsvpData = list(EventRsvp.select(EventRsvp, User).join(User).where(EventRsvp.event==event).order_by(EventRsvp.rsvpTime))
263 eventNonAttendedData = [rsvp for rsvp in eventRsvpData if rsvp.user not in eventParticipants]
265 if event.isPastStart:
266 eventVolunteerData = eventParticipants
268 # if the event date has passed disregard the waitlist
269 eventWaitlistData = []
270 else:
271 # if rsvp is required for the event, grab all volunteers that are in the waitlist
272 eventWaitlistData = [volunteer for volunteer in (eventParticipants + eventRsvpData) if volunteer.rsvpWaitlist and event.isRsvpRequired]
274 # put the rest of the users that are not on the waitlist into the volunteer data
275 eventVolunteerData = [volunteer for volunteer in eventNonAttendedData if volunteer not in eventWaitlistData]
276 eventNonAttendedData = []
278 return eventNonAttendedData, eventWaitlistData, eventVolunteerData, eventParticipants
280def hasGoneToTraining(participant, term):
281 """
282 Taken in a User object, and returns which training (specifically, All volunteers training or All CELTS labor training) they attended for this term.
283 This is necessary for delivering the correct handbook to the student for signing.
285 return: A single event object of, in this order of precedence:
286 1) the All Celts training, if they attended,
287 2) the All Volunteers training, if they attended,
288 3) None
289 """
290 attended = (EventParticipant.select()
291 .join(User)
292 .switch(EventParticipant)
293 .join(Event)
294 .join(Term)
295 .where(User.username == participant.username,
296 Term.id == term.id,
297 Event.isAllVolunteerTraining | Event.isCeltsTraining)
298 .order_by(Event.isCeltsTraining)
299 )
301 if not attended:
302 return None
303 if len(attended) > 1:
304 attended = attended[-1]
305 return attended.get().event