Coverage for app/controllers/admin/routes.py: 22%
464 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-04-29 19:36 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-04-29 19:36 +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, getTargetList
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 # Existing RSVP-specific log entries
199 event_logs = list(EventRsvpLog.select(EventRsvpLog, User)
200 .join(User, on=(EventRsvpLog.createdBy == User.username))
201 .where(EventRsvpLog.event_id == eventId))
203 # Include invited users from EventRsvp so the log display reflects invitations too
204 invited_rsvps = EventRsvp.select(EventRsvp, User).join(User).where(EventRsvp.event == eventId)
206 from collections import namedtuple
207 LogEntry = namedtuple('LogEntry', ['createdOn', 'createdBy', 'rsvpLogContent'])
209 allLogs = []
210 allLogs.extend(event_logs)
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 invited_rsvps:
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 ""