Coverage for app/logic/participants.py: 58%

175 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2026-10-10 20:46 +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 

19 

20 

21 

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" 

32 

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" 

36 

37 elif checkUserVolunteer(kioskUser, event): 

38 userStatus = "already signed in" 

39 

40 else: 

41 # Non-RSVP and RSVP event handling 

42 userStatus = "success" 

43 if event.isRsvpRequired: 

44 # RSVP event: standard logic (RSVP before event, attend after) 

45 if event.isPastStart: 

46 totalHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) 

47 EventParticipant.create(user=kioskUser, event=event, hoursEarned=totalHours) 

48 else: 

49 if not checkUserRsvp(kioskUser, event): 

50 currentRsvp = getEventRsvpCountsForTerm(event.term) 

51 waitlist = currentRsvp[event.id] >= event.rsvpLimit if event.rsvpLimit is not None else False 

52 EventRsvp.create(user=kioskUser, event=event, rsvpWaitlist=waitlist) 

53 targetList = getTargetList(event, waitlist) 

54 try: 

55 if g.current_user.username == kioskUser.username: 

56 createRsvpLog(event.id, f"{kioskUser.fullName} joined {targetList}.") 

57 else: 

58 createRsvpLog(event.id, f"Added {kioskUser.fullName} to {targetList}.") 

59 except Exception: 

60 pass 

61 else: 

62 # Non-RSVP event: scanner entry ALWAYS marks as attended regardless of timing 

63 totalHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) 

64 EventParticipant.create(user=kioskUser, event=event, hoursEarned=totalHours) 

65 

66 return kioskUser, userStatus 

67 

68def checkUserRsvp(user, event): 

69 return EventRsvp.select().where(EventRsvp.user==user, EventRsvp.event == event).exists() 

70 

71def checkUserVolunteer(user, event): 

72 return EventParticipant.select().where(EventParticipant.user == user, EventParticipant.event == event).exists() 

73 

74def getTargetList(event, waitlist=False): 

75 return "the waitlist" if waitlist else "the Invited list" if not event.isRsvpRequired else "the RSVP list" 

76 

77def addPersonToEvent(user, event): 

78 """ 

79 Add a user to an event. 

80 If the event is in the past, add the user as a volunteer (EventParticipant) including hours worked. 

81 If the event is in the future, rsvp for the user (EventRsvp) 

82 

83 Returns True if the operation was successful, false otherwise 

84 """ 

85 try: 

86 volunteerExists = checkUserVolunteer(user, event) 

87 rsvpExists = checkUserRsvp(user, event) 

88 

89 if event.isRsvpRequired: 

90 # RSVP event logic 

91 if event.isPastStart: 

92 if not volunteerExists: 

93 eventHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) 

94 EventParticipant.create(user = user, event = event, hoursEarned = eventHours) 

95 try: 

96 createRsvpLog(event.id, f"Marked {user.fullName} as attended.") 

97 except Exception: 

98 pass 

99 else: 

100 if not rsvpExists: 

101 currentRsvp = getEventRsvpCountsForTerm(event.term) 

102 waitlist = currentRsvp[event.id] >= event.rsvpLimit if event.rsvpLimit is not None else 0 

103 EventRsvp.create(user = user, event = event, rsvpWaitlist = waitlist) 

104 targetList = "the waitlist" if waitlist else "the RSVP list" 

105 if g.current_user.username == user.username: 

106 createRsvpLog(event.id, f"{user.fullName} joined {targetList}.") 

107 else: 

108 createRsvpLog(event.id, f"Added {user.fullName} to {targetList}.") 

109 else: 

110 # Non-RSVP event logic 

111 if event.isPastStart: 

112 # After event: create EventParticipant (attended) 

113 if not volunteerExists: 

114 eventHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) 

115 EventParticipant.create(user = user, event = event, hoursEarned = eventHours) 

116 try: 

117 createRsvpLog(event.id, f"Marked {user.fullName} as attended.") 

118 except Exception: 

119 pass 

120 else: 

121 # Before event: create EventRsvp (invited status) 

122 if not rsvpExists: 

123 EventRsvp.create(user = user, event = event, rsvpWaitlist = False) 

124 

125 if volunteerExists or rsvpExists: 

126 return "already in" 

127 except Exception as e: 

128 print(e) 

129 return False 

130 

131 return True 

132 

133def unattendedRequiredEvents(program, user): 

134 # Check for events that are prerequisite for program 

135 requiredEvents = (Event.select(Event) 

136 .where(Event.isTraining == True, Event.program == program)) 

137 

138 if requiredEvents: 

139 attendedRequiredEventsList = [] 

140 for event in requiredEvents: 

141 attendedRequirement = (EventParticipant.select() 

142 .join(User) 

143 .where(EventParticipant.user == User.username, EventParticipant.event == event)) 

144 if not attendedRequirement: 

145 attendedRequiredEventsList.append(event.name) 

146 if attendedRequiredEventsList is not None: 

147 return attendedRequiredEventsList 

148 else: 

149 return [] 

150 

151 

152def getEventParticipants(event): 

153 eventParticipants = (EventParticipant.select(EventParticipant, User) 

154 .join(User) 

155 .where(EventParticipant.event == event)) 

156 

157 return [p for p in eventParticipants] 

158 

159def getParticipationStatusForTrainings(program, userList, term, includeFutureEvents = True): 

160 """ 

161 This function returns a dictionary of all trainings for a program and 

162 whether the current user participated in them. 

163 

164 :returns: trainings for program and if the user participated 

165 """ 

166 isRelevantTraining = ((Event.isAllVolunteerTraining | Event.isCeltsTraining | ((Event.isTraining) & (Event.program == program))) & 

167 (Event.term.academicYear == term.academicYear)) 

168 programTrainings = (Event.select(Event, Term, EventParticipant, EventRsvp) 

169 .join(EventParticipant, JOIN.LEFT_OUTER).switch() 

170 .join(EventRsvp, JOIN.LEFT_OUTER).switch() 

171 .join(Term) 

172 .where(isRelevantTraining, (Event.isCanceled != True)).order_by(Event.startDate)) 

173 if not includeFutureEvents: 

174 programTrainings = programTrainings.where( 

175 (Event.startDate < datetime.now().date()) | 

176 ((Event.startDate == datetime.now().date()) & (Event.timeStart <= datetime.now().time()))) 

177 # Create a dictionary where the keys are trainings and values are a set of those who attended 

178 trainingData = defaultdict(set) 

179 for training in programTrainings: 

180 try: 

181 if training.isPastStart: 

182 trainingData[training].add(training.eventparticipant.user_id) 

183 else: # The training has yet to happen 

184 trainingData[training].add(training.eventrsvp.user_id) 

185 except AttributeError: 

186 pass 

187 # 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) 

188 

189 # Necessarily complex algorithm to merge the attendances of trainings which have the same name 

190 # Structure of userParticipationStatus for a single user: 

191 # {user.username: {training1.name: [EventObject, hasAttended], training2.name: [EventObject, hasAttended]}, ...} 

192 userParticipationStatus = {user.username: {} for user in userList} 

193 for training, attendeeList in trainingData.items(): 

194 for user in userList: 

195 if training.name not in userParticipationStatus[user.username] or user.username in attendeeList: 

196 userParticipationStatus[user.username][training.name] = [training, user.username in attendeeList] 

197 if includeFutureEvents: 

198 return {user.username: list(userParticipationStatus[user.username].values()) for user in userList} 

199 else: 

200 return {user: list(userParticipationStatus[user.username].values()) for user in userList} 

201 

202def getTrainingsForInterestedParticipants(programID, interestedUsers): 

203 """ 

204 Takes in a programID and a list of interested users, and returns all of the trainings and background checks they have completed.  

205 Returns a nested dictionary which looks like the following:  

206 {'userID1': {'userObj': <User>, 

207 'allVolunteer': True, 

208 'programSpecific': False, 

209 'bgCheck': '0/22/2026', 

210 'eligible': True,  

211 'star': False 

212 },  

213 'userID2': {'userObj': <User>, 

214 'allVolunteer': True, 

215 'programSpecific': True, 

216 'bgCheck': '0/22/2026', 

217 'eligible': True,  

218 'star': True 

219 } 

220 } 

221 

222 Gracefully handles multiple trainings of the same type (e.g., two All Volunteers Trainings) 

223 """ 

224 trainedUsers = getParticipationStatusForTrainings(programID, interestedUsers, g.current_term, includeFutureEvents = False) 

225 now = datetime.now() 

226 bannedUsers = list(User 

227 .select(User.username) 

228 .join(ProgramBan) 

229 .where(ProgramBan.program == programID, 

230 ProgramBan.endDate > now, 

231 ProgramBan.unbanNote == None, 

232 User.username << [user.username for user in interestedUsers])) 

233 bgCheckSubmitted = (User.select(User.username, BackgroundCheck.dateCompleted) 

234 .join(BackgroundCheck) 

235 .where(BackgroundCheck.user == User.username, 

236 BackgroundCheck.deletionDate.is_null()) 

237 .distinct()) 

238 trainedAndInterested = {} 

239 for interestedUser in interestedUsers: 

240 if interestedUser in trainedUsers: 

241 trainedAndInterested[interestedUser.username] = {} 

242 trainedAndInterested[interestedUser.username]["userObj"] = interestedUser 

243 trainedAndInterested[interestedUser.username]['allVolunteer'] = False 

244 trainedAndInterested[interestedUser.username]['programSpecific'] = False 

245 trainedAndInterested[interestedUser.username]['bgCheck'] = "Not submitted" 

246 trainedAndInterested[interestedUser.username]["eligible"] = True 

247 trainedAndInterested[interestedUser.username]["star"] = False 

248 

249 # Go through the trainings 

250 for event in trainedUsers[interestedUser]: 

251 if not event[1]: # they didn't attend this training 

252 continue 

253 elif event[0].isAllVolunteerTraining or event[0].isCeltsTraining: # They attended AVT or ACT 

254 trainedAndInterested[interestedUser.username]["allVolunteer"] = True 

255 elif event[0].isTraining: # They attended the Program-specific training 

256 trainedAndInterested[interestedUser.username]["programSpecific"] = True 

257 # They are banned 

258 if interestedUser in bannedUsers: 

259 trainedAndInterested[interestedUser.username]["eligible"] = False 

260 # They submitted their background check 

261 if interestedUser in bgCheckSubmitted: 

262 trainedAndInterested[interestedUser.username]['bgCheck'] = "Submitted" 

263 

264 # NOTE: Handbook signature already tracked inside the user object 

265 

266 # Give them a star if they have met all the requirements 

267 if ( trainedAndInterested[interestedUser.username]["allVolunteer"] and 

268 trainedAndInterested[interestedUser.username]["programSpecific"] and 

269 trainedAndInterested[interestedUser.username]["eligible"] and 

270 trainedAndInterested[interestedUser.username]['bgCheck'] == "Submitted" and 

271 trainedAndInterested[interestedUser.username]['userObj'].lastHandbookSignature is not None and 

272 trainedAndInterested[interestedUser.username]['userObj'].signatureTerm.academicYear == g.current_term.academicYear): 

273 trainedAndInterested[interestedUser.username]["star"] = True 

274 

275 return trainedAndInterested 

276 

277def getParticipantsForProgramForAY(program, academicYear): 

278 participants = (User.select() 

279 .join(EventParticipant) 

280 .join(Event) 

281 .join(Program) 

282 .switch(Event) 

283 .join(Term) 

284 .where(Program.id == program, 

285 Term.academicYear == academicYear, 

286 User.hasGraduated == False, 

287 EventParticipant.hoursEarned > 0, 

288 # Only include currently enrolled undergraduate students. 

289 # This prevents alumni and past participants from appearing in the older reports. 

290 User.rawClassLevel.in_(["Freshman", "Sophomore", "Junior", "Senior"])) 

291 .distinct() 

292 ) 

293 return participants 

294 

295 

296def sortParticipantsByStatus(event): 

297 """ 

298 Takes in an event object, queries all participants, and then filters those 

299 participants by their attendee status. 

300 

301 return: a list of participants who didn't attend, a list of participants who are waitlisted, 

302 a list of participants who attended, and a list of all participants who have some status for the  

303 event. 

304 """ 

305 eventParticipants = getEventParticipants(event) 

306 

307 # get all RSVPs for event and filter out those that did not attend into separate list 

308 eventRsvpData = list(EventRsvp.select(EventRsvp, User).join(User).where(EventRsvp.event==event).order_by(EventRsvp.rsvpTime)) 

309 eventNonAttendedData = [rsvp for rsvp in eventRsvpData if rsvp.user not in eventParticipants] 

310 

311 if event.isPastStart: 

312 eventVolunteerData = eventParticipants 

313 

314 # if the event date has passed disregard the waitlist 

315 eventWaitlistData = [] 

316 else: 

317 # if rsvp is required for the event, grab all volunteers that are in the waitlist 

318 eventWaitlistData = [volunteer for volunteer in (eventParticipants + eventRsvpData) if volunteer.rsvpWaitlist and event.isRsvpRequired] 

319 

320 # put all participants and non-waitlisted RSVPs into the volunteer data 

321 eventVolunteerData = [volunteer for volunteer in (eventParticipants + eventNonAttendedData) if volunteer not in eventWaitlistData] 

322 eventNonAttendedData = [] 

323 

324 return eventNonAttendedData, eventWaitlistData, eventVolunteerData, eventParticipants 

325 

326def hasGoneToTraining(participant, term): 

327 """ 

328 Taken in a User object, and returns which training (specifically, All volunteers training or All CELTS labor training) they attended for this term. 

329 This is necessary for delivering the correct handbook to the student for signing.  

330  

331 return: A single event object of, in this order of precedence: 

332 1) the All Celts training, if they attended, 

333 2) the All Volunteers training, if they attended,  

334 3) None  

335 """ 

336 attended = (EventParticipant.select() 

337 .join(User) 

338 .switch(EventParticipant) 

339 .join(Event) 

340 .join(Term) 

341 .where(User.username == participant.username, 

342 Term.id == term.id, 

343 Event.isAllVolunteerTraining | Event.isCeltsTraining) 

344 .order_by(Event.isCeltsTraining) 

345 ) 

346 if not attended: 

347 return None 

348 attended = attended[-1] if len(attended) > 1 else attended[0] 

349 return attended.event 

350