Coverage for app/controllers/admin/routes.py: 22%
474 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-08-24 19:35 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-08-24 19:35 +0000
1from flask import request, render_template, url_for, g, redirect
2from flask import flash, abort, jsonify, session, send_file
3from peewee import DoesNotExist, IntegrityError
4from playhouse.shortcuts import model_to_dict
5import json
6from datetime import datetime
7import os
9from app import app
10from app.models.backgroundCheck import BackgroundCheck
11from app.models.program import Program
12from app.models.event import Event
13from app.models.eventRsvp import EventRsvp
14from app.models.eventParticipant import EventParticipant
15from app.models.user import User
16from app.models.course import Course
17from app.models.courseInstructor import CourseInstructor
18from app.models.courseParticipant import CourseParticipant
19from app.models.eventTemplate import EventTemplate
20from app.models.activityLog import ActivityLog
21from app.models.eventRsvpLog import EventRsvpLog
22from app.models.attachmentUpload import AttachmentUpload
23from app.models.bonnerCohort import BonnerCohort
24from app.models.eventCohort import EventCohort
25from app.models.certification import Certification
26from app.models.user import User
27from app.models.term import Term
28from app.models.eventViews import EventView
29from app.models.courseStatus import CourseStatus
30from app.models.programManager import ProgramManager
32from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates
33from app.logic.createLogs import createActivityLog
34from app.logic.certification import getCertRequirements, updateCertRequirements
35from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget
36from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRepeatingEventsData, deleteEventAndAllFollowing, deleteAllEventsInSeries, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewSeriesId, inviteCohortsToEvent, updateEventCohorts
37from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp
38from app.logic.minor import getMinorInterest
39from app.logic.fileHandler import FileHandler
40from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog
41from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses
43from app.controllers.admin import admin_bp
44from app.logic.volunteerSpreadsheet import createSpreadsheet
47@admin_bp.route('/admin/reports')
48def reports():
49 academicYears = Term.select(Term.academicYear).distinct().order_by(Term.academicYear.desc())
50 academicYears = list(map(lambda t: t.academicYear, academicYears))
51 return render_template("/admin/reports.html", academicYears=academicYears)
53@admin_bp.route('/admin/reports/download', methods=['POST'])
54def downloadFile():
55 academicYear = request.form.get('academicYear')
56 filepath = os.path.abspath(createSpreadsheet(academicYear))
57 return send_file(filepath, as_attachment=True)
61@admin_bp.route('/switch_user', methods=['POST'])
62def switchUser():
63 if app.env == "production":
64 print(f"An attempt was made to switch to another user by {g.current_user.username}!")
65 abort(403)
67 print(f"Switching user from {g.current_user} to",request.form['newuser'])
68 session['current_user'] = model_to_dict(User.get_by_id(request.form['newuser']))
70 return redirect(request.referrer)
73@admin_bp.route('/eventTemplates')
74def templateSelect():
75 programs = getAllowedPrograms(g.current_user)
76 if not programs:
77 abort(403)
78 visibleTemplates = getAllowedTemplates(g.current_user)
80 # Create extravaganza QR code if it doesn't exist
81 imgPath = app.config["files"]["image_path"] + "/extravaganza_qr.png"
82 if not os.path.exists(imgPath):
83 import qrcode
84 img = qrcode.make(request.url.split("/")[-2] + "/extravaganza")
85 img.save(imgPath)
86 return render_template("/events/templateSelector.html",
87 programs=programs,
88 celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored),
89 templates=visibleTemplates)
91@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
92def createEvent(templateid, programid):
93 if not g.current_user.canManageProgram(programid):
94 abort(403)
96 # Validate given URL
97 program = None
98 try:
99 template = EventTemplate.get_by_id(templateid)
100 if programid:
101 program = Program.get_by_id(programid)
102 except DoesNotExist as e:
103 print("Invalid template or program id:", e)
104 flash("There was an error with your selection. Please try again or contact Systems Support.", "danger")
105 return redirect(url_for("admin.program_picker"))
107 # Get the data from the form or from the template
108 eventData = template.templateData
109 eventData['program'] = program
111 if request.method == "GET":
112 eventData['contactName'] = "CELTS Admin"
113 eventData['contactEmail'] = app.config['celts_admin_contact']
114 if program:
115 eventData['location'] = program.defaultLocation
116 if program.contactName:
117 eventData['contactName'] = program.contactName
118 if program.contactEmail:
119 eventData['contactEmail'] = program.contactEmail
121 # Try to save the form
122 if request.method == "POST":
123 savedEvents = None
124 eventData.update(request.form.copy())
125 eventData = preprocessEventData(eventData)
126 if eventData.get('isSeries'):
127 eventData['seriesData'] = json.loads(eventData['seriesData'])
128 succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request))
129 if not succeeded:
130 for index, validationErrorMessage in failedSavedOfferings:
131 eventData['seriesData'][index]['isDuplicate'] = True
132 validationErrorMessage = failedSavedOfferings[-1][1] # The last validation error message from the list of offerings if there are multiple
133 print(f"Failed to save offerings {failedSavedOfferings}")
134 else:
135 try:
136 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
137 except Exception as e:
138 print("Failed saving regular event: ", e)
139 validationErrorMessage = "Failed to save event."
140 if savedEvents:
141 rsvpCohorts = request.form.getlist("cohorts[]")
142 if rsvpCohorts:
143 success, message, invitedCohorts = inviteCohortsToEvent(savedEvents[0], rsvpCohorts)
144 if not success:
145 flash(message, 'warning')
147 noun = ((eventData.get('isSeries')) and "Events" or "Event") # pluralize
148 flash(f"{noun} successfully created!", 'success')
151 if program:
152 if len(savedEvents) > 1 and eventData.get('isRepeating'):
153 createActivityLog(f'''Created a repeating series, <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name[:-7]}</a>, for {program.programName}, with a start date of {datetime.strftime(savedEvents[0].startDate, '%m/%d/%Y')}. The last event in the series will be on {datetime.strftime(savedEvents[-1].startDate, '%m/%d/%Y')}.''')
154 elif len(savedEvents) >= 1 and eventData.get('isSeries'):
155 eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents]
156 eventList = ', '.join(f'''<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>''' for event in savedEvents)
158 if len(savedEvents) > 1:
159 #creates list of events created in a multiple series to display in the logs
160 eventList = ', '.join(eventList.split(', ')[:-1]) + f', and ' + eventList.split(', ')[-1]
161 #get last date and stick at the end after 'and' so that it reads like a sentence in admin log
162 lastEventDate = eventDates[-1]
163 eventDates = ', '.join(eventDates[:-1]) + f', and {lastEventDate}'
165 createActivityLog(f"Created series {eventList} for {program.programName}, with start dates of {eventDates}.")
167 else:
168 createActivityLog('''Created event <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a> for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.''')
169 else:
170 createActivityLog(f'''Created a non-program event, <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a>, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.''')
172 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id))
173 else:
174 flash(validationErrorMessage, 'warning')
176 # make sure our data is the same regardless of GET or POST
177 preprocessEventData(eventData)
178 isProgramManager = g.current_user.isProgramManagerFor(programid)
180 requirements, bonnerCohorts = [], []
181 if eventData['program'] is not None and eventData['program'].isBonnerScholars:
182 requirements = getCertRequirements(Certification.BONNER)
183 rawBonnerCohorts = getBonnerCohorts(limit=5)
184 bonnerCohorts = {}
186 for year, cohort in rawBonnerCohorts.items():
187 if cohort:
188 bonnerCohorts[year] = cohort
189 return render_template(f"/events/{template.templateFile}",
190 template = template,
191 eventData = eventData,
192 termList = selectSurroundingTerms(g.current_term, prevTerms=0),
193 requirements = requirements,
194 bonnerCohorts = bonnerCohorts,
195 isProgramManager = isProgramManager)
198@admin_bp.route('/event/<eventId>/rsvp', methods=['GET'])
199def rsvpLogDisplay(eventId):
200 event = Event.get_by_id(eventId)
201 if g.current_user.canManageProgram(event.program):
202 allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc())
203 return render_template("/events/rsvpLog.html",
204 event = event,
205 allLogs = allLogs)
206 else:
207 abort(403)
209@admin_bp.route('/event/<eventId>/renew', methods=['POST'])
210def renewEvent(eventId):
211 try:
212 formData = request.form
213 try:
214 assert formData['timeStart'] < formData['timeEnd']
215 except AssertionError:
216 flash("End time must be after start time", 'warning')
217 return redirect(url_for('admin.eventDisplay', eventId = eventId))
219 try:
220 if formData.get('dateEnd'):
221 assert formData['dateStart'] < formData['dateEnd']
222 except AssertionError:
223 flash("End date must be after start date", 'warning')
224 return redirect(url_for('admin.eventDisplay', eventId = eventId))
227 priorEvent = model_to_dict(Event.get_by_id(eventId))
228 newEventDict = priorEvent.copy()
229 newEventDict.pop('id')
230 newEventDict.update({
231 'program': int(priorEvent['program']['id']),
232 'term': int(priorEvent['term']['id']),
233 'timeStart': formData['timeStart'],
234 'timeEnd': formData['timeEnd'],
235 'location': formData['location'],
236 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
237 'isRepeating': bool(priorEvent['isRepeating']),
238 'seriesId': priorEvent['seriesId'],
239 })
240 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
241 if message:
242 flash(message, "danger")
243 return redirect(url_for('admin.eventDisplay', eventId = eventId))
245 copyRsvpToNewEvent(priorEvent, newEvent[0])
246 createActivityLog(f'''Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.''')
247 flash("Event successfully renewed.", "success")
248 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
251 except Exception as e:
252 print("Error while trying to renew event:", e)
253 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
254 return redirect(url_for('admin.eventDisplay', eventId = eventId))
258@admin_bp.route('/event/<eventId>/view', methods=['GET'])
259@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
260def eventDisplay(eventId):
261 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
262 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
263 viewer = g.current_user
264 event = Event.get_by_id(eventId)
265 addEventView(viewer,event)
266 # Validate given URL
267 try:
268 event = Event.get_by_id(eventId)
269 invitedCohorts = list(EventCohort.select().where(
270 EventCohort.event == event
271 ))
272 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
273 except DoesNotExist as e:
274 print(f"Unknown event: {eventId}")
275 abort(404)
277 notPermitted = not g.current_user.canManageProgram(event.program)
278 if 'edit' in request.url_rule.rule and notPermitted:
279 abort(403)
281 eventData = model_to_dict(event, recurse=False)
282 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
283 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
285 image = None
286 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
287 for attachment in associatedAttachments:
288 for extension in picurestype:
289 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
290 image = filepaths[attachment.fileName][0]
291 if image:
292 break
295 if request.method == "POST": # Attempt to save form
296 eventData = request.form.copy()
297 try:
298 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
300 except IntegrityError as e:
301 print("Error saving event:", e)
302 savedEvents = False
303 validationErrorMessage = "This combination of settings is not allowed."
304 except Exception as e:
305 print("Error saving event:", e)
306 savedEvents = False
307 validationErrorMessage = "Unknown Error Saving Event. Please try again"
310 if savedEvents:
311 rsvpCohorts = request.form.getlist("cohorts[]")
312 updateEventCohorts(savedEvents[0], rsvpCohorts)
313 flash("Event successfully updated!", "success")
314 return redirect(url_for("admin.eventDisplay", eventId = event.id))
315 else:
316 flash(validationErrorMessage, 'warning')
318 # make sure our data is the same regardless of GET and POST
319 preprocessEventData(eventData)
320 eventData['program'] = event.program
321 userHasRSVPed = checkUserRsvp(g.current_user, event)
322 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
323 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
324 requirements, bonnerCohorts = [], []
326 if eventData['program'] and eventData['program'].isBonnerScholars:
327 requirements = getCertRequirements(Certification.BONNER)
328 rawBonnerCohorts = getBonnerCohorts(limit=5)
329 bonnerCohorts = {}
331 for year, cohort in rawBonnerCohorts.items():
332 if cohort:
333 bonnerCohorts[year] = cohort
335 invitedCohorts = list(EventCohort.select().where(
336 EventCohort.event_id == eventId,
337 ))
338 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
339 else:
340 requirements, bonnerCohorts, invitedYears = [], [], []
342 rule = request.url_rule
344 # Event Edit
345 if 'edit' in rule.rule:
346 return render_template("events/createEvent.html",
347 eventData = eventData,
348 termList = Term.select().order_by(Term.termOrder),
349 event = event,
350 requirements = requirements,
351 bonnerCohorts = bonnerCohorts,
352 invitedYears = invitedYears,
353 userHasRSVPed = userHasRSVPed,
354 isProgramManager = isProgramManager,
355 filepaths = filepaths)
356 # Event View
357 else:
358 # get text representations of dates for html
359 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
360 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
361 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
362 eventCountdown = getCountdownToEvent(event)
365 # Identify the next event in a repeating series
366 if event.seriesId:
367 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId)
368 .where((Event.isCanceled == False) | (Event.id == event.id))
369 .order_by(Event.startDate))
370 eventIndex = eventSeriesList.index(event)
371 if len(eventSeriesList) != (eventIndex + 1):
372 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1]
374 currentEventRsvpAmount = getEventRsvpCount(event.id)
376 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
378 return render_template("events/eventView.html",
379 eventData=eventData,
380 event=event,
381 userHasRSVPed=userHasRSVPed,
382 programTrainings=userParticipatedTrainingEvents,
383 currentEventRsvpAmount=currentEventRsvpAmount,
384 isProgramManager=isProgramManager,
385 filepaths=filepaths,
386 image=image,
387 pageViewsCount=pageViewsCount,
388 invitedYears=invitedYears,
389 eventCountdown=eventCountdown
390 )
394@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
395def cancelRoute(eventId):
396 if g.current_user.isAdmin:
397 try:
398 cancelEvent(eventId)
399 return redirect(request.referrer)
401 except Exception as e:
402 print('Error while canceling event:', e)
403 return "", 500
405 else:
406 abort(403)
408@admin_bp.route('/profile/undo', methods=['GET'])
409def undoBackgroundCheck():
410 try:
411 username = g.current_user
412 bgCheckId = session['lastDeletedBgCheck']
413 BackgroundCheck.update({BackgroundCheck.deletionDate: None, BackgroundCheck.deletedBy: None}).where(BackgroundCheck.id == bgCheckId).execute()
414 flash("Background Check has been successfully restored.", "success")
415 return redirect (f"/profile/{username}?accordion=background")
416 except Exception as e:
417 print('Error while undoing background check:', e)
418 return "", 500
420@admin_bp.route('/event/undo', methods=['GET'])
421def undoEvent():
422 try:
423 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted
424 for eventId in eventIds:
425 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
426 event = Event.get_or_none(Event.id == eventId)
427 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id))
428 if event.isRepeating:
429 nameCounter = 1
430 for repeatingEvent in repeatingEvents:
431 newEventNameList = repeatingEvent.name.split()
432 newEventNameList[-1] = f"{nameCounter}"
433 newEventNameList = " ".join(newEventNameList)
434 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute()
435 nameCounter += 1
436 flash("Event has been successfully restored.", "success")
437 return redirect(url_for("main.events", selectedTerm=g.current_term))
438 except Exception as e:
439 print('Error while canceling event:', e)
440 return "", 500
442@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
443def deleteRoute(eventId):
444 try:
445 deleteEvent(eventId)
446 session['lastDeletedEvent'] = [eventId]
447 flash("Event successfully deleted.", "success")
448 return redirect(url_for("main.events", selectedTerm=g.current_term))
450 except Exception as e:
451 print('Error while canceling event:', e)
452 return "", 500
454@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
455def deleteEventAndAllFollowingRoute(eventId):
456 try:
457 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
458 flash("Events successfully deleted.", "success")
459 return redirect(url_for("main.events", selectedTerm=g.current_term))
461 except Exception as e:
462 print('Error while canceling event:', e)
463 return "", 500
465@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST'])
466def deleteAllEventsInSeriesRoute(eventId):
467 try:
468 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId)
469 flash("Events successfully deleted.", "success")
470 return redirect(url_for("main.events", selectedTerm=g.current_term))
472 except Exception as e:
473 print('Error while canceling event:', e)
474 return "", 500
476@admin_bp.route('/makeRepeatingEvents', methods=['POST'])
477def addRepeatingEvents():
478 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy()))
479 return json.dumps(repeatingEvents, default=str)
482@admin_bp.route('/userProfile', methods=['POST'])
483def userProfile():
484 volunteerName= request.form.copy()
485 if volunteerName['searchStudentsInput']:
486 username = volunteerName['searchStudentsInput'].strip("()")
487 user=username.split('(')[-1]
488 return redirect(url_for('main.viewUsersProfile', username=user))
489 else:
490 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
491 return redirect(url_for('admin.studentSearchPage'))
493@admin_bp.route('/search_student', methods=['GET'])
494def studentSearchPage():
495 if g.current_user.isAdmin:
496 return render_template("/admin/searchStudentPage.html")
497 abort(403)
499@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
500def activityLogs():
501 if g.current_user.isCeltsAdmin:
502 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
503 return render_template("/admin/activityLogs.html",
504 allLogs = allLogs)
505 else:
506 abort(403)
508@admin_bp.route("/deleteEventFile", methods=["POST"])
509def deleteEventFile():
510 fileData= request.form
511 eventfile=FileHandler(eventId=fileData["databaseId"])
512 eventfile.deleteFile(fileData["fileId"])
513 return ""
515@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
516def addCourseFile():
517 fileData = request.files['addCourseParticipants']
518 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
519 fileData.save(filePath)
520 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
521 os.remove(filePath)
522 return redirect(url_for("admin.manageServiceLearningCourses"))
524@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
525@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
526def manageServiceLearningCourses(term=None):
528 """
529 The SLC management page for admins
530 """
531 if not g.current_user.isCeltsAdmin:
532 abort(403)
534 if request.method == 'POST' and "submitParticipant" in request.form:
535 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
536 flash('Courses and participants saved successfully!', 'success')
537 return redirect(url_for('admin.manageServiceLearningCourses'))
539 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
541 setRedirectTarget(request.full_path)
542 # retrieve and store the courseID of the imported course from a session variable if it exists.
543 # This allows us to export the courseID in the html and use it.
544 courseID = session.get("alterCourseId")
546 if courseID:
547 # delete courseID from the session if it was retrieved, for storage purposes.
548 session.pop("alterCourseId")
549 return render_template('/admin/manageServiceLearningFaculty.html',
550 courseInstructors = getInstructorCourses(),
551 unapprovedCourses = unapprovedCourses(manageTerm),
552 approvedCourses = approvedCourses(manageTerm),
553 importedCourses = getImportedCourses(manageTerm),
554 terms = selectSurroundingTerms(g.current_term),
555 term = manageTerm,
556 cpPreview = session.get('cpPreview', {}),
557 cpPreviewErrors = session.get('cpErrors', []),
558 courseID = courseID
559 )
561 return render_template('/admin/manageServiceLearningFaculty.html',
562 courseInstructors = getInstructorCourses(),
563 unapprovedCourses = unapprovedCourses(manageTerm),
564 approvedCourses = approvedCourses(manageTerm),
565 importedCourses = getImportedCourses(manageTerm),
566 terms = selectSurroundingTerms(g.current_term),
567 term = manageTerm,
568 cpPreview= session.get('cpPreview',{}),
569 cpPreviewErrors = session.get('cpErrors',[])
570 )
572@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
573def getSidebarInformation() -> str:
574 """
575 Get the count of unapproved courses and students interested in the minor for the current term
576 to display in the admin sidebar. It must be returned as a string to be received by the
577 ajax request.
578 """
579 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
580 interestedStudentsCount: int = len(getMinorInterest())
581 return {"unapprovedCoursesCount": unapprovedCoursesCount,
582 "interestedStudentsCount": interestedStudentsCount}
584@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
585def removeFromSession():
586 try:
587 session.pop('cpPreview')
588 except KeyError:
589 pass
591 return ""
593@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
594def alterImportedCourse(courseID):
595 """
596 This route handles a GET and a POST request for the purpose of imported courses.
597 The GET request provides preexisting information of an imported course in a modal.
598 The POST request updates a specific imported course (course name, course abbreviation,
599 hours earned on completion, list of instructors) in the database with new information
600 coming from the imported courses modal.
601 """
602 if request.method == 'GET':
603 try:
604 targetCourse = Course.get_by_id(courseID)
605 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
607 try:
608 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
609 except IndexError: # If a course has no participant, IndexError will be raised
610 serviceHours = 20
612 courseData = model_to_dict(targetCourse, recurse=False)
613 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
614 courseData['hoursEarned'] = serviceHours
616 return jsonify(courseData)
618 except DoesNotExist:
619 flash("Course not found")
620 return jsonify({"error": "Course not found"}), 404
622 if request.method == 'POST':
623 # Update course information in the database
624 courseData = request.form.copy()
625 editImportedCourses(courseData)
626 session['alterCourseId'] = courseID
628 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
631@admin_bp.route("/manageBonner")
632def manageBonner():
633 if not g.current_user.isCeltsAdmin:
634 abort(403)
636 return render_template("/admin/bonnerManagement.html",
637 cohorts=getBonnerCohorts(),
638 events=getBonnerEvents(g.current_term),
639 requirements=getCertRequirements(certification=Certification.BONNER))
641@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
642def updatecohort(year, method, username):
643 if not g.current_user.isCeltsAdmin:
644 abort(403)
646 try:
647 user = User.get_by_id(username)
648 except:
649 abort(500)
651 if method == "add":
652 try:
653 BonnerCohort.create(year=year, user=user)
654 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
655 except IntegrityError as e:
656 # if they already exist, ignore the error
657 flash(f'Error: {user.fullName} already added.', "danger")
658 pass
660 elif method == "remove":
661 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
662 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
663 else:
664 flash(f"Error: {user.fullName} can't be added.", "danger")
665 abort(500)
666 return ""
668@admin_bp.route("/bonnerXls/<startingYear>/<noOfYears>")
669def getBonnerXls(startingYear, noOfYears):
670 if not g.current_user.isCeltsAdmin:
671 abort(403)
672 newfile = makeBonnerXls(startingYear, noOfYears)
673 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
676@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
677def saveRequirements(certid):
678 if not g.current_user.isCeltsAdmin:
679 abort(403)
681 newRequirements = updateCertRequirements(certid, request.get_json())
683 return jsonify([requirement.id for requirement in newRequirements])
686@admin_bp.route("/displayEventFile", methods=["POST"])
687def displayEventFile():
688 fileData = request.form
689 eventfile = FileHandler(eventId=fileData["id"])
690 isChecked = fileData.get('checked') == 'true'
691 eventfile.changeDisplay(fileData['id'], isChecked)
692 return ""
694@admin_bp.route("/handbookSignature", methods=["POST"])
695def handbookSignature():
696 if not (g.current_user.username == request.form.get('studentUsername')):
697 abort(403)
699 signer = g.current_user
700 signer.lastHandbookSignature = datetime.now()
701 signer.signatureTerm = g.current_term
702 signer.save()
704 return ""