Coverage for app/controllers/admin/routes.py: 22%
455 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-12-17 17:33 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-12-17 17:33 +0000
1from flask import request, render_template, url_for, g, redirect
2from flask import flash, abort, jsonify, session, send_file
3from peewee import DoesNotExist, fn, 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
31from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates
32from app.logic.createLogs import createActivityLog
33from app.logic.certification import getCertRequirements, updateCertRequirements
34from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget
35from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRepeatingEventsData, deleteEventAndAllFollowing, deleteAllEventsInSeries, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewSeriesId, inviteCohortsToEvent, updateEventCohorts
36from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp
37from app.logic.minor import getMinorInterest
38from app.logic.fileHandler import FileHandler
39from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog
40from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses
42from app.controllers.admin import admin_bp
43from app.logic.volunteerSpreadsheet import createSpreadsheet
46@admin_bp.route('/admin/reports')
47def reports():
48 academicYears = Term.select(Term.academicYear).distinct().order_by(Term.academicYear.desc())
49 academicYears = list(map(lambda t: t.academicYear, academicYears))
50 return render_template("/admin/reports.html", academicYears=academicYears)
52@admin_bp.route('/admin/reports/download', methods=['POST'])
53def downloadFile():
54 academicYear = request.form.get('academicYear')
55 filepath = os.path.abspath(createSpreadsheet(academicYear))
56 return send_file(filepath, as_attachment=True)
60@admin_bp.route('/switch_user', methods=['POST'])
61def switchUser():
62 if app.env == "production":
63 print(f"An attempt was made to switch to another user by {g.current_user.username}!")
64 abort(403)
66 print(f"Switching user from {g.current_user} to",request.form['newuser'])
67 session['current_user'] = model_to_dict(User.get_by_id(request.form['newuser']))
69 return redirect(request.referrer)
72@admin_bp.route('/eventTemplates')
73def templateSelect():
74 programs = getAllowedPrograms(g.current_user)
75 if not programs:
76 abort(403)
77 visibleTemplates = getAllowedTemplates(g.current_user)
78 return render_template("/events/templateSelector.html",
79 programs=programs,
80 celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored),
81 templates=visibleTemplates)
83@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
84def createEvent(templateid, programid):
85 if not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerFor(programid)):
86 abort(403)
88 # Validate given URL
89 program = None
90 try:
91 template = EventTemplate.get_by_id(templateid)
92 if programid:
93 program = Program.get_by_id(programid)
94 except DoesNotExist as e:
95 print("Invalid template or program id:", e)
96 flash("There was an error with your selection. Please try again or contact Systems Support.", "danger")
97 return redirect(url_for("admin.program_picker"))
99 # Get the data from the form or from the template
100 eventData = template.templateData
101 eventData['program'] = program
103 if request.method == "GET":
104 eventData['contactName'] = "CELTS Admin"
105 eventData['contactEmail'] = app.config['celts_admin_contact']
106 if program:
107 eventData['location'] = program.defaultLocation
108 if program.contactName:
109 eventData['contactName'] = program.contactName
110 if program.contactEmail:
111 eventData['contactEmail'] = program.contactEmail
113 # Try to save the form
114 if request.method == "POST":
115 savedEvents = None
116 eventData.update(request.form.copy())
117 eventData = preprocessEventData(eventData)
119 if eventData.get('isSeries'):
120 eventData['seriesData'] = json.loads(eventData['seriesData'])
121 succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request))
122 if not succeeded:
123 for index, validationErrorMessage in failedSavedOfferings:
124 eventData['seriesData'][index]['isDuplicate'] = True
125 validationErrorMessage = failedSavedOfferings[-1][1] # The last validation error message from the list of offerings if there are multiple
126 print(f"Failed to save offerings {failedSavedOfferings}")
127 else:
128 try:
129 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
130 except Exception as e:
131 print("Failed saving regular event", e)
132 validationErrorMessage = "Failed to save event."
134 if savedEvents:
135 rsvpCohorts = request.form.getlist("cohorts[]")
136 if rsvpCohorts:
137 success, message, invitedCohorts = inviteCohortsToEvent(savedEvents[0], rsvpCohorts)
138 if not success:
139 flash(message, 'warning')
141 noun = ((eventData.get('isSeries')) and "Events" or "Event") # pluralize
142 flash(f"{noun} successfully created!", 'success')
145 if program:
146 if len(savedEvents) > 1 and eventData.get('isRepeating'):
147 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')}.")
148 elif len(savedEvents) >= 1 and eventData.get('isSeries'):
149 eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents]
150 eventList = ', '.join(f"<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>" for event in savedEvents)
152 if len(savedEvents) > 1:
153 #creates list of events created in a multiple series to display in the logs
154 eventList = ', '.join(eventList.split(', ')[:-1]) + f', and ' + eventList.split(', ')[-1]
155 #get last date and stick at the end after 'and' so that it reads like a sentence in admin log
156 lastEventDate = eventDates[-1]
157 eventDates = ', '.join(eventDates[:-1]) + f', and {lastEventDate}'
159 createActivityLog(f"Created series {eventList} for {program.programName}, with start dates of {eventDates}.")
161 else:
162 createActivityLog(f"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')}.")
163 else:
164 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')}.")
166 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id))
167 else:
168 flash(validationErrorMessage, 'warning')
170 # make sure our data is the same regardless of GET or POST
171 preprocessEventData(eventData)
172 isProgramManager = g.current_user.isProgramManagerFor(programid)
174 requirements, bonnerCohorts = [], []
175 if eventData['program'] is not None and eventData['program'].isBonnerScholars:
176 requirements = getCertRequirements(Certification.BONNER)
177 rawBonnerCohorts = getBonnerCohorts(limit=5)
178 bonnerCohorts = {}
180 for year, cohort in rawBonnerCohorts.items():
181 if cohort:
182 bonnerCohorts[year] = cohort
185 return render_template(f"/events/{template.templateFile}",
186 template = template,
187 eventData = eventData,
188 termList = selectSurroundingTerms(g.current_term, prevTerms=0),
189 requirements = requirements,
190 bonnerCohorts = bonnerCohorts,
191 isProgramManager = isProgramManager)
194@admin_bp.route('/event/<eventId>/rsvp', methods=['GET'])
195def rsvpLogDisplay(eventId):
196 event = Event.get_by_id(eventId)
197 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)):
198 allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc())
199 return render_template("/events/rsvpLog.html",
200 event = event,
201 allLogs = allLogs)
202 else:
203 abort(403)
205@admin_bp.route('/event/<eventId>/renew', methods=['POST'])
206def renewEvent(eventId):
207 try:
208 formData = request.form
209 try:
210 assert formData['timeStart'] < formData['timeEnd']
211 except AssertionError:
212 flash("End time must be after start time", 'warning')
213 return redirect(url_for('admin.eventDisplay', eventId = eventId))
215 try:
216 if formData.get('dateEnd'):
217 assert formData['dateStart'] < formData['dateEnd']
218 except AssertionError:
219 flash("End date must be after start date", 'warning')
220 return redirect(url_for('admin.eventDisplay', eventId = eventId))
223 priorEvent = model_to_dict(Event.get_by_id(eventId))
224 newEventDict = priorEvent.copy()
225 newEventDict.pop('id')
226 newEventDict.update({
227 'program': int(priorEvent['program']['id']),
228 'term': int(priorEvent['term']['id']),
229 'timeStart': formData['timeStart'],
230 'timeEnd': formData['timeEnd'],
231 'location': formData['location'],
232 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
233 'isRepeating': bool(priorEvent['isRepeating']),
234 'seriesId': priorEvent['seriesId'],
235 })
236 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
237 if message:
238 flash(message, "danger")
239 return redirect(url_for('admin.eventDisplay', eventId = eventId))
241 copyRsvpToNewEvent(priorEvent, newEvent[0])
242 createActivityLog(f"Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.")
243 flash("Event successfully renewed.", "success")
244 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
247 except Exception as e:
248 print("Error while trying to renew event:", e)
249 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
250 return redirect(url_for('admin.eventDisplay', eventId = eventId))
254@admin_bp.route('/event/<eventId>/view', methods=['GET'])
255@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
256def eventDisplay(eventId):
257 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
258 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
259 viewer = g.current_user
260 event = Event.get_by_id(eventId)
261 addEventView(viewer,event)
262 # Validate given URL
263 try:
264 event = Event.get_by_id(eventId)
265 invitedCohorts = list(EventCohort.select().where(
266 EventCohort.event == event
267 ))
268 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
269 except DoesNotExist as e:
270 print(f"Unknown event: {eventId}")
271 abort(404)
273 notPermitted = not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerForEvent(event))
274 if 'edit' in request.url_rule.rule and notPermitted:
275 abort(403)
277 eventData = model_to_dict(event, recurse=False)
278 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
279 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
281 image = None
282 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
283 for attachment in associatedAttachments:
284 for extension in picurestype:
285 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
286 image = filepaths[attachment.fileName][0]
287 if image:
288 break
291 if request.method == "POST": # Attempt to save form
292 eventData = request.form.copy()
293 try:
294 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
296 except Exception as e:
297 print("Error saving event:", e)
298 savedEvents = False
299 validationErrorMessage = "Unknown Error Saving Event. Please try again"
302 if savedEvents:
303 rsvpCohorts = request.form.getlist("cohorts[]")
304 updateEventCohorts(savedEvents[0], rsvpCohorts)
305 flash("Event successfully updated!", "success")
306 return redirect(url_for("admin.eventDisplay", eventId = event.id))
307 else:
308 flash(validationErrorMessage, 'warning')
310 # make sure our data is the same regardless of GET and POST
311 preprocessEventData(eventData)
312 eventData['program'] = event.program
313 userHasRSVPed = checkUserRsvp(g.current_user, event)
314 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
315 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
316 requirements, bonnerCohorts = [], []
318 if eventData['program'] and eventData['program'].isBonnerScholars:
319 requirements = getCertRequirements(Certification.BONNER)
320 rawBonnerCohorts = getBonnerCohorts(limit=5)
321 bonnerCohorts = {}
323 for year, cohort in rawBonnerCohorts.items():
324 if cohort:
325 bonnerCohorts[year] = cohort
327 invitedCohorts = list(EventCohort.select().where(
328 EventCohort.event_id == eventId,
329 ))
330 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
331 else:
332 requirements, bonnerCohorts, invitedYears = [], [], []
334 rule = request.url_rule
336 # Event Edit
337 if 'edit' in rule.rule:
338 return render_template("events/createEvent.html",
339 eventData = eventData,
340 termList = Term.select().order_by(Term.termOrder),
341 event = event,
342 requirements = requirements,
343 bonnerCohorts = bonnerCohorts,
344 invitedYears = invitedYears,
345 userHasRSVPed = userHasRSVPed,
346 isProgramManager = isProgramManager,
347 filepaths = filepaths)
348 # Event View
349 else:
350 # get text representations of dates for html
351 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
352 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
353 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
354 eventCountdown = getCountdownToEvent(event)
357 # Identify the next event in a repeating series
358 if event.seriesId:
359 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId)
360 .where((Event.isCanceled == False) | (Event.id == event.id))
361 .order_by(Event.startDate))
362 eventIndex = eventSeriesList.index(event)
363 if len(eventSeriesList) != (eventIndex + 1):
364 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1]
366 currentEventRsvpAmount = getEventRsvpCount(event.id)
368 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
370 return render_template("events/eventView.html",
371 eventData=eventData,
372 event=event,
373 userHasRSVPed=userHasRSVPed,
374 programTrainings=userParticipatedTrainingEvents,
375 currentEventRsvpAmount=currentEventRsvpAmount,
376 isProgramManager=isProgramManager,
377 filepaths=filepaths,
378 image=image,
379 pageViewsCount=pageViewsCount,
380 invitedYears=invitedYears,
381 eventCountdown=eventCountdown
382 )
386@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
387def cancelRoute(eventId):
388 if g.current_user.isAdmin:
389 try:
390 cancelEvent(eventId)
391 return redirect(request.referrer)
393 except Exception as e:
394 print('Error while canceling event:', e)
395 return "", 500
397 else:
398 abort(403)
400@admin_bp.route('/profile/undo', methods=['GET'])
401def undoBackgroundCheck():
402 try:
403 username = g.current_user
404 bgCheckId = session['lastDeletedBgCheck']
405 BackgroundCheck.update({BackgroundCheck.deletionDate: None, BackgroundCheck.deletedBy: None}).where(BackgroundCheck.id == bgCheckId).execute()
406 flash("Background Check has been successfully restored.", "success")
407 return redirect (f"/profile/{username}?accordion=background")
408 except Exception as e:
409 print('Error while undoing background check:', e)
410 return "", 500
412@admin_bp.route('/event/undo', methods=['GET'])
413def undoEvent():
414 try:
415 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted
416 for eventId in eventIds:
417 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
418 event = Event.get_or_none(Event.id == eventId)
419 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id))
420 if event.isRepeating:
421 nameCounter = 1
422 for repeatingEvent in repeatingEvents:
423 newEventNameList = repeatingEvent.name.split()
424 newEventNameList[-1] = f"{nameCounter}"
425 newEventNameList = " ".join(newEventNameList)
426 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute()
427 nameCounter += 1
428 flash("Event has been successfully restored.", "success")
429 return redirect(url_for("main.events", selectedTerm=g.current_term))
430 except Exception as e:
431 print('Error while canceling event:', e)
432 return "", 500
434@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
435def deleteRoute(eventId):
436 try:
437 deleteEvent(eventId)
438 session['lastDeletedEvent'] = [eventId]
439 flash("Event successfully deleted.", "success")
440 return redirect(url_for("main.events", selectedTerm=g.current_term))
442 except Exception as e:
443 print('Error while canceling event:', e)
444 return "", 500
446@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
447def deleteEventAndAllFollowingRoute(eventId):
448 try:
449 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
450 flash("Events successfully deleted.", "success")
451 return redirect(url_for("main.events", selectedTerm=g.current_term))
453 except Exception as e:
454 print('Error while canceling event:', e)
455 return "", 500
457@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST'])
458def deleteAllEventsInSeriesRoute(eventId):
459 try:
460 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId)
461 flash("Events successfully deleted.", "success")
462 return redirect(url_for("main.events", selectedTerm=g.current_term))
464 except Exception as e:
465 print('Error while canceling event:', e)
466 return "", 500
468@admin_bp.route('/makeRepeatingEvents', methods=['POST'])
469def addRepeatingEvents():
470 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy()))
471 return json.dumps(repeatingEvents, default=str)
474@admin_bp.route('/userProfile', methods=['POST'])
475def userProfile():
476 volunteerName= request.form.copy()
477 if volunteerName['searchStudentsInput']:
478 username = volunteerName['searchStudentsInput'].strip("()")
479 user=username.split('(')[-1]
480 return redirect(url_for('main.viewUsersProfile', username=user))
481 else:
482 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
483 return redirect(url_for('admin.studentSearchPage'))
485@admin_bp.route('/search_student', methods=['GET'])
486def studentSearchPage():
487 if g.current_user.isAdmin:
488 return render_template("/admin/searchStudentPage.html")
489 abort(403)
491@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
492def activityLogs():
493 if g.current_user.isCeltsAdmin:
494 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
495 return render_template("/admin/activityLogs.html",
496 allLogs = allLogs)
497 else:
498 abort(403)
500@admin_bp.route("/deleteEventFile", methods=["POST"])
501def deleteEventFile():
502 fileData= request.form
503 eventfile=FileHandler(eventId=fileData["databaseId"])
504 eventfile.deleteFile(fileData["fileId"])
505 return ""
507@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
508def addCourseFile():
509 fileData = request.files['addCourseParticipants']
510 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
511 fileData.save(filePath)
512 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
513 os.remove(filePath)
514 return redirect(url_for("admin.manageServiceLearningCourses"))
516@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
517@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
518def manageServiceLearningCourses(term=None):
520 """
521 The SLC management page for admins
522 """
523 if not g.current_user.isCeltsAdmin:
524 abort(403)
526 if request.method == 'POST' and "submitParticipant" in request.form:
527 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
528 flash('Courses and participants saved successfully!', 'success')
529 return redirect(url_for('admin.manageServiceLearningCourses'))
531 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
533 setRedirectTarget(request.full_path)
534 # retrieve and store the courseID of the imported course from a session variable if it exists.
535 # This allows us to export the courseID in the html and use it.
536 courseID = session.get("alterCourseId")
538 if courseID:
539 # delete courseID from the session if it was retrieved, for storage purposes.
540 session.pop("alterCourseId")
541 return render_template('/admin/manageServiceLearningFaculty.html',
542 courseInstructors = getInstructorCourses(),
543 unapprovedCourses = unapprovedCourses(manageTerm),
544 approvedCourses = approvedCourses(manageTerm),
545 importedCourses = getImportedCourses(manageTerm),
546 terms = selectSurroundingTerms(g.current_term),
547 term = manageTerm,
548 cpPreview = session.get('cpPreview', {}),
549 cpPreviewErrors = session.get('cpErrors', []),
550 courseID = courseID
551 )
553 return render_template('/admin/manageServiceLearningFaculty.html',
554 courseInstructors = getInstructorCourses(),
555 unapprovedCourses = unapprovedCourses(manageTerm),
556 approvedCourses = approvedCourses(manageTerm),
557 importedCourses = getImportedCourses(manageTerm),
558 terms = selectSurroundingTerms(g.current_term),
559 term = manageTerm,
560 cpPreview= session.get('cpPreview',{}),
561 cpPreviewErrors = session.get('cpErrors',[])
562 )
564@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
565def getSidebarInformation() -> str:
566 """
567 Get the count of unapproved courses and students interested in the minor for the current term
568 to display in the admin sidebar. It must be returned as a string to be received by the
569 ajax request.
570 """
571 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
572 interestedStudentsCount: int = len(getMinorInterest())
573 return {"unapprovedCoursesCount": unapprovedCoursesCount,
574 "interestedStudentsCount": interestedStudentsCount}
576@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
577def removeFromSession():
578 try:
579 session.pop('cpPreview')
580 except KeyError:
581 pass
583 return ""
585@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
586def alterImportedCourse(courseID):
587 """
588 This route handles a GET and a POST request for the purpose of imported courses.
589 The GET request provides preexisting information of an imported course in a modal.
590 The POST request updates a specific imported course (course name, course abbreviation,
591 hours earned on completion, list of instructors) in the database with new information
592 coming from the imported courses modal.
593 """
594 if request.method == 'GET':
595 try:
596 targetCourse = Course.get_by_id(courseID)
597 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
599 try:
600 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
601 except IndexError: # If a course has no participant, IndexError will be raised
602 serviceHours = 20
604 courseData = model_to_dict(targetCourse, recurse=False)
605 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
606 courseData['hoursEarned'] = serviceHours
608 return jsonify(courseData)
610 except DoesNotExist:
611 flash("Course not found")
612 return jsonify({"error": "Course not found"}), 404
614 if request.method == 'POST':
615 # Update course information in the database
616 courseData = request.form.copy()
617 editImportedCourses(courseData)
618 session['alterCourseId'] = courseID
620 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
623@admin_bp.route("/manageBonner")
624def manageBonner():
625 if not g.current_user.isCeltsAdmin:
626 abort(403)
628 return render_template("/admin/bonnerManagement.html",
629 cohorts=getBonnerCohorts(),
630 events=getBonnerEvents(g.current_term),
631 requirements=getCertRequirements(certification=Certification.BONNER))
633@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
634def updatecohort(year, method, username):
635 if not g.current_user.isCeltsAdmin:
636 abort(403)
638 try:
639 user = User.get_by_id(username)
640 except:
641 abort(500)
643 if method == "add":
644 try:
645 BonnerCohort.create(year=year, user=user)
646 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
647 except IntegrityError as e:
648 # if they already exist, ignore the error
649 flash(f'Error: {user.fullName} already added.', "danger")
650 pass
652 elif method == "remove":
653 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
654 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
655 else:
656 flash(f"Error: {user.fullName} can't be added.", "danger")
657 abort(500)
658 return ""
660@admin_bp.route("/bonnerXls/<startingYear>/<noOfYears>")
661def getBonnerXls(startingYear, noOfYears):
662 if not g.current_user.isCeltsAdmin:
663 abort(403)
664 newfile = makeBonnerXls(startingYear, noOfYears)
665 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
668@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
669def saveRequirements(certid):
670 if not g.current_user.isCeltsAdmin:
671 abort(403)
673 newRequirements = updateCertRequirements(certid, request.get_json())
675 return jsonify([requirement.id for requirement in newRequirements])
678@admin_bp.route("/displayEventFile", methods=["POST"])
679def displayEventFile():
680 fileData = request.form
681 eventfile = FileHandler(eventId=fileData["id"])
682 isChecked = fileData.get('checked') == 'true'
683 eventfile.changeDisplay(fileData['id'], isChecked)
684 return ""