Coverage for app/controllers/main/routes.py: 25%

333 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-09-06 19:35 +0000

1import json 

2import datetime 

3from peewee import JOIN 

4from http import cookies 

5from playhouse.shortcuts import model_to_dict 

6from flask import request, render_template, jsonify, g, abort, flash, redirect, url_for, make_response, session, request 

7 

8from app.controllers.main import main_bp 

9from app import app 

10from app.models.term import Term 

11from app.models.user import User 

12from app.models.note import Note 

13from app.models.event import Event 

14from app.models.program import Program 

15from app.models.interest import Interest 

16from app.models.eventRsvp import EventRsvp 

17from app.models.celtsLabor import CeltsLabor 

18from app.models.programBan import ProgramBan 

19from app.models.profileNote import ProfileNote 

20from app.models.insuranceInfo import InsuranceInfo 

21from app.models.certification import Certification 

22from app.models.programManager import ProgramManager 

23from app.models.backgroundCheck import BackgroundCheck 

24from app.models.emergencyContact import EmergencyContact 

25from app.models.eventParticipant import EventParticipant 

26from app.models.courseInstructor import CourseInstructor 

27from app.models.backgroundCheckType import BackgroundCheckType 

28 

29from app.logic.events import getUpcomingEventsForUser, getParticipatedEventsForUser, getTrainingEvents, getEventRsvpCountsForTerm, getUpcomingStudentLedCount, getStudentLedEvents, getBonnerEvents, getOtherEvents 

30from app.logic.transcript import * 

31from app.logic.loginManager import logout 

32from app.logic.searchUsers import searchUsers 

33from app.logic.utils import selectSurroundingTerms 

34from app.logic.celtsLabor import getCeltsLaborHistory 

35from app.logic.createLogs import createRsvpLog, createActivityLog 

36from app.logic.certification import getCertRequirementsWithCompletion 

37from app.logic.landingPage import getManagerProgramDict, getActiveEventTab 

38from app.logic.minor import toggleMinorInterest, getCommunityEngagementByTerm, getEngagementTotal 

39from app.logic.participants import unattendedRequiredEvents, trainedParticipants, getParticipationStatusForTrainings, checkUserRsvp, addPersonToEvent 

40from app.logic.users import addUserInterest, removeUserInterest, banUser, unbanUser, isEligibleForProgram, getUserBGCheckHistory, addProfileNote, deleteProfileNote, updateDietInfo 

41 

42@main_bp.route('/logout', methods=['GET']) 

43def redirectToLogout(): 

44 return redirect(logout()) 

45 

46@main_bp.route('/', methods=['GET']) 

47def landingPage(): 

48 

49 managerProgramDict = getManagerProgramDict(g.current_user) 

50 # Optimize the query to fetch programs with non-canceled, non-past events in the current term 

51 

52 programsWithEventsList = list(Program.select(Program, Event) 

53 .join(Event) 

54 .where((Event.term == g.current_term) & (Event.isCanceled == False)) 

55 .distinct() 

56 .execute()) # Ensure only unique programs are included 

57 # Limit returned list to events in the future 

58 futureEvents = [p for p in programsWithEventsList if not p.event.isPastEnd] 

59 

60 return render_template("/main/landingPage.html", 

61 managerProgramDict=managerProgramDict, 

62 term=g.current_term, 

63 programsWithEventsList = futureEvents) 

64 

65 

66 

67 

68@main_bp.route('/goToEventsList/<programID>', methods=['GET']) 

69def goToEventsList(programID): 

70 return {"activeTab": getActiveEventTab(programID)} 

71 

72@main_bp.route('/eventsList/<selectedTerm>', methods=['GET'], defaults={'activeTab': "studentLedEvents", 'programID': 0}) 

73@main_bp.route('/eventsList/<selectedTerm>/<activeTab>', methods=['GET'], defaults={'programID': 0}) 

74@main_bp.route('/eventsList/<selectedTerm>/<activeTab>/<programID>', methods=['GET']) 

75def events(selectedTerm, activeTab, programID): 

76 currentTerm = g.current_term 

77 if selectedTerm: 

78 currentTerm = selectedTerm 

79 currentTime = datetime.datetime.now() 

80 

81 listOfTerms = Term.select() 

82 participantRSVP = EventRsvp.select(EventRsvp, Event).join(Event).where(EventRsvp.user == g.current_user) 

83 rsvpedEventsID = [event.event.id for event in participantRSVP] 

84 

85 term: Term = Term.get_by_id(currentTerm) 

86 

87 currentEventRsvpAmount = getEventRsvpCountsForTerm(term) 

88 studentLedEvents = getStudentLedEvents(term) 

89 countUpcomingStudentLedEvents = getUpcomingStudentLedCount(term, currentTime) 

90 trainingEvents = getTrainingEvents(term, g.current_user) 

91 bonnerEvents = getBonnerEvents(term) 

92 otherEvents = getOtherEvents(term) 

93 

94 managersProgramDict = getManagerProgramDict(g.current_user) 

95 

96 # Fetch toggle state from session  

97 toggleState = request.args.get('toggleState', 'unchecked') 

98 

99 # compile all student led events into one list 

100 studentEvents = [] 

101 for studentEvent in studentLedEvents.values(): 

102 studentEvents += studentEvent # add all contents of studentEvent to the studentEvents list 

103 

104 # Get the count of all term events for each category to display in the event list page. 

105 studentLedEventsCount: int = len(studentEvents) 

106 trainingEventsCount: int = len(trainingEvents) 

107 bonnerEventsCount: int = len(bonnerEvents) 

108 otherEventsCount: int = len(otherEvents) 

109 

110 # gets only upcoming events to display in indicators 

111 if (toggleState == 'unchecked'): 

112 studentLedEventsCount: int = sum(list(countUpcomingStudentLedEvents.values())) 

113 for event in trainingEvents: 

114 if event.isPastEnd: 

115 trainingEventsCount -= 1 

116 for event in bonnerEvents: 

117 if event.isPastEnd: 

118 bonnerEventsCount -= 1 

119 for event in otherEvents: 

120 if event.isPastEnd: 

121 otherEventsCount -= 1 

122 

123 # Handle ajax request for Event category header number notifiers and toggle 

124 if request.headers.get('X-Requested-With') == 'XMLHttpRequest': 

125 return jsonify({ 

126 "studentLedEventsCount": studentLedEventsCount, 

127 "trainingEventsCount": trainingEventsCount, 

128 "bonnerEventsCount": bonnerEventsCount, 

129 "otherEventsCount": otherEventsCount, 

130 "toggleStatus": toggleState 

131 }) 

132 

133 return render_template("/events/event_list.html", 

134 selectedTerm = term, 

135 studentLedEvents = studentLedEvents, 

136 trainingEvents = trainingEvents, 

137 bonnerEvents = bonnerEvents, 

138 otherEvents = otherEvents, 

139 listOfTerms = listOfTerms, 

140 rsvpedEventsID = rsvpedEventsID, 

141 currentEventRsvpAmount = currentEventRsvpAmount, 

142 currentTime = currentTime, 

143 user = g.current_user, 

144 activeTab = activeTab, 

145 programID = int(programID), 

146 managersProgramDict = managersProgramDict, 

147 countUpcomingStudentLedEvents = countUpcomingStudentLedEvents, 

148 toggleState = toggleState, 

149 ) 

150 

151@main_bp.route('/profile/<username>', methods=['GET']) 

152def viewUsersProfile(username): 

153 """ 

154 This function displays the information of a volunteer to the user 

155 """ 

156 try: 

157 volunteer = User.get(User.username == username) 

158 except Exception as e: 

159 if g.current_user.isAdmin: 

160 flash(f"{username} does not exist! ", category='danger') 

161 return redirect(url_for('admin.studentSearchPage')) 

162 else: 

163 abort(403) # Error 403 if non admin/student-staff user trys to access via url 

164 

165 if (g.current_user == volunteer) or g.current_user.isAdmin: 

166 upcomingEvents = getUpcomingEventsForUser(volunteer) 

167 participatedEvents = getParticipatedEventsForUser(volunteer) 

168 programs = Program.select() 

169 if not g.current_user.isBonnerScholar and not g.current_user.isAdmin: 

170 programs = programs.where(Program.isBonnerScholars == False) 

171 interests = Interest.select(Interest, Program).join(Program).where(Interest.user == volunteer) 

172 programsInterested = [interest.program for interest in interests] 

173 

174 rsvpedEventsList = EventRsvp.select(EventRsvp, Event).join(Event).where(EventRsvp.user == volunteer) 

175 rsvpedEvents = [event.event.id for event in rsvpedEventsList] 

176 

177 programManagerPrograms = ProgramManager.select(ProgramManager, Program).join(Program).where(ProgramManager.user == volunteer) 

178 permissionPrograms = [entry.program.id for entry in programManagerPrograms] 

179 

180 allBackgroundHistory = getUserBGCheckHistory(volunteer) 

181 backgroundTypes = list(BackgroundCheckType.select()) 

182 

183 eligibilityTable = [] 

184 

185 for program in programs: 

186 banNotes = list(ProgramBan.select(ProgramBan, Note) 

187 .join(Note, on=(ProgramBan.banNote == Note.id)) 

188 .where(ProgramBan.user == volunteer, 

189 ProgramBan.program == program, 

190 ProgramBan.endDate > datetime.datetime.now()).execute()) 

191 userParticipatedTrainingEvents = getParticipationStatusForTrainings(program, [volunteer], g.current_term) 

192 try: 

193 allTrainingsComplete = False not in [attended for event, attended in userParticipatedTrainingEvents[username]] # Did volunteer attend all events 

194 except KeyError: 

195 allTrainingsComplete = False 

196 noteForDict = banNotes[-1].banNote.noteContent if banNotes else "" 

197 eligibilityTable.append({"program": program, 

198 "completedTraining": allTrainingsComplete, 

199 "trainingList": userParticipatedTrainingEvents, 

200 "isNotBanned": (not banNotes), 

201 "banNote": noteForDict}) 

202 profileNotes = ProfileNote.select().where(ProfileNote.user == volunteer) 

203 

204 bonnerRequirements = getCertRequirementsWithCompletion(certification=Certification.BONNER, username=volunteer) 

205 

206 managersProgramDict = getManagerProgramDict(g.current_user) 

207 managersList = [id[1] for id in managersProgramDict.items()] 

208 totalSustainedEngagements = getEngagementTotal(getCommunityEngagementByTerm(volunteer)) 

209 

210 return render_template ("/main/userProfile.html", 

211 programs = programs, 

212 programsInterested = programsInterested, 

213 upcomingEvents = upcomingEvents, 

214 participatedEvents = participatedEvents, 

215 rsvpedEvents = rsvpedEvents, 

216 permissionPrograms = permissionPrograms, 

217 eligibilityTable = eligibilityTable, 

218 volunteer = volunteer, 

219 backgroundTypes = backgroundTypes, 

220 allBackgroundHistory = allBackgroundHistory, 

221 currentDateTime = datetime.datetime.now(), 

222 profileNotes = profileNotes, 

223 bonnerRequirements = bonnerRequirements, 

224 managersList = managersList, 

225 participatedInLabor = getCeltsLaborHistory(volunteer), 

226 totalSustainedEngagements = totalSustainedEngagements, 

227 ) 

228 abort(403) 

229 

230@main_bp.route('/profile/<username>/emergencyContact', methods=['GET', 'POST']) 

231def emergencyContactInfo(username): 

232 """ 

233 This loads the Emergency Contact Page 

234 """ 

235 if not (g.current_user.username == username or g.current_user.isCeltsAdmin): 

236 abort(403) 

237 

238 user = User.get(User.username == username) 

239 

240 if request.method == 'GET': 

241 readOnly = g.current_user.username != username 

242 contactInfo = EmergencyContact.get_or_none(EmergencyContact.user_id == username) 

243 return render_template ("/main/emergencyContactInfo.html", 

244 username=username, 

245 contactInfo=contactInfo, 

246 readOnly=readOnly 

247 ) 

248 

249 elif request.method == 'POST': 

250 if g.current_user.username != username: 

251 abort(403) 

252 

253 rowsUpdated = EmergencyContact.update(**request.form).where(EmergencyContact.user == username).execute() 

254 if not rowsUpdated: 

255 EmergencyContact.create(user = username, **request.form) 

256 createActivityLog(f"{g.current_user.fullName} updated {user.fullName}'s emergency contact information.") 

257 flash('Emergency contact information saved successfully!', 'success') 

258 

259 if request.args.get('action') == 'exit': 

260 return redirect (f"/profile/{username}") 

261 else: 

262 return redirect (f"/profile/{username}/insuranceInfo") 

263 

264@main_bp.route('/profile/<username>/insuranceInfo', methods=['GET', 'POST']) 

265def insuranceInfo(username): 

266 """ 

267 This loads the Insurance Information Page 

268 """ 

269 if not (g.current_user.username == username or g.current_user.isCeltsAdmin): 

270 abort(403) 

271 

272 user = User.get(User.username == username) 

273 

274 if request.method == 'GET': 

275 readOnly = g.current_user.username != username 

276 userInsuranceInfo = InsuranceInfo.get_or_none(InsuranceInfo.user == username) 

277 return render_template ("/main/insuranceInfo.html", 

278 username=username, 

279 userInsuranceInfo=userInsuranceInfo, 

280 readOnly=readOnly 

281 ) 

282 

283 # Save the form data 

284 elif request.method == 'POST': 

285 if g.current_user.username != username: 

286 abort(403) 

287 

288 rowsUpdated = InsuranceInfo.update(**request.form).where(InsuranceInfo.user == username).execute() 

289 if not rowsUpdated: 

290 InsuranceInfo.create(user = username, **request.form) 

291 createActivityLog(f"{g.current_user.fullName} updated {user.fullName}'s insurance information.") 

292 flash('Insurance information saved successfully!', 'success') 

293 

294 if request.args.get('action') == 'exit': 

295 return redirect (f"/profile/{username}") 

296 else: 

297 return redirect (f"/profile/{username}/emergencyContact") 

298 

299@main_bp.route('/profile/<username>/travelForm', methods=['GET', 'POST']) 

300def travelForm(username): 

301 if not (g.current_user.username == username or g.current_user.isCeltsAdmin): 

302 abort(403) 

303 

304 user = (User.select(User, EmergencyContact, InsuranceInfo) 

305 .join(EmergencyContact, JOIN.LEFT_OUTER).switch() 

306 .join(InsuranceInfo, JOIN.LEFT_OUTER) 

307 .where(User.username == username).limit(1)) 

308 if not list(user): 

309 abort(404) 

310 userData = list(user.dicts())[0] 

311 userData = {key: value if value else '' for (key, value) in userData.items()} 

312 

313 return render_template ('/main/travelForm.html', 

314 userData = userData 

315 ) 

316 

317 

318@main_bp.route('/profile/addNote', methods=['POST']) 

319def addNote(): 

320 """ 

321 This function adds a note to the user's profile. 

322 """ 

323 postData = request.form 

324 try: 

325 note = addProfileNote(postData["visibility"], postData["bonner"] == "yes", postData["noteTextbox"], postData["username"]) 

326 flash("Successfully added profile note", "success") 

327 return redirect(url_for("main.viewUsersProfile", username=postData["username"])) 

328 except Exception as e: 

329 print("Error adding note", e) 

330 flash("Failed to add profile note", "danger") 

331 return "Failed to add profile note", 500 

332 

333@main_bp.route('/<username>/deleteNote', methods=['POST']) 

334def deleteNote(username): 

335 """ 

336 This function deletes a note from the user's profile. 

337 """ 

338 try: 

339 deleteProfileNote(request.form["id"]) 

340 flash("Successfully deleted profile note", "success") 

341 except Exception as e: 

342 print("Error deleting note", e) 

343 flash("Failed to delete profile note", "danger") 

344 return "success" 

345 

346# ===========================Ban=============================================== 

347@main_bp.route('/<username>/ban/<program_id>', methods=['POST']) 

348def ban(program_id, username): 

349 """ 

350 This function updates the ban status of a username either when they are banned from a program. 

351 program_id: the primary id of the program the student is being banned from 

352 username: unique value of a user to correctly identify them 

353 """ 

354 postData = request.form 

355 banNote = postData["note"] # This contains the note left about the change 

356 banEndDate = postData["endDate"] # Contains the date the ban will no longer be effective 

357 try: 

358 banUser(program_id, username, banNote, banEndDate, g.current_user) 

359 programInfo = Program.get(int(program_id)) 

360 flash("Successfully banned the volunteer", "success") 

361 createActivityLog(f'Banned {username} from {programInfo.programName} until {banEndDate}.') 

362 return "Successfully banned the volunteer." 

363 except Exception as e: 

364 print("Error while updating ban", e) 

365 flash("Failed to ban the volunteer", "danger") 

366 return "Failed to ban the volunteer", 500 

367 

368# ===========================Unban=============================================== 

369@main_bp.route('/<username>/unban/<program_id>', methods=['POST']) 

370def unban(program_id, username): 

371 """ 

372 This function updates the ban status of a username either when they are unbanned from a program. 

373 program_id: the primary id of the program the student is being unbanned from 

374 username: unique value of a user to correctly identify them 

375 """ 

376 postData = request.form 

377 unbanNote = postData["note"] # This contains the note left about the change 

378 try: 

379 unbanUser(program_id, username, unbanNote, g.current_user) 

380 programInfo = Program.get(int(program_id)) 

381 createActivityLog(f'Unbanned {username} from {programInfo.programName}.') 

382 flash("Successfully unbanned the volunteer", "success") 

383 return "Successfully unbanned the volunteer" 

384 

385 except Exception as e: 

386 print("Error while updating Unban", e) 

387 flash("Failed to unban the volunteer", "danger") 

388 return "Failed to unban the volunteer", 500 

389 

390 

391@main_bp.route('/<username>/addInterest/<program_id>', methods=['POST']) 

392def addInterest(program_id, username): 

393 """ 

394 This function adds a program to the list of programs a user interested in 

395 program_id: the primary id of the program the student is adding interest of 

396 username: unique value of a user to correctly identify them 

397 """ 

398 try: 

399 success = addUserInterest(program_id, username) 

400 if success: 

401 flash("Successfully added " + Program.get_by_id(program_id).programName + " as an interest", "success") 

402 return "" 

403 else: 

404 flash("Was unable to remove " + Program.get_by_id(program_id).programName + " as an interest.", "danger") 

405 

406 except Exception as e: 

407 print(e) 

408 return "Error Updating Interest", 500 

409 

410@main_bp.route('/<username>/removeInterest/<program_id>', methods=['POST']) 

411def removeInterest(program_id, username): 

412 """ 

413 This function removes a program to the list of programs a user interested in 

414 program_id: the primary id of the program the student is adding interest of 

415 username: unique value of a user to correctly identify them 

416 """ 

417 try: 

418 removed = removeUserInterest(program_id, username) 

419 if removed: 

420 flash("Successfully removed " + Program.get_by_id(program_id).programName + " as an interest.", "success") 

421 return "" 

422 else: 

423 flash("Was unable to remove " + Program.get_by_id(program_id).programName + " as an interest.", "danger") 

424 except Exception as e: 

425 print(e) 

426 return "Error Updating Interest", 500 

427 

428@main_bp.route('/rsvpForEvent', methods = ['POST']) 

429def volunteerRegister(): 

430 """ 

431 This function selects the user ID and event ID and registers the user 

432 for the event they have clicked register for. 

433 """ 

434 event = Event.get_by_id(request.form['id']) 

435 program = event.program 

436 user = g.current_user 

437 

438 isAdded = checkUserRsvp(user, event) 

439 isEligible = isEligibleForProgram(program, user) 

440 listOfRequirements = unattendedRequiredEvents(program, user) 

441 

442 personAdded = False 

443 if isEligible: 

444 personAdded = addPersonToEvent(user, event) 

445 if personAdded and listOfRequirements: 

446 reqListToString = ', '.join(listOfRequirements) 

447 flash(f"{user.firstName} {user.lastName} successfully registered. However, the following training may be required: {reqListToString}.", "success") 

448 elif personAdded: 

449 flash("Successfully registered for event!","success") 

450 else: 

451 flash(f"RSVP Failed due to an unknown error.", "danger") 

452 else: 

453 flash(f"Cannot RSVP. Contact CELTS administrators: {app.config['celts_admin_contact']}.", "danger") 

454 

455 

456 if 'from' in request.form: 

457 if request.form['from'] == 'ajax': 

458 return '' 

459 return redirect(url_for("admin.eventDisplay", eventId=event.id)) 

460 

461@main_bp.route('/rsvpRemove', methods = ['POST']) 

462def RemoveRSVP(): 

463 """ 

464 This function deletes the user ID and event ID from database when RemoveRSVP is clicked 

465 """ 

466 eventData = request.form 

467 event = Event.get_by_id(eventData['id']) 

468 

469 currentRsvpParticipant = EventRsvp.get(EventRsvp.user == g.current_user, EventRsvp.event == event) 

470 logBody = "withdrew from the waitlist" if currentRsvpParticipant.rsvpWaitlist else "un-RSVP'd" 

471 currentRsvpParticipant.delete_instance() 

472 createRsvpLog(event.id, f"{g.current_user.fullName} {logBody}.") 

473 flash("Successfully unregistered for event!", "success") 

474 if 'from' in eventData: 

475 if eventData['from'] == 'ajax': 

476 return '' 

477 return redirect(url_for("admin.eventDisplay", eventId=event.id)) 

478 

479@main_bp.route('/profile/<username>/serviceTranscript', methods = ['GET']) 

480def serviceTranscript(username): 

481 user = User.get_or_none(User.username == username) 

482 if user is None: 

483 abort(404) 

484 if user != g.current_user and not g.current_user.isAdmin: 

485 abort(403) 

486 

487 slCourses = getSlCourseTranscript(username) 

488 totalHours = getTotalHours(username) 

489 allEventTranscript = getProgramTranscript(username) 

490 startDate = getStartYear(username) 

491 return render_template('main/serviceTranscript.html', 

492 allEventTranscript = allEventTranscript, 

493 slCourses = slCourses.objects(), 

494 totalHours = totalHours, 

495 startDate = startDate, 

496 userData = user) 

497 

498@main_bp.route('/searchUser/<query>', methods = ['GET']) 

499def searchUser(query): 

500 

501 category= request.args.get("category") 

502 

503 '''Accepts user input and queries the database returning results that matches user search''' 

504 try: 

505 query = query.strip() 

506 search = query.upper() 

507 splitSearch = search.split() 

508 searchResults = searchUsers(query,category) 

509 return searchResults 

510 except Exception as e: 

511 print(e) 

512 return "Error in searching for user", 500 

513 

514@main_bp.route('/contributors',methods = ['GET']) 

515def contributors(): 

516 return render_template("/contributors.html") 

517 

518@main_bp.route('/updateDietInformation', methods = ['GET', 'POST']) 

519def getDietInfo(): 

520 dietaryInfo = request.form 

521 user = dietaryInfo["user"] 

522 dietInfo = dietaryInfo["dietInfo"] 

523 

524 if (g.current_user.username == user) or g.current_user.isAdmin: 

525 updateDietInfo(user, dietInfo) 

526 userInfo = User.get(User.username == user) 

527 if len(dietInfo) > 0: 

528 createActivityLog(f"Updated {userInfo.fullName}'s dietary restrictions to {dietInfo}.") if dietInfo.strip() else None 

529 else: 

530 createActivityLog(f"Deleted all {userInfo.fullName}'s dietary restrictions dietary restrictions.") 

531 

532 

533 return " " 

534 

535@main_bp.route('/profile/<username>/indicateInterest', methods=['POST']) 

536def indicateMinorInterest(username): 

537 if g.current_user.isCeltsAdmin or g.current_user.username == username: 

538 toggleMinorInterest(username) 

539 

540 else: 

541 abort(403) 

542 

543 return ""