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

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

48 

49 return kioskUser, userStatus 

50 

51def checkUserRsvp(user, event): 

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

53 

54def checkUserVolunteer(user, event): 

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

56 

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) 

62 

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) 

78 

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}.") 

84 

85 if volunteerExists or rsvpExists: 

86 return "already in" 

87 except Exception as e: 

88 print(e) 

89 return False 

90 

91 return True 

92 

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

97 

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 [] 

110 

111 

112def getEventParticipants(event): 

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

114 .join(User) 

115 .where(EventParticipant.event == event)) 

116 

117 return [p for p in eventParticipants] 

118 

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. 

123 

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 

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) 

145 

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} 

158 

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 } 

178 

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 

205 

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" 

220 

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

222 

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 

231 

232 return trainedAndInterested 

233 

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 

248 

249 

250def sortParticipantsByStatus(event): 

251 """ 

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

253 participants by their attendee status. 

254 

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) 

260 

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] 

264 

265 if event.isPastStart: 

266 eventVolunteerData = eventParticipants 

267 

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] 

273 

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 = [] 

277 

278 return eventNonAttendedData, eventWaitlistData, eventVolunteerData, eventParticipants 

279 

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.  

284  

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 ) 

300 

301 if not attended: 

302 return None 

303 if len(attended) > 1: 

304 attended = attended[-1] 

305 return attended.get().event 

306