Coverage for app/controllers/admin/routes.py: 21%
480 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-09-22 13:52 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-09-22 13:52 +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 event.isCampusGroupsSynced = False
347 event.save(only=[Event.isCampusGroupsSynced])
348 return render_template("events/createEvent.html",
349 eventData = eventData,
350 termList = Term.select().order_by(Term.termOrder),
351 event = event,
352 requirements = requirements,
353 bonnerCohorts = bonnerCohorts,
354 invitedYears = invitedYears,
355 userHasRSVPed = userHasRSVPed,
356 isProgramManager = isProgramManager,
357 filepaths = filepaths)
358 # Event View
359 else:
360 # get text representations of dates for html
361 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
362 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
363 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
364 eventCountdown = getCountdownToEvent(event)
367 # Identify the next event in a repeating series
368 if event.seriesId:
369 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId)
370 .where((Event.isCanceled == False) | (Event.id == event.id))
371 .order_by(Event.startDate))
372 eventIndex = eventSeriesList.index(event)
373 if len(eventSeriesList) != (eventIndex + 1):
374 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1]
376 currentEventRsvpAmount = getEventRsvpCount(event.id)
378 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term, includeFutureEvents = False)
380 # CampusGroups integration
381 if event.campusGroupsId:
382 eventData["campusGroupsURL"] = event.campusGroupsURL
383 eventData["campusGroupsId"] = event.campusGroupsId
384 eventData["isCampusGroupsSynced"] = event.isCampusGroupsSynced
385 return render_template("events/eventView.html",
386 eventData=eventData,
387 event=event,
388 userHasRSVPed=userHasRSVPed,
389 programTrainings=userParticipatedTrainingEvents,
390 currentEventRsvpAmount=currentEventRsvpAmount,
391 isProgramManager=isProgramManager,
392 filepaths=filepaths,
393 image=image,
394 pageViewsCount=pageViewsCount,
395 invitedYears=invitedYears,
396 eventCountdown=eventCountdown
397 )
401@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
402def cancelRoute(eventId):
403 if g.current_user.isAdmin:
404 try:
405 cancelEvent(eventId)
406 return redirect(request.referrer)
408 except Exception as e:
409 print('Error while canceling event:', e)
410 return "", 500
412 else:
413 abort(403)
415@admin_bp.route('/profile/undo', methods=['GET'])
416def undoBackgroundCheck():
417 try:
418 username = g.current_user
419 bgCheckId = session['lastDeletedBgCheck']
420 BackgroundCheck.update({BackgroundCheck.deletionDate: None, BackgroundCheck.deletedBy: None}).where(BackgroundCheck.id == bgCheckId).execute()
421 flash("Background Check has been successfully restored.", "success")
422 return redirect (f"/profile/{username}?accordion=background")
423 except Exception as e:
424 print('Error while undoing background check:', e)
425 return "", 500
427@admin_bp.route('/event/undo', methods=['GET'])
428def undoEvent():
429 try:
430 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted
431 for eventId in eventIds:
432 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
433 event = Event.get_or_none(Event.id == eventId)
434 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id))
435 if event.isRepeating:
436 nameCounter = 1
437 for repeatingEvent in repeatingEvents:
438 newEventNameList = repeatingEvent.name.split()
439 newEventNameList[-1] = f"{nameCounter}"
440 newEventNameList = " ".join(newEventNameList)
441 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute()
442 nameCounter += 1
443 flash("Event has been successfully restored.", "success")
444 return redirect(url_for("main.events", selectedTerm=g.current_term))
445 except Exception as e:
446 print('Error while canceling event:', e)
447 return "", 500
449@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
450def deleteRoute(eventId):
451 try:
452 deleteEvent(eventId)
453 session['lastDeletedEvent'] = [eventId]
454 flash("Event successfully deleted.", "success")
455 return redirect(url_for("main.events", selectedTerm=g.current_term))
457 except Exception as e:
458 print('Error while canceling event:', e)
459 return "", 500
461@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
462def deleteEventAndAllFollowingRoute(eventId):
463 try:
464 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
465 flash("Events successfully deleted.", "success")
466 return redirect(url_for("main.events", selectedTerm=g.current_term))
468 except Exception as e:
469 print('Error while canceling event:', e)
470 return "", 500
472@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST'])
473def deleteAllEventsInSeriesRoute(eventId):
474 try:
475 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId)
476 flash("Events successfully deleted.", "success")
477 return redirect(url_for("main.events", selectedTerm=g.current_term))
479 except Exception as e:
480 print('Error while canceling event:', e)
481 return "", 500
483@admin_bp.route('/makeRepeatingEvents', methods=['POST'])
484def addRepeatingEvents():
485 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy()))
486 return json.dumps(repeatingEvents, default=str)
489@admin_bp.route('/userProfile', methods=['POST'])
490def userProfile():
491 volunteerName= request.form.copy()
492 if volunteerName['searchStudentsInput']:
493 username = volunteerName['searchStudentsInput'].strip("()")
494 user=username.split('(')[-1]
495 return redirect(url_for('main.viewUsersProfile', username=user))
496 else:
497 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
498 return redirect(url_for('admin.studentSearchPage'))
500@admin_bp.route('/search_student', methods=['GET'])
501def studentSearchPage():
502 if g.current_user.isAdmin:
503 return render_template("/admin/searchStudentPage.html")
504 abort(403)
506@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
507def activityLogs():
508 if g.current_user.isCeltsAdmin:
509 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
510 return render_template("/admin/activityLogs.html",
511 allLogs = allLogs)
512 else:
513 abort(403)
515@admin_bp.route("/deleteEventFile", methods=["POST"])
516def deleteEventFile():
517 fileData= request.form
518 eventfile=FileHandler(eventId=fileData["databaseId"])
519 eventfile.deleteFile(fileData["fileId"])
520 return ""
522@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
523def addCourseFile():
524 fileData = request.files['addCourseParticipants']
525 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
526 fileData.save(filePath)
527 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
528 os.remove(filePath)
529 return redirect(url_for("admin.manageServiceLearningCourses"))
531@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
532@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
533def manageServiceLearningCourses(term=None):
535 """
536 The SLC management page for admins
537 """
538 if not g.current_user.isCeltsAdmin:
539 abort(403)
541 if request.method == 'POST' and "submitParticipant" in request.form:
542 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
543 flash('Courses and participants saved successfully!', 'success')
544 return redirect(url_for('admin.manageServiceLearningCourses'))
546 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
548 setRedirectTarget(request.full_path)
549 # retrieve and store the courseID of the imported course from a session variable if it exists.
550 # This allows us to export the courseID in the html and use it.
551 courseID = session.get("alterCourseId")
553 if courseID:
554 # delete courseID from the session if it was retrieved, for storage purposes.
555 session.pop("alterCourseId")
556 return render_template('/admin/manageServiceLearningFaculty.html',
557 courseInstructors = getInstructorCourses(),
558 unapprovedCourses = unapprovedCourses(manageTerm),
559 approvedCourses = approvedCourses(manageTerm),
560 importedCourses = getImportedCourses(manageTerm),
561 terms = selectSurroundingTerms(g.current_term),
562 term = manageTerm,
563 cpPreview = session.get('cpPreview', {}),
564 cpPreviewErrors = session.get('cpErrors', []),
565 courseID = courseID
566 )
568 return render_template('/admin/manageServiceLearningFaculty.html',
569 courseInstructors = getInstructorCourses(),
570 unapprovedCourses = unapprovedCourses(manageTerm),
571 approvedCourses = approvedCourses(manageTerm),
572 importedCourses = getImportedCourses(manageTerm),
573 terms = selectSurroundingTerms(g.current_term),
574 term = manageTerm,
575 cpPreview= session.get('cpPreview',{}),
576 cpPreviewErrors = session.get('cpErrors',[])
577 )
579@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
580def getSidebarInformation() -> str:
581 """
582 Get the count of unapproved courses and students interested in the minor for the current term
583 to display in the admin sidebar. It must be returned as a string to be received by the
584 ajax request.
585 """
586 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
587 interestedStudentsCount: int = len(getMinorInterest())
588 return {"unapprovedCoursesCount": unapprovedCoursesCount,
589 "interestedStudentsCount": interestedStudentsCount}
591@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
592def removeFromSession():
593 try:
594 session.pop('cpPreview')
595 except KeyError:
596 pass
598 return ""
600@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
601def alterImportedCourse(courseID):
602 """
603 This route handles a GET and a POST request for the purpose of imported courses.
604 The GET request provides preexisting information of an imported course in a modal.
605 The POST request updates a specific imported course (course name, course abbreviation,
606 hours earned on completion, list of instructors) in the database with new information
607 coming from the imported courses modal.
608 """
609 if request.method == 'GET':
610 try:
611 targetCourse = Course.get_by_id(courseID)
612 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
614 try:
615 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
616 except IndexError: # If a course has no participant, IndexError will be raised
617 serviceHours = 20
619 courseData = model_to_dict(targetCourse, recurse=False)
620 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
621 courseData['hoursEarned'] = serviceHours
623 return jsonify(courseData)
625 except DoesNotExist:
626 flash("Course not found")
627 return jsonify({"error": "Course not found"}), 404
629 if request.method == 'POST':
630 # Update course information in the database
631 courseData = request.form.copy()
632 editImportedCourses(courseData)
633 session['alterCourseId'] = courseID
635 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
638@admin_bp.route("/manageBonner")
639def manageBonner():
640 if not g.current_user.isCeltsAdmin:
641 abort(403)
643 return render_template("/admin/bonnerManagement.html",
644 cohorts=getBonnerCohorts(),
645 events=getBonnerEvents(g.current_term),
646 requirements=getCertRequirements(certification=Certification.BONNER))
648@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
649def updatecohort(year, method, username):
650 if not g.current_user.isCeltsAdmin:
651 abort(403)
653 try:
654 user = User.get_by_id(username)
655 except:
656 abort(500)
658 if method == "add":
659 try:
660 BonnerCohort.create(year=year, user=user)
661 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
662 except IntegrityError as e:
663 # if they already exist, ignore the error
664 flash(f'Error: {user.fullName} already added.', "danger")
665 pass
667 elif method == "remove":
668 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
669 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
670 else:
671 flash(f"Error: {user.fullName} can't be added.", "danger")
672 abort(500)
673 return ""
675@admin_bp.route("/bonnerXls/<startingYear>/<noOfYears>")
676def getBonnerXls(startingYear, noOfYears):
677 if not g.current_user.isCeltsAdmin:
678 abort(403)
679 newfile = makeBonnerXls(startingYear, noOfYears)
680 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
683@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
684def saveRequirements(certid):
685 if not g.current_user.isCeltsAdmin:
686 abort(403)
688 newRequirements = updateCertRequirements(certid, request.get_json())
690 return jsonify([requirement.id for requirement in newRequirements])
693@admin_bp.route("/displayEventFile", methods=["POST"])
694def displayEventFile():
695 fileData = request.form
696 eventfile = FileHandler(eventId=fileData["id"])
697 isChecked = fileData.get('checked') == 'true'
698 eventfile.changeDisplay(fileData['id'], isChecked)
699 return ""
701@admin_bp.route("/handbookSignature", methods=["POST"])
702def handbookSignature():
703 if not (g.current_user.username == request.form.get('studentUsername')):
704 abort(403)
706 signer = g.current_user
707 signer.lastHandbookSignature = datetime.now()
708 signer.signatureTerm = g.current_term
709 signer.save()
711 return ""