Coverage for app/controllers/admin/routes.py: 22%
457 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-03-12 21:45 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2025-03-12 21:45 +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.spreadsheet 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 if g.current_user.isCeltsAdmin or g.current_user.isCeltsStudentStaff:
75 allprograms = getAllowedPrograms(g.current_user)
76 visibleTemplates = getAllowedTemplates(g.current_user)
77 return render_template("/events/templateSelector.html",
78 programs=allprograms,
79 celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored),
80 templates=visibleTemplates)
81 else:
82 abort(403)
85@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
86def createEvent(templateid, programid):
87 if not (g.current_user.isAdmin or g.current_user.isProgramManagerFor(programid)):
88 abort(403)
90 # Validate given URL
91 program = None
92 try:
93 template = EventTemplate.get_by_id(templateid)
94 if programid:
95 program = Program.get_by_id(programid)
96 except DoesNotExist as e:
97 print("Invalid template or program id:", e)
98 flash("There was an error with your selection. Please try again or contact Systems Support.", "danger")
99 return redirect(url_for("admin.program_picker"))
101 # Get the data from the form or from the template
102 eventData = template.templateData
103 eventData['program'] = program
105 if request.method == "GET":
106 eventData['contactName'] = "CELTS Admin"
107 eventData['contactEmail'] = app.config['celts_admin_contact']
108 if program:
109 eventData['location'] = program.defaultLocation
110 if program.contactName:
111 eventData['contactName'] = program.contactName
112 if program.contactEmail:
113 eventData['contactEmail'] = program.contactEmail
115 # Try to save the form
116 if request.method == "POST":
117 savedEvents = None
118 eventData.update(request.form.copy())
119 eventData = preprocessEventData(eventData)
120 if eventData.get('isSeries'):
121 eventData['seriesData'] = json.loads(eventData['seriesData'])
122 succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request))
123 if not succeeded:
124 for index, validationErrorMessage in failedSavedOfferings:
125 eventData['seriesData'][index]['isDuplicate'] = True
126 validationErrorMessage = failedSavedOfferings[-1][1] # The last validation error message from the list of offerings if there are multiple
127 print(f"Failed to save offerings {failedSavedOfferings}")
128 else:
129 try:
130 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
131 except Exception as e:
132 print("Failed saving regular event", e)
133 validationErrorMessage = "Failed to save event."
135 if savedEvents:
136 rsvpCohorts = request.form.getlist("cohorts[]")
137 if rsvpCohorts:
138 success, message, invitedCohorts = inviteCohortsToEvent(savedEvents[0], rsvpCohorts)
139 if not success:
140 flash(message, 'warning')
142 noun = ((eventData.get('isSeries')) and "Events" or "Event") # pluralize
143 flash(f"{noun} successfully created!", 'success')
146 if program:
147 if len(savedEvents) > 1 and eventData.get('isRepeating'):
148 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')}.")
149 elif len(savedEvents) >= 1 and eventData.get('isSeries'):
150 eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents]
151 eventList = ', '.join(f"<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>" for event in savedEvents)
153 if len(savedEvents) > 1:
154 #creates list of events created in a multiple series to display in the logs
155 eventList = ', '.join(eventList.split(', ')[:-1]) + f', and ' + eventList.split(', ')[-1]
156 #get last date and stick at the end after 'and' so that it reads like a sentence in admin log
157 lastEventDate = eventDates[-1]
158 eventDates = ', '.join(eventDates[:-1]) + f', and {lastEventDate}'
160 createActivityLog(f"Created series {eventList} for {program.programName}, with start dates of {eventDates}.")
162 else:
163 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')}.")
164 else:
165 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')}.")
167 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id))
168 else:
169 flash(validationErrorMessage, 'warning')
171 # make sure our data is the same regardless of GET or POST
172 preprocessEventData(eventData)
173 isProgramManager = g.current_user.isProgramManagerFor(programid)
175 futureTerms = selectSurroundingTerms(g.current_term, prevTerms=0)
177 requirements, bonnerCohorts = [], []
178 if eventData['program'] is not None and eventData['program'].isBonnerScholars:
179 requirements = getCertRequirements(Certification.BONNER)
180 rawBonnerCohorts = getBonnerCohorts(limit=5)
181 bonnerCohorts = {}
183 for year, cohort in rawBonnerCohorts.items():
184 if cohort:
185 bonnerCohorts[year] = cohort
188 return render_template(f"/events/{template.templateFile}",
189 template = template,
190 eventData = eventData,
191 futureTerms = futureTerms,
192 requirements = requirements,
193 bonnerCohorts = bonnerCohorts,
194 isProgramManager = isProgramManager)
197@admin_bp.route('/event/<eventId>/rsvp', methods=['GET'])
198def rsvpLogDisplay(eventId):
199 event = Event.get_by_id(eventId)
200 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)):
201 allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc())
202 return render_template("/events/rsvpLog.html",
203 event = event,
204 allLogs = allLogs)
205 else:
206 abort(403)
208@admin_bp.route('/event/<eventId>/renew', methods=['POST'])
209def renewEvent(eventId):
210 try:
211 formData = request.form
212 try:
213 assert formData['timeStart'] < formData['timeEnd']
214 except AssertionError:
215 flash("End time must be after start time", 'warning')
216 return redirect(url_for('admin.eventDisplay', eventId = eventId))
218 try:
219 if formData.get('dateEnd'):
220 assert formData['dateStart'] < formData['dateEnd']
221 except AssertionError:
222 flash("End date must be after start date", 'warning')
223 return redirect(url_for('admin.eventDisplay', eventId = eventId))
226 priorEvent = model_to_dict(Event.get_by_id(eventId))
227 newEventDict = priorEvent.copy()
228 newEventDict.pop('id')
229 newEventDict.update({
230 'program': int(priorEvent['program']['id']),
231 'term': int(priorEvent['term']['id']),
232 'timeStart': formData['timeStart'],
233 'timeEnd': formData['timeEnd'],
234 'location': formData['location'],
235 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
236 'isRepeating': bool(priorEvent['isRepeating']),
237 'seriesId': priorEvent['seriesId'],
238 })
239 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
240 if message:
241 flash(message, "danger")
242 return redirect(url_for('admin.eventDisplay', eventId = eventId))
244 copyRsvpToNewEvent(priorEvent, newEvent[0])
245 createActivityLog(f"Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.")
246 flash("Event successfully renewed.", "success")
247 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
250 except Exception as e:
251 print("Error while trying to renew event:", e)
252 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
253 return redirect(url_for('admin.eventDisplay', eventId = eventId))
257@admin_bp.route('/event/<eventId>/view', methods=['GET'])
258@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
259def eventDisplay(eventId):
260 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
261 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
262 viewer = g.current_user
263 event = Event.get_by_id(eventId)
264 addEventView(viewer,event)
265 # Validate given URL
266 try:
267 event = Event.get_by_id(eventId)
268 invitedCohorts = list(EventCohort.select().where(
269 EventCohort.event == event
270 ))
271 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
272 except DoesNotExist as e:
273 print(f"Unknown event: {eventId}")
274 abort(404)
276 notPermitted = not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerForEvent(event))
277 if 'edit' in request.url_rule.rule and notPermitted:
278 abort(403)
280 eventData = model_to_dict(event, recurse=False)
281 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
282 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
284 image = None
285 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
286 for attachment in associatedAttachments:
287 for extension in picurestype:
288 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
289 image = filepaths[attachment.fileName][0]
290 if image:
291 break
294 if request.method == "POST": # Attempt to save form
295 eventData = request.form.copy()
296 try:
297 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
299 except Exception as e:
300 print("Error saving event:", e)
301 savedEvents = False
302 validationErrorMessage = "Unknown Error Saving Event. Please try again"
305 if savedEvents:
306 rsvpCohorts = request.form.getlist("cohorts[]")
307 updateEventCohorts(savedEvents[0], rsvpCohorts)
308 flash("Event successfully updated!", "success")
309 return redirect(url_for("admin.eventDisplay", eventId = event.id))
310 else:
311 flash(validationErrorMessage, 'warning')
313 # make sure our data is the same regardless of GET and POST
314 preprocessEventData(eventData)
315 eventData['program'] = event.program
316 futureTerms = selectSurroundingTerms(g.current_term)
317 userHasRSVPed = checkUserRsvp(g.current_user, event)
318 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
319 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
320 requirements, bonnerCohorts = [], []
322 if eventData['program'] and eventData['program'].isBonnerScholars:
323 requirements = getCertRequirements(Certification.BONNER)
324 rawBonnerCohorts = getBonnerCohorts(limit=5)
325 bonnerCohorts = {}
327 for year, cohort in rawBonnerCohorts.items():
328 if cohort:
329 bonnerCohorts[year] = cohort
331 invitedCohorts = list(EventCohort.select().where(
332 EventCohort.event_id == eventId,
333 ))
334 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
335 else:
336 requirements, bonnerCohorts, invitedYears = [], [], []
338 rule = request.url_rule
340 # Event Edit
341 if 'edit' in rule.rule:
342 return render_template("events/createEvent.html",
343 eventData = eventData,
344 futureTerms = futureTerms,
345 event = event,
346 requirements = requirements,
347 bonnerCohorts = bonnerCohorts,
348 invitedYears = invitedYears,
349 userHasRSVPed = userHasRSVPed,
350 isProgramManager = isProgramManager,
351 filepaths = filepaths)
352 # Event View
353 else:
354 # get text representations of dates for html
355 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
356 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
357 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
358 eventCountdown = getCountdownToEvent(event)
361 # Identify the next event in a repeating series
362 if event.seriesId:
363 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId)
364 .where((Event.isCanceled == False) | (Event.id == event.id))
365 .order_by(Event.startDate))
366 eventIndex = eventSeriesList.index(event)
367 if len(eventSeriesList) != (eventIndex + 1):
368 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1]
370 currentEventRsvpAmount = getEventRsvpCount(event.id)
372 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
376 return render_template("events/eventView.html",
377 eventData=eventData,
378 event=event,
379 userHasRSVPed=userHasRSVPed,
380 programTrainings=userParticipatedTrainingEvents,
381 currentEventRsvpAmount=currentEventRsvpAmount,
382 isProgramManager=isProgramManager,
383 filepaths=filepaths,
384 image=image,
385 pageViewsCount=pageViewsCount,
386 invitedYears=invitedYears,
387 eventCountdown=eventCountdown
388 )
392@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
393def cancelRoute(eventId):
394 if g.current_user.isAdmin:
395 try:
396 cancelEvent(eventId)
397 return redirect(request.referrer)
399 except Exception as e:
400 print('Error while canceling event:', e)
401 return "", 500
403 else:
404 abort(403)
406@admin_bp.route('/profile/undo', methods=['GET'])
407def undoBackgroundCheck():
408 try:
409 username = g.current_user
410 bgCheckId = session['lastDeletedBgCheck']
411 BackgroundCheck.update({BackgroundCheck.deletionDate: None, BackgroundCheck.deletedBy: None}).where(BackgroundCheck.id == bgCheckId).execute()
412 flash("Background Check has been successfully restored.", "success")
413 return redirect (f"/profile/{username}?accordion=background")
414 except Exception as e:
415 print('Error while undoing background check:', e)
416 return "", 500
418@admin_bp.route('/event/undo', methods=['GET'])
419def undoEvent():
420 try:
421 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted
422 for eventId in eventIds:
423 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
424 event = Event.get_or_none(Event.id == eventId)
425 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id))
426 if event.isRepeating:
427 nameCounter = 1
428 for repeatingEvent in repeatingEvents:
429 newEventNameList = repeatingEvent.name.split()
430 newEventNameList[-1] = f"{nameCounter}"
431 newEventNameList = " ".join(newEventNameList)
432 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute()
433 nameCounter += 1
434 flash("Event has been successfully restored.", "success")
435 return redirect(url_for("main.events", selectedTerm=g.current_term))
436 except Exception as e:
437 print('Error while canceling event:', e)
438 return "", 500
440@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
441def deleteRoute(eventId):
442 try:
443 deleteEvent(eventId)
444 session['lastDeletedEvent'] = [eventId]
445 flash("Event successfully deleted.", "success")
446 return redirect(url_for("main.events", selectedTerm=g.current_term))
448 except Exception as e:
449 print('Error while canceling event:', e)
450 return "", 500
452@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
453def deleteEventAndAllFollowingRoute(eventId):
454 try:
455 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
456 flash("Events successfully deleted.", "success")
457 return redirect(url_for("main.events", selectedTerm=g.current_term))
459 except Exception as e:
460 print('Error while canceling event:', e)
461 return "", 500
463@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST'])
464def deleteAllEventsInSeriesRoute(eventId):
465 try:
466 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId)
467 flash("Events successfully deleted.", "success")
468 return redirect(url_for("main.events", selectedTerm=g.current_term))
470 except Exception as e:
471 print('Error while canceling event:', e)
472 return "", 500
474@admin_bp.route('/makeRepeatingEvents', methods=['POST'])
475def addRepeatingEvents():
476 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy()))
477 return json.dumps(repeatingEvents, default=str)
480@admin_bp.route('/userProfile', methods=['POST'])
481def userProfile():
482 volunteerName= request.form.copy()
483 if volunteerName['searchStudentsInput']:
484 username = volunteerName['searchStudentsInput'].strip("()")
485 user=username.split('(')[-1]
486 return redirect(url_for('main.viewUsersProfile', username=user))
487 else:
488 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
489 return redirect(url_for('admin.studentSearchPage'))
491@admin_bp.route('/search_student', methods=['GET'])
492def studentSearchPage():
493 if g.current_user.isAdmin:
494 return render_template("/admin/searchStudentPage.html")
495 abort(403)
497@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
498def activityLogs():
499 if g.current_user.isCeltsAdmin:
500 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
501 return render_template("/admin/activityLogs.html",
502 allLogs = allLogs)
503 else:
504 abort(403)
506@admin_bp.route("/deleteEventFile", methods=["POST"])
507def deleteEventFile():
508 fileData= request.form
509 eventfile=FileHandler(eventId=fileData["databaseId"])
510 eventfile.deleteFile(fileData["fileId"])
511 return ""
513@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
514def addCourseFile():
515 fileData = request.files['addCourseParticipants']
516 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
517 fileData.save(filePath)
518 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
519 os.remove(filePath)
520 return redirect(url_for("admin.manageServiceLearningCourses"))
522@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
523@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
524def manageServiceLearningCourses(term=None):
526 """
527 The SLC management page for admins
528 """
529 if not g.current_user.isCeltsAdmin:
530 abort(403)
532 if request.method == 'POST' and "submitParticipant" in request.form:
533 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
534 flash('Courses and participants saved successfully!', 'success')
535 return redirect(url_for('admin.manageServiceLearningCourses'))
537 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
539 setRedirectTarget(request.full_path)
540 # retrieve and store the courseID of the imported course from a session variable if it exists.
541 # This allows us to export the courseID in the html and use it.
542 courseID = session.get("alterCourseId")
544 if courseID:
545 # delete courseID from the session if it was retrieved, for storage purposes.
546 session.pop("alterCourseId")
547 return render_template('/admin/manageServiceLearningFaculty.html',
548 courseInstructors = getInstructorCourses(),
549 unapprovedCourses = unapprovedCourses(manageTerm),
550 approvedCourses = approvedCourses(manageTerm),
551 importedCourses = getImportedCourses(manageTerm),
552 terms = selectSurroundingTerms(g.current_term),
553 term = manageTerm,
554 cpPreview = session.get('cpPreview', {}),
555 cpPreviewErrors = session.get('cpErrors', []),
556 courseID = courseID
557 )
559 return render_template('/admin/manageServiceLearningFaculty.html',
560 courseInstructors = getInstructorCourses(),
561 unapprovedCourses = unapprovedCourses(manageTerm),
562 approvedCourses = approvedCourses(manageTerm),
563 importedCourses = getImportedCourses(manageTerm),
564 terms = selectSurroundingTerms(g.current_term),
565 term = manageTerm,
566 cpPreview= session.get('cpPreview',{}),
567 cpPreviewErrors = session.get('cpErrors',[])
568 )
570@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
571def getSidebarInformation() -> str:
572 """
573 Get the count of unapproved courses and students interested in the minor for the current term
574 to display in the admin sidebar. It must be returned as a string to be received by the
575 ajax request.
576 """
577 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
578 interestedStudentsCount: int = len(getMinorInterest())
579 return {"unapprovedCoursesCount": unapprovedCoursesCount,
580 "interestedStudentsCount": interestedStudentsCount}
582@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
583def removeFromSession():
584 try:
585 session.pop('cpPreview')
586 except KeyError:
587 pass
589 return ""
591@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
592def alterImportedCourse(courseID):
593 """
594 This route handles a GET and a POST request for the purpose of imported courses.
595 The GET request provides preexisting information of an imported course in a modal.
596 The POST request updates a specific imported course (course name, course abbreviation,
597 hours earned on completion, list of instructors) in the database with new information
598 coming from the imported courses modal.
599 """
600 if request.method == 'GET':
601 try:
602 targetCourse = Course.get_by_id(courseID)
603 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
605 try:
606 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
607 except IndexError: # If a course has no participant, IndexError will be raised
608 serviceHours = 20
610 courseData = model_to_dict(targetCourse, recurse=False)
611 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
612 courseData['hoursEarned'] = serviceHours
614 return jsonify(courseData)
616 except DoesNotExist:
617 flash("Course not found")
618 return jsonify({"error": "Course not found"}), 404
620 if request.method == 'POST':
621 # Update course information in the database
622 courseData = request.form.copy()
623 editImportedCourses(courseData)
624 session['alterCourseId'] = courseID
626 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
629@admin_bp.route("/manageBonner")
630def manageBonner():
631 if not g.current_user.isCeltsAdmin:
632 abort(403)
634 return render_template("/admin/bonnerManagement.html",
635 cohorts=getBonnerCohorts(),
636 events=getBonnerEvents(g.current_term),
637 requirements=getCertRequirements(certification=Certification.BONNER))
639@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
640def updatecohort(year, method, username):
641 if not g.current_user.isCeltsAdmin:
642 abort(403)
644 try:
645 user = User.get_by_id(username)
646 except:
647 abort(500)
649 if method == "add":
650 try:
651 BonnerCohort.create(year=year, user=user)
652 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
653 except IntegrityError as e:
654 # if they already exist, ignore the error
655 flash(f'Error: {user.fullName} already added.', "danger")
656 pass
658 elif method == "remove":
659 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
660 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
661 else:
662 flash(f"Error: {user.fullName} can't be added.", "danger")
663 abort(500)
665 return ""
667@admin_bp.route("/bonnerxls")
668def bonnerxls():
669 if not g.current_user.isCeltsAdmin:
670 abort(403)
672 newfile = makeBonnerXls()
673 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
675@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
676def saveRequirements(certid):
677 if not g.current_user.isCeltsAdmin:
678 abort(403)
680 newRequirements = updateCertRequirements(certid, request.get_json())
682 return jsonify([requirement.id for requirement in newRequirements])
685@admin_bp.route("/displayEventFile", methods=["POST"])
686def displayEventFile():
687 fileData = request.form
688 eventfile = FileHandler(eventId=fileData["id"])
689 isChecked = fileData.get('checked') == 'true'
690 eventfile.changeDisplay(fileData['id'], isChecked)
691 return ""