Coverage for app/controllers/main/routes.py: 24%
351 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-01-29 15:39 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2025-01-29 15:39 +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
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
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
42@main_bp.route('/logout', methods=['GET'])
43def redirectToLogout():
44 return redirect(logout())
46@main_bp.route('/', methods=['GET'])
47def landingPage():
49 managerProgramDict = getManagerProgramDict(g.current_user)
50 # Optimize the query to fetch programs with non-canceled, non-past events in the current term
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]
60 return render_template("/main/landingPage.html",
61 managerProgramDict=managerProgramDict,
62 term=g.current_term,
63 programsWithEventsList = futureEvents)
68@main_bp.route('/goToEventsList/<programID>', methods=['GET'])
69def goToEventsList(programID):
70 return {"activeTab": getActiveEventTab(programID)}
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()
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]
85 term: Term = Term.get_by_id(currentTerm)
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)
94 managersProgramDict = getManagerProgramDict(g.current_user)
96 # Fetch toggle state from session
97 toggleState = request.args.get('toggleState', 'unchecked')
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
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)
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
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 })
133 return render_template("/events/eventList.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 )
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
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]
174 rsvpedEventsList = EventRsvp.select(EventRsvp, Event).join(Event).where(EventRsvp.user == volunteer)
175 rsvpedEvents = [event.event.id for event in rsvpedEventsList]
177 programManagerPrograms = ProgramManager.select(ProgramManager, Program).join(Program).where(ProgramManager.user == volunteer)
178 permissionPrograms = [entry.program.id for entry in programManagerPrograms]
180 allBackgroundHistory = getUserBGCheckHistory(volunteer)
181 backgroundTypes = list(BackgroundCheckType.select())
185 eligibilityTable = []
187 for program in programs:
188 banNotes = list(ProgramBan.select(ProgramBan, Note)
189 .join(Note, on=(ProgramBan.banNote == Note.id))
190 .where(ProgramBan.user == volunteer,
191 ProgramBan.program == program,
192 ProgramBan.endDate > datetime.datetime.now()).execute())
193 onTranscriptQuery = list(ProgramBan.select(ProgramBan)
194 .where(ProgramBan.user == volunteer,
195 ProgramBan.program == program,
196 ProgramBan.unbanNote.is_null(),
197 ProgramBan.removeFromTranscript == 0))
199 onTranscript = True if len(onTranscriptQuery) > 0 else False
200 userParticipatedTrainingEvents = getParticipationStatusForTrainings(program, [volunteer], g.current_term)
201 try:
202 allTrainingsComplete = False not in [attended for event, attended in userParticipatedTrainingEvents[username]] # Did volunteer attend all events
203 except KeyError:
204 allTrainingsComplete = False
205 noteForDict = banNotes[-1].banNote.noteContent if banNotes else ""
206 eligibilityTable.append({"program": program,
207 "completedTraining": allTrainingsComplete,
208 "trainingList": userParticipatedTrainingEvents,
209 "isNotBanned": (not banNotes),
210 "banNote": noteForDict,
211 "onTranscript": onTranscript}),
213 profileNotes = ProfileNote.select().where(ProfileNote.user == volunteer)
215 bonnerRequirements = getCertRequirementsWithCompletion(certification=Certification.BONNER, username=volunteer)
217 managersProgramDict = getManagerProgramDict(g.current_user)
218 managersList = [id[1] for id in managersProgramDict.items()]
219 totalSustainedEngagements = getEngagementTotal(getCommunityEngagementByTerm(volunteer))
221 return render_template ("/main/userProfile.html",
222 programs = programs,
223 programsInterested = programsInterested,
224 upcomingEvents = upcomingEvents,
225 participatedEvents = participatedEvents,
226 rsvpedEvents = rsvpedEvents,
227 permissionPrograms = permissionPrograms,
228 eligibilityTable = eligibilityTable,
229 volunteer = volunteer,
230 backgroundTypes = backgroundTypes,
231 allBackgroundHistory = allBackgroundHistory,
232 currentDateTime = datetime.datetime.now(),
233 profileNotes = profileNotes,
234 bonnerRequirements = bonnerRequirements,
235 managersList = managersList,
236 participatedInLabor = getCeltsLaborHistory(volunteer),
237 totalSustainedEngagements = totalSustainedEngagements,
238 )
239 abort(403)
241@main_bp.route('/profile/<username>/emergencyContact', methods=['GET', 'POST'])
242def emergencyContactInfo(username):
243 """
244 This loads the Emergency Contact Page
245 """
246 if not (g.current_user.username == username or g.current_user.isCeltsAdmin):
247 abort(403)
249 user = User.get(User.username == username)
251 if request.method == 'GET':
252 readOnly = g.current_user.username != username
253 contactInfo = EmergencyContact.get_or_none(EmergencyContact.user_id == username)
254 return render_template ("/main/emergencyContactInfo.html",
255 username=username,
256 contactInfo=contactInfo,
257 readOnly=readOnly
258 )
260 elif request.method == 'POST':
261 if g.current_user.username != username:
262 abort(403)
264 rowsUpdated = EmergencyContact.update(**request.form).where(EmergencyContact.user == username).execute()
265 if not rowsUpdated:
266 EmergencyContact.create(user = username, **request.form)
268 createActivityLog(f"{g.current_user.fullName} updated {user.fullName}'s emergency contact information.")
269 flash('Emergency contact information saved successfully!', 'success')
271 if request.args.get('action') == 'exit':
272 return redirect (f"/profile/{username}")
273 else:
274 return redirect (f"/profile/{username}/insuranceInfo")
276@main_bp.route('/profile/<username>/insuranceInfo', methods=['GET', 'POST'])
277def insuranceInfo(username):
278 """
279 This loads the Insurance Information Page
280 """
281 if not (g.current_user.username == username or g.current_user.isCeltsAdmin):
282 abort(403)
284 user = User.get(User.username == username)
286 if request.method == 'GET':
287 readOnly = g.current_user.username != username
288 userInsuranceInfo = InsuranceInfo.get_or_none(InsuranceInfo.user == username)
289 return render_template ("/main/insuranceInfo.html",
290 username=username,
291 userInsuranceInfo=userInsuranceInfo,
292 readOnly=readOnly
293 )
295 # Save the form data
296 elif request.method == 'POST':
297 if g.current_user.username != username:
298 abort(403)
300 rowsUpdated = InsuranceInfo.update(**request.form).where(InsuranceInfo.user == username).execute()
301 if not rowsUpdated:
302 InsuranceInfo.create(user = username, **request.form)
304 createActivityLog(f"{g.current_user.fullName} updated {user.fullName}'s insurance information.")
305 flash('Insurance information saved successfully!', 'success')
307 if request.args.get('action') == 'exit':
308 return redirect (f"/profile/{username}")
309 else:
310 return redirect (f"/profile/{username}/emergencyContact")
312@main_bp.route('/profile/<username>/travelForm', methods=['GET', 'POST'])
313def travelForm(username):
314 if not (g.current_user.username == username or g.current_user.isCeltsAdmin):
315 abort(403)
317 user = (User.select(User, EmergencyContact, InsuranceInfo)
318 .join(EmergencyContact, JOIN.LEFT_OUTER).switch()
319 .join(InsuranceInfo, JOIN.LEFT_OUTER)
320 .where(User.username == username).limit(1))
321 if not list(user):
322 abort(404)
323 userData = list(user.dicts())[0]
324 userData = {key: value if value else '' for (key, value) in userData.items()}
326 return render_template ('/main/travelForm.html',
327 userData = userData
328 )
331@main_bp.route('/profile/addNote', methods=['POST'])
332def addNote():
333 """
334 This function adds a note to the user's profile.
335 """
336 postData = request.form
337 try:
338 note = addProfileNote(postData["visibility"], postData["bonner"] == "yes", postData["noteTextbox"], postData["username"])
339 flash("Successfully added profile note", "success")
340 return redirect(url_for("main.viewUsersProfile", username=postData["username"]))
341 except Exception as e:
342 print("Error adding note", e)
343 flash("Failed to add profile note", "danger")
344 return "Failed to add profile note", 500
346@main_bp.route('/<username>/deleteNote', methods=['POST'])
347def deleteNote(username):
348 """
349 This function deletes a note from the user's profile.
350 """
351 try:
352 deleteProfileNote(request.form["id"])
353 flash("Successfully deleted profile note", "success")
354 except Exception as e:
355 print("Error deleting note", e)
356 flash("Failed to delete profile note", "danger")
357 return "success"
359# ===========================Ban===============================================
360@main_bp.route('/<username>/ban/<program_id>', methods=['POST'])
361def ban(program_id, username):
362 """
363 This function updates the ban status of a username either when they are banned from a program.
364 program_id: the primary id of the program the student is being banned from
365 username: unique value of a user to correctly identify them
366 """
367 postData = request.form
368 banNote = postData["note"] # This contains the note left about the change
369 banEndDate = postData["endDate"] # Contains the date the ban will no longer be effective
371 try:
372 banUser(program_id, username, banNote, banEndDate, g.current_user)
373 programInfo = Program.get(int(program_id))
374 flash("Successfully banned the volunteer", "success")
375 createActivityLog(f'Banned {username} from {programInfo.programName} until {banEndDate}.')
376 return "Successfully banned the volunteer."
377 except Exception as e:
378 print("Error while updating ban", e)
379 flash("Failed to ban the volunteer", "danger")
380 return "Failed to ban the volunteer", 500
382# ===========================Unban===============================================
383@main_bp.route('/<username>/unban/<program_id>', methods=['POST'])
384def unban(program_id, username):
385 """
386 This function updates the ban status of a username either when they are unbanned from a program.
387 program_id: the primary id of the program the student is being unbanned from
388 username: unique value of a user to correctly identify them
389 """
390 postData = request.form
391 unbanNote = postData["note"] # This contains the note left about the change
392 try:
393 unbanUser(program_id, username, unbanNote, g.current_user)
394 programInfo = Program.get(int(program_id))
395 createActivityLog(f'Unbanned {username} from {programInfo.programName}.')
396 flash("Successfully unbanned the volunteer", "success")
397 return "Successfully unbanned the volunteer"
399 except Exception as e:
400 print("Error while updating Unban", e)
401 flash("Failed to unban the volunteer", "danger")
402 return "Failed to unban the volunteer", 500
405@main_bp.route('/<username>/addInterest/<program_id>', methods=['POST'])
406def addInterest(program_id, username):
407 """
408 This function adds a program to the list of programs a user interested in
409 program_id: the primary id of the program the student is adding interest of
410 username: unique value of a user to correctly identify them
411 """
412 try:
413 success = addUserInterest(program_id, username)
414 if success:
415 flash("Successfully added " + Program.get_by_id(program_id).programName + " as an interest", "success")
416 return ""
417 else:
418 flash("Was unable to remove " + Program.get_by_id(program_id).programName + " as an interest.", "danger")
420 except Exception as e:
421 print(e)
422 return "Error Updating Interest", 500
424@main_bp.route('/<username>/removeInterest/<program_id>', methods=['POST'])
425def removeInterest(program_id, username):
426 """
427 This function removes a program to the list of programs a user interested in
428 program_id: the primary id of the program the student is adding interest of
429 username: unique value of a user to correctly identify them
430 """
431 try:
432 removed = removeUserInterest(program_id, username)
433 if removed:
434 flash("Successfully removed " + Program.get_by_id(program_id).programName + " as an interest.", "success")
435 return ""
436 else:
437 flash("Was unable to remove " + Program.get_by_id(program_id).programName + " as an interest.", "danger")
438 except Exception as e:
439 print(e)
440 return "Error Updating Interest", 500
442@main_bp.route('/rsvpForEvent', methods = ['POST'])
443def volunteerRegister():
444 """
445 This function selects the user ID and event ID and registers the user
446 for the event they have clicked register for.
447 """
448 event = Event.get_by_id(request.form['id'])
449 program = event.program
450 user = g.current_user
452 isAdded = checkUserRsvp(user, event)
453 isEligible = isEligibleForProgram(program, user)
454 listOfRequirements = unattendedRequiredEvents(program, user)
456 personAdded = False
457 if isEligible:
458 personAdded = addPersonToEvent(user, event)
459 if personAdded and listOfRequirements:
460 reqListToString = ', '.join(listOfRequirements)
461 flash(f"{user.firstName} {user.lastName} successfully registered. However, the following training may be required: {reqListToString}.", "success")
462 elif personAdded:
463 flash("Successfully registered for event!","success")
464 else:
465 flash(f"RSVP Failed due to an unknown error.", "danger")
466 else:
467 flash(f"Cannot RSVP. Contact CELTS administrators: {app.config['celts_admin_contact']}.", "danger")
470 if 'from' in request.form:
471 if request.form['from'] == 'ajax':
472 return ''
473 return redirect(url_for("admin.eventDisplay", eventId=event.id))
475@main_bp.route('/rsvpRemove', methods = ['POST'])
476def RemoveRSVP():
477 """
478 This function deletes the user ID and event ID from database when RemoveRSVP is clicked
479 """
480 eventData = request.form
481 event = Event.get_by_id(eventData['id'])
483 currentRsvpParticipant = EventRsvp.get(EventRsvp.user == g.current_user, EventRsvp.event == event)
484 logBody = "withdrew from the waitlist" if currentRsvpParticipant.rsvpWaitlist else "un-RSVP'd"
485 currentRsvpParticipant.delete_instance()
486 createRsvpLog(event.id, f"{g.current_user.fullName} {logBody}.")
487 flash("Successfully unregistered for event!", "success")
488 if 'from' in eventData:
489 if eventData['from'] == 'ajax':
490 return ''
491 return redirect(url_for("admin.eventDisplay", eventId=event.id))
493@main_bp.route('/profile/<username>/serviceTranscript', methods = ['GET'])
494def serviceTranscript(username):
495 user = User.get_or_none(User.username == username)
496 if user is None:
497 abort(404)
498 if user != g.current_user and not g.current_user.isAdmin:
499 abort(403)
501 slCourses = getSlCourseTranscript(username)
502 totalHours = getTotalHours(username)
503 allEventTranscript = getProgramTranscript(username)
504 startDate = getStartYear(username)
505 return render_template('main/serviceTranscript.html',
506 allEventTranscript = allEventTranscript,
507 slCourses = slCourses.objects(),
508 totalHours = totalHours,
509 startDate = startDate,
510 userData = user)
512@main_bp.route('/profile/<username>/updateTranscript/<program_id>', methods=['POST'])
513def updateTranscript(username, program_id):
514 # Check user permissions
515 user = User.get_or_none(User.username == username)
516 if user is None:
517 abort(404)
518 if user != g.current_user and not g.current_user.isAdmin:
519 abort(403)
521 # Get the data sent from the client-side JavaScript
522 data = request.json
524 # Retrieve removeFromTranscript value from the request data
525 removeFromTranscript = data.get('removeFromTranscript')
527 # Update the ProgramBan object matching the program_id and username
528 try:
529 bannedProgramForUser = ProgramBan.get((ProgramBan.program == program_id) & (ProgramBan.user == user) & (ProgramBan.unbanNote.is_null()))
530 bannedProgramForUser.removeFromTranscript = removeFromTranscript
531 bannedProgramForUser.save()
532 return jsonify({'status': 'success'})
533 except ProgramBan.DoesNotExist:
534 return jsonify({'status': 'error', 'message': 'ProgramBan not found'})
537@main_bp.route('/searchUser/<query>', methods = ['GET'])
538def searchUser(query):
540 category= request.args.get("category")
542 '''Accepts user input and queries the database returning results that matches user search'''
543 try:
544 query = query.strip()
545 search = query.upper()
546 splitSearch = search.split()
547 searchResults = searchUsers(query,category)
548 return searchResults
549 except Exception as e:
550 print(e)
551 return "Error in searching for user", 500
553@main_bp.route('/contributors',methods = ['GET'])
554def contributors():
555 return render_template("/contributors.html")
557@main_bp.route('/updateDietInformation', methods = ['GET', 'POST'])
558def getDietInfo():
559 dietaryInfo = request.form
560 user = dietaryInfo["user"]
561 dietInfo = dietaryInfo["dietInfo"]
563 if (g.current_user.username == user) or g.current_user.isAdmin:
564 updateDietInfo(user, dietInfo)
565 userInfo = User.get(User.username == user)
566 if len(dietInfo) > 0:
567 createActivityLog(f"Updated {userInfo.fullName}'s dietary restrictions to {dietInfo}.") if dietInfo.strip() else None
568 else:
569 createActivityLog(f"Deleted all {userInfo.fullName}'s dietary restrictions dietary restrictions.")
572 return " "
574@main_bp.route('/profile/<username>/indicateInterest', methods=['POST'])
575def indicateMinorInterest(username):
576 if g.current_user.isCeltsAdmin or g.current_user.username == username:
577 toggleMinorInterest(username)
579 else:
580 abort(403)
582 return ""