Coverage for app/controllers/admin/routes.py: 22%
464 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-06-03 19:34 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-06-03 19:34 +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
8from collections import namedtuple
10from app import app
11from app.models.backgroundCheck import BackgroundCheck
12from app.models.program import Program
13from app.models.event import Event
14from app.models.eventRsvp import EventRsvp
15from app.models.eventParticipant import EventParticipant
16from app.models.user import User
17from app.models.course import Course
18from app.models.courseInstructor import CourseInstructor
19from app.models.courseParticipant import CourseParticipant
20from app.models.eventTemplate import EventTemplate
21from app.models.activityLog import ActivityLog
22from app.models.eventRsvpLog import EventRsvpLog
23from app.models.attachmentUpload import AttachmentUpload
24from app.models.bonnerCohort import BonnerCohort
25from app.models.eventCohort import EventCohort
26from app.models.certification import Certification
27from app.models.user import User
28from app.models.term import Term
29from app.models.eventViews import EventView
30from app.models.courseStatus import CourseStatus
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, getTargetList
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)
79 return render_template("/events/templateSelector.html",
80 programs=programs,
81 celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored),
82 templates=visibleTemplates)
84@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
85def createEvent(templateid, programid):
86 if not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerFor(programid)):
87 abort(403)
89 # Validate given URL
90 program = None
91 try:
92 template = EventTemplate.get_by_id(templateid)
93 if programid:
94 program = Program.get_by_id(programid)
95 except DoesNotExist as e:
96 print("Invalid template or program id:", e)
97 flash("There was an error with your selection. Please try again or contact Systems Support.", "danger")
98 return redirect(url_for("admin.program_picker"))
100 # Get the data from the form or from the template
101 eventData = template.templateData
102 eventData['program'] = program
104 if request.method == "GET":
105 eventData['contactName'] = "CELTS Admin"
106 eventData['contactEmail'] = app.config['celts_admin_contact']
107 if program:
108 eventData['location'] = program.defaultLocation
109 if program.contactName:
110 eventData['contactName'] = program.contactName
111 if program.contactEmail:
112 eventData['contactEmail'] = program.contactEmail
114 # Try to save the form
115 if request.method == "POST":
116 savedEvents = None
117 eventData.update(request.form.copy())
118 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 requirements, bonnerCohorts = [], []
176 if eventData['program'] is not None and eventData['program'].isBonnerScholars:
177 requirements = getCertRequirements(Certification.BONNER)
178 rawBonnerCohorts = getBonnerCohorts(limit=5)
179 bonnerCohorts = {}
181 for year, cohort in rawBonnerCohorts.items():
182 if cohort:
183 bonnerCohorts[year] = cohort
186 return render_template(f"/events/{template.templateFile}",
187 template = template,
188 eventData = eventData,
189 termList = selectSurroundingTerms(g.current_term, prevTerms=0),
190 requirements = requirements,
191 bonnerCohorts = bonnerCohorts,
192 isProgramManager = isProgramManager)
195@admin_bp.route('/event/<eventId>/rsvp', methods=['GET'])
196def rsvpLogDisplay(eventId):
197 event = Event.get_by_id(eventId)
198 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)):
199 # Existing RSVP-specific log entries
200 eventLogs = list(EventRsvpLog.select(EventRsvpLog, User)
201 .join(User, on=(EventRsvpLog.createdBy == User.username))
202 .where(EventRsvpLog.event_id == eventId))
204 # Include invited users from EventRsvp so the log display reflects invitations too
205 invitedRsvps = EventRsvp.select(EventRsvp, User).join(User).where(EventRsvp.event == eventId)
207 LogEntry = namedtuple('LogEntry', ['createdOn', 'createdBy', 'rsvpLogContent'])
209 allLogs = []
210 allLogs.extend(eventLogs)
212 # Only add invitation logs for non-RSVP events, as for RSVP events, EventRsvp represents RSVPs, not invitations
213 if not event.isRsvpRequired:
214 for rsvp in invitedRsvps:
215 # Provide an explicit invitation action for EventRsvp records
216 allLogs.append(LogEntry(
217 createdOn=rsvp.rsvpTime,
218 createdBy=rsvp.user,
219 rsvpLogContent=f"Added {rsvp.user.fullName} to {getTargetList(event)}"
220 ))
222 allLogs.sort(key=lambda entry: entry.createdOn, reverse=True)
224 return render_template("/events/rsvpLog.html",
225 event = event,
226 allLogs = allLogs)
227 else:
228 abort(403)
230@admin_bp.route('/event/<eventId>/renew', methods=['POST'])
231def renewEvent(eventId):
232 try:
233 formData = request.form
234 try:
235 assert formData['timeStart'] < formData['timeEnd']
236 except AssertionError:
237 flash("End time must be after start time", 'warning')
238 return redirect(url_for('admin.eventDisplay', eventId = eventId))
240 try:
241 if formData.get('dateEnd'):
242 assert formData['dateStart'] < formData['dateEnd']
243 except AssertionError:
244 flash("End date must be after start date", 'warning')
245 return redirect(url_for('admin.eventDisplay', eventId = eventId))
248 priorEvent = model_to_dict(Event.get_by_id(eventId))
249 newEventDict = priorEvent.copy()
250 newEventDict.pop('id')
251 newEventDict.update({
252 'program': int(priorEvent['program']['id']),
253 'term': int(priorEvent['term']['id']),
254 'timeStart': formData['timeStart'],
255 'timeEnd': formData['timeEnd'],
256 'location': formData['location'],
257 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
258 'isRepeating': bool(priorEvent['isRepeating']),
259 'seriesId': priorEvent['seriesId'],
260 })
261 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
262 if message:
263 flash(message, "danger")
264 return redirect(url_for('admin.eventDisplay', eventId = eventId))
266 copyRsvpToNewEvent(priorEvent, newEvent[0])
267 createActivityLog(f"Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.")
268 flash("Event successfully renewed.", "success")
269 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
272 except Exception as e:
273 print("Error while trying to renew event:", e)
274 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
275 return redirect(url_for('admin.eventDisplay', eventId = eventId))
279@admin_bp.route('/event/<eventId>/view', methods=['GET'])
280@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
281def eventDisplay(eventId):
282 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
283 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
284 viewer = g.current_user
285 event = Event.get_by_id(eventId)
286 addEventView(viewer,event)
287 # Validate given URL
288 try:
289 event = Event.get_by_id(eventId)
290 invitedCohorts = list(EventCohort.select().where(
291 EventCohort.event == event
292 ))
293 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
294 except DoesNotExist as e:
295 print(f"Unknown event: {eventId}")
296 abort(404)
298 notPermitted = not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerForEvent(event))
299 if 'edit' in request.url_rule.rule and notPermitted:
300 abort(403)
302 eventData = model_to_dict(event, recurse=False)
303 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
304 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
306 image = None
307 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
308 for attachment in associatedAttachments:
309 for extension in picurestype:
310 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
311 image = filepaths[attachment.fileName][0]
312 if image:
313 break
316 if request.method == "POST": # Attempt to save form
317 eventData = request.form.copy()
318 try:
319 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
321 except Exception as e:
322 print("Error saving event:", e)
323 savedEvents = False
324 validationErrorMessage = "Unknown Error Saving Event. Please try again"
327 if savedEvents:
328 rsvpCohorts = request.form.getlist("cohorts[]")
329 updateEventCohorts(savedEvents[0], rsvpCohorts)
330 flash("Event successfully updated!", "success")
331 return redirect(url_for("admin.eventDisplay", eventId = event.id))
332 else:
333 flash(validationErrorMessage, 'warning')
335 # make sure our data is the same regardless of GET and POST
336 preprocessEventData(eventData)
337 eventData['program'] = event.program
338 userHasRSVPed = checkUserRsvp(g.current_user, event)
339 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
340 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
341 requirements, bonnerCohorts = [], []
343 if eventData['program'] and eventData['program'].isBonnerScholars:
344 requirements = getCertRequirements(Certification.BONNER)
345 rawBonnerCohorts = getBonnerCohorts(limit=5)
346 bonnerCohorts = {}
348 for year, cohort in rawBonnerCohorts.items():
349 if cohort:
350 bonnerCohorts[year] = cohort
352 invitedCohorts = list(EventCohort.select().where(
353 EventCohort.event_id == eventId,
354 ))
355 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
356 else:
357 requirements, bonnerCohorts, invitedYears = [], [], []
359 rule = request.url_rule
361 # Event Edit
362 if 'edit' in rule.rule:
363 return render_template("events/createEvent.html",
364 eventData = eventData,
365 termList = Term.select().order_by(Term.termOrder),
366 event = event,
367 requirements = requirements,
368 bonnerCohorts = bonnerCohorts,
369 invitedYears = invitedYears,
370 userHasRSVPed = userHasRSVPed,
371 isProgramManager = isProgramManager,
372 filepaths = filepaths)
373 # Event View
374 else:
375 # get text representations of dates for html
376 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
377 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
378 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
379 eventCountdown = getCountdownToEvent(event)
382 # Identify the next event in a repeating series
383 if event.seriesId:
384 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId)
385 .where((Event.isCanceled == False) | (Event.id == event.id))
386 .order_by(Event.startDate))
387 eventIndex = eventSeriesList.index(event)
388 if len(eventSeriesList) != (eventIndex + 1):
389 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1]
391 currentEventRsvpAmount = getEventRsvpCount(event.id)
393 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
395 return render_template("events/eventView.html",
396 eventData=eventData,
397 event=event,
398 userHasRSVPed=userHasRSVPed,
399 programTrainings=userParticipatedTrainingEvents,
400 currentEventRsvpAmount=currentEventRsvpAmount,
401 isProgramManager=isProgramManager,
402 filepaths=filepaths,
403 image=image,
404 pageViewsCount=pageViewsCount,
405 invitedYears=invitedYears,
406 eventCountdown=eventCountdown
407 )
411@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
412def cancelRoute(eventId):
413 if g.current_user.isAdmin:
414 try:
415 cancelEvent(eventId)
416 return redirect(request.referrer)
418 except Exception as e:
419 print('Error while canceling event:', e)
420 return "", 500
422 else:
423 abort(403)
425@admin_bp.route('/profile/undo', methods=['GET'])
426def undoBackgroundCheck():
427 try:
428 username = g.current_user
429 bgCheckId = session['lastDeletedBgCheck']
430 BackgroundCheck.update({BackgroundCheck.deletionDate: None, BackgroundCheck.deletedBy: None}).where(BackgroundCheck.id == bgCheckId).execute()
431 flash("Background Check has been successfully restored.", "success")
432 return redirect (f"/profile/{username}?accordion=background")
433 except Exception as e:
434 print('Error while undoing background check:', e)
435 return "", 500
437@admin_bp.route('/event/undo', methods=['GET'])
438def undoEvent():
439 try:
440 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted
441 for eventId in eventIds:
442 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
443 event = Event.get_or_none(Event.id == eventId)
444 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id))
445 if event.isRepeating:
446 nameCounter = 1
447 for repeatingEvent in repeatingEvents:
448 newEventNameList = repeatingEvent.name.split()
449 newEventNameList[-1] = f"{nameCounter}"
450 newEventNameList = " ".join(newEventNameList)
451 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute()
452 nameCounter += 1
453 flash("Event has been successfully restored.", "success")
454 return redirect(url_for("main.events", selectedTerm=g.current_term))
455 except Exception as e:
456 print('Error while canceling event:', e)
457 return "", 500
459@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
460def deleteRoute(eventId):
461 try:
462 deleteEvent(eventId)
463 session['lastDeletedEvent'] = [eventId]
464 flash("Event successfully deleted.", "success")
465 return redirect(url_for("main.events", selectedTerm=g.current_term))
467 except Exception as e:
468 print('Error while canceling event:', e)
469 return "", 500
471@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
472def deleteEventAndAllFollowingRoute(eventId):
473 try:
474 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
475 flash("Events successfully deleted.", "success")
476 return redirect(url_for("main.events", selectedTerm=g.current_term))
478 except Exception as e:
479 print('Error while canceling event:', e)
480 return "", 500
482@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST'])
483def deleteAllEventsInSeriesRoute(eventId):
484 try:
485 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId)
486 flash("Events successfully deleted.", "success")
487 return redirect(url_for("main.events", selectedTerm=g.current_term))
489 except Exception as e:
490 print('Error while canceling event:', e)
491 return "", 500
493@admin_bp.route('/makeRepeatingEvents', methods=['POST'])
494def addRepeatingEvents():
495 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy()))
496 return json.dumps(repeatingEvents, default=str)
499@admin_bp.route('/userProfile', methods=['POST'])
500def userProfile():
501 volunteerName= request.form.copy()
502 if volunteerName['searchStudentsInput']:
503 username = volunteerName['searchStudentsInput'].strip("()")
504 user=username.split('(')[-1]
505 return redirect(url_for('main.viewUsersProfile', username=user))
506 else:
507 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
508 return redirect(url_for('admin.studentSearchPage'))
510@admin_bp.route('/search_student', methods=['GET'])
511def studentSearchPage():
512 if g.current_user.isAdmin:
513 return render_template("/admin/searchStudentPage.html")
514 abort(403)
516@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
517def activityLogs():
518 if g.current_user.isCeltsAdmin:
519 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
520 return render_template("/admin/activityLogs.html",
521 allLogs = allLogs)
522 else:
523 abort(403)
525@admin_bp.route("/deleteEventFile", methods=["POST"])
526def deleteEventFile():
527 fileData= request.form
528 eventfile=FileHandler(eventId=fileData["databaseId"])
529 eventfile.deleteFile(fileData["fileId"])
530 return ""
532@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
533def addCourseFile():
534 fileData = request.files['addCourseParticipants']
535 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
536 fileData.save(filePath)
537 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
538 os.remove(filePath)
539 return redirect(url_for("admin.manageServiceLearningCourses"))
541@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
542@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
543def manageServiceLearningCourses(term=None):
545 """
546 The SLC management page for admins
547 """
548 if not g.current_user.isCeltsAdmin:
549 abort(403)
551 if request.method == 'POST' and "submitParticipant" in request.form:
552 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
553 flash('Courses and participants saved successfully!', 'success')
554 return redirect(url_for('admin.manageServiceLearningCourses'))
556 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
558 setRedirectTarget(request.full_path)
559 # retrieve and store the courseID of the imported course from a session variable if it exists.
560 # This allows us to export the courseID in the html and use it.
561 courseID = session.get("alterCourseId")
563 if courseID:
564 # delete courseID from the session if it was retrieved, for storage purposes.
565 session.pop("alterCourseId")
566 return render_template('/admin/manageServiceLearningFaculty.html',
567 courseInstructors = getInstructorCourses(),
568 unapprovedCourses = unapprovedCourses(manageTerm),
569 approvedCourses = approvedCourses(manageTerm),
570 importedCourses = getImportedCourses(manageTerm),
571 terms = selectSurroundingTerms(g.current_term),
572 term = manageTerm,
573 cpPreview = session.get('cpPreview', {}),
574 cpPreviewErrors = session.get('cpErrors', []),
575 courseID = courseID
576 )
578 return render_template('/admin/manageServiceLearningFaculty.html',
579 courseInstructors = getInstructorCourses(),
580 unapprovedCourses = unapprovedCourses(manageTerm),
581 approvedCourses = approvedCourses(manageTerm),
582 importedCourses = getImportedCourses(manageTerm),
583 terms = selectSurroundingTerms(g.current_term),
584 term = manageTerm,
585 cpPreview= session.get('cpPreview',{}),
586 cpPreviewErrors = session.get('cpErrors',[])
587 )
589@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
590def getSidebarInformation() -> str:
591 """
592 Get the count of unapproved courses and students interested in the minor for the current term
593 to display in the admin sidebar. It must be returned as a string to be received by the
594 ajax request.
595 """
596 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
597 interestedStudentsCount: int = len(getMinorInterest())
598 return {"unapprovedCoursesCount": unapprovedCoursesCount,
599 "interestedStudentsCount": interestedStudentsCount}
601@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
602def removeFromSession():
603 try:
604 session.pop('cpPreview')
605 except KeyError:
606 pass
608 return ""
610@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
611def alterImportedCourse(courseID):
612 """
613 This route handles a GET and a POST request for the purpose of imported courses.
614 The GET request provides preexisting information of an imported course in a modal.
615 The POST request updates a specific imported course (course name, course abbreviation,
616 hours earned on completion, list of instructors) in the database with new information
617 coming from the imported courses modal.
618 """
619 if request.method == 'GET':
620 try:
621 targetCourse = Course.get_by_id(courseID)
622 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
624 try:
625 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
626 except IndexError: # If a course has no participant, IndexError will be raised
627 serviceHours = 20
629 courseData = model_to_dict(targetCourse, recurse=False)
630 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
631 courseData['hoursEarned'] = serviceHours
633 return jsonify(courseData)
635 except DoesNotExist:
636 flash("Course not found")
637 return jsonify({"error": "Course not found"}), 404
639 if request.method == 'POST':
640 # Update course information in the database
641 courseData = request.form.copy()
642 editImportedCourses(courseData)
643 session['alterCourseId'] = courseID
645 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
648@admin_bp.route("/manageBonner")
649def manageBonner():
650 if not g.current_user.isCeltsAdmin:
651 abort(403)
653 return render_template("/admin/bonnerManagement.html",
654 cohorts=getBonnerCohorts(),
655 events=getBonnerEvents(g.current_term),
656 requirements=getCertRequirements(certification=Certification.BONNER))
658@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
659def updatecohort(year, method, username):
660 if not g.current_user.isCeltsAdmin:
661 abort(403)
663 try:
664 user = User.get_by_id(username)
665 except:
666 abort(500)
668 if method == "add":
669 try:
670 BonnerCohort.create(year=year, user=user)
671 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
672 except IntegrityError as e:
673 # if they already exist, ignore the error
674 flash(f'Error: {user.fullName} already added.', "danger")
675 pass
677 elif method == "remove":
678 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
679 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
680 else:
681 flash(f"Error: {user.fullName} can't be added.", "danger")
682 abort(500)
683 return ""
685@admin_bp.route("/bonnerXls/<startingYear>/<noOfYears>")
686def getBonnerXls(startingYear, noOfYears):
687 if not g.current_user.isCeltsAdmin:
688 abort(403)
689 newfile = makeBonnerXls(startingYear, noOfYears)
690 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
693@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
694def saveRequirements(certid):
695 if not g.current_user.isCeltsAdmin:
696 abort(403)
698 newRequirements = updateCertRequirements(certid, request.get_json())
700 return jsonify([requirement.id for requirement in newRequirements])
703@admin_bp.route("/displayEventFile", methods=["POST"])
704def displayEventFile():
705 fileData = request.form
706 eventfile = FileHandler(eventId=fileData["id"])
707 isChecked = fileData.get('checked') == 'true'
708 eventfile.changeDisplay(fileData['id'], isChecked)
709 return ""