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

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, includeFutureEvents = 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 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) 

148 

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} 

161 

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 } 

181 

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 

208 

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" 

223 

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

225 

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 

234 

235 return trainedAndInterested 

236 

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 

251 

252 

253def sortParticipantsByStatus(event): 

254 """ 

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

256 participants by their attendee status. 

257 

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) 

263 

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] 

267 

268 if event.isPastStart: 

269 eventVolunteerData = eventParticipants 

270 

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] 

276 

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

280 

281 return eventNonAttendedData, eventWaitlistData, eventVolunteerData, eventParticipants 

282 

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.  

287  

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 

307