Coverage for app/controllers/admin/routes.py: 22%
483 statements
« prev ^ index » next coverage.py v7.10.2, created at 2026-10-10 20:46 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2026-10-10 20:46 +0000
1from flask import request, render_template, url_for, g, redirect
2from flask import flash, abort, jsonify, session, send_file
3from peewee import DoesNotExist, IntegrityError
4from playhouse.shortcuts import model_to_dict
5import json
6from datetime import datetime
7import os
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
31from app.models.programManager import ProgramManager
33from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates
34from app.logic.createLogs import createActivityLog
35from app.logic.certification import getCertRequirements, updateCertRequirements
36from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget
37from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRepeatingEventsData, deleteEventAndAllFollowing, deleteAllEventsInSeries, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewSeriesId, inviteCohortsToEvent, updateEventCohorts
38from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp, getTargetList
39from app.logic.minor import getMinorInterest
40from app.logic.fileHandler import FileHandler
41from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog
42from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses
44from app.controllers.admin import admin_bp
45from app.logic.volunteerSpreadsheet import createSpreadsheet
48@admin_bp.route('/admin/reports')
49def reports():
50 academicYears = Term.select(Term.academicYear).distinct().order_by(Term.academicYear.desc())
51 academicYears = list(map(lambda t: t.academicYear, academicYears))
52 return render_template("/admin/reports.html", academicYears=academicYears)
54@admin_bp.route('/admin/reports/download', methods=['POST'])
55def downloadFile():
56 academicYear = request.form.get('academicYear')
57 filepath = os.path.abspath(createSpreadsheet(academicYear))
58 return send_file(filepath, as_attachment=True)
62@admin_bp.route('/switch_user', methods=['POST'])
63def switchUser():
64 if app.env == "production":
65 print(f"An attempt was made to switch to another user by {g.current_user.username}!")
66 abort(403)
68 print(f"Switching user from {g.current_user} to",request.form['newuser'])
69 session['current_user'] = model_to_dict(User.get_by_id(request.form['newuser']))
71 return redirect(request.referrer)
74@admin_bp.route('/eventTemplates')
75def templateSelect():
76 programs = getAllowedPrograms(g.current_user)
77 if not programs:
78 abort(403)
79 visibleTemplates = getAllowedTemplates(g.current_user)
81 # Create extravaganza QR code if it doesn't exist
82 imgPath = app.config["files"]["image_path"] + "/extravaganza_qr.png"
83 if not os.path.exists(imgPath):
84 import qrcode
85 img = qrcode.make(request.url.split("/")[-2] + "/extravaganza")
86 img.save(imgPath)
87 return render_template("/events/templateSelector.html",
88 programs=programs,
89 celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored),
90 templates=visibleTemplates)
92@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
93def createEvent(templateid, programid):
94 if not g.current_user.canManageProgram(programid):
95 abort(403)
97 # Validate given URL
98 program = None
99 try:
100 template = EventTemplate.get_by_id(templateid)
101 if programid:
102 program = Program.get_by_id(programid)
103 except DoesNotExist as e:
104 print("Invalid template or program id:", e)
105 flash("There was an error with your selection. Please try again or contact Systems Support.", "danger")
106 return redirect(url_for("admin.program_picker"))
108 # Get the data from the form or from the template
109 eventData = template.templateData
110 eventData['program'] = program
112 if request.method == "GET":
113 eventData['contactName'] = "CELTS Admin"
114 eventData['contactEmail'] = app.config['celts_admin_contact']
115 if program:
116 eventData['location'] = program.defaultLocation
117 if program.contactName:
118 eventData['contactName'] = program.contactName
119 if program.contactEmail:
120 eventData['contactEmail'] = program.contactEmail
122 # Try to save the form
123 if request.method == "POST":
124 savedEvents = None
125 eventData.update(request.form.copy())
126 eventData = preprocessEventData(eventData)
127 if eventData.get('isSeries'):
128 eventData['seriesData'] = json.loads(eventData['seriesData'])
129 succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request))
130 if not succeeded:
131 for index, validationErrorMessage in failedSavedOfferings:
132 eventData['seriesData'][index]['isDuplicate'] = True
133 validationErrorMessage = failedSavedOfferings[-1][1] # The last validation error message from the list of offerings if there are multiple
134 print(f"Failed to save offerings {failedSavedOfferings}")
135 else:
136 try:
137 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
138 except Exception as e:
139 print("Failed saving regular event: ", e)
140 validationErrorMessage = "Failed to save event."
141 if savedEvents:
142 rsvpCohorts = request.form.getlist("cohorts[]")
143 if rsvpCohorts:
144 success, message, invitedCohorts = inviteCohortsToEvent(savedEvents[0], rsvpCohorts)
145 if not success:
146 flash(message, 'warning')
148 noun = ((eventData.get('isSeries')) and "Events" or "Event") # pluralize
149 flash(f"{noun} successfully created!", 'success')
152 if program:
153 if len(savedEvents) > 1 and eventData.get('isRepeating'):
154 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')}.''')
155 elif len(savedEvents) >= 1 and eventData.get('isSeries'):
156 eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents]
157 eventList = ', '.join(f'''<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>''' for event in savedEvents)
159 if len(savedEvents) > 1:
160 #creates list of events created in a multiple series to display in the logs
161 eventList = ', '.join(eventList.split(', ')[:-1]) + f', and ' + eventList.split(', ')[-1]
162 #get last date and stick at the end after 'and' so that it reads like a sentence in admin log
163 lastEventDate = eventDates[-1]
164 eventDates = ', '.join(eventDates[:-1]) + f', and {lastEventDate}'
166 createActivityLog(f"Created series {eventList} for {program.programName}, with start dates of {eventDates}.")
168 else:
169 createActivityLog('''Created event <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a> for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.''')
170 else:
171 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')}.''')
173 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id))
174 else:
175 flash(validationErrorMessage, 'warning')
177 # make sure our data is the same regardless of GET or POST
178 preprocessEventData(eventData)
179 isProgramManager = g.current_user.isProgramManagerFor(programid)
181 requirements, bonnerCohorts = [], []
182 if eventData['program'] is not None and eventData['program'].isBonnerScholars:
183 requirements = getCertRequirements(Certification.BONNER)
184 rawBonnerCohorts = getBonnerCohorts(limit=5)
185 bonnerCohorts = {}
187 for year, cohort in rawBonnerCohorts.items():
188 if cohort:
189 bonnerCohorts[year] = cohort
190 return render_template(f"/events/{template.templateFile}",
191 template = template,
192 eventData = eventData,
193 termList = selectSurroundingTerms(g.current_term, prevTerms=0),
194 requirements = requirements,
195 bonnerCohorts = bonnerCohorts,
196 isProgramManager = isProgramManager)
199@admin_bp.route('/event/<eventId>/rsvp', methods=['GET'])
200def rsvpLogDisplay(eventId):
201 event = Event.get_by_id(eventId)
202 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)):
203 # Existing RSVP-specific log entries
204 eventLogs = list(EventRsvpLog.select(EventRsvpLog, User)
205 .join(User, on=(EventRsvpLog.createdBy == User.username))
206 .where(EventRsvpLog.event_id == eventId))
208 # Include invited users from EventRsvp so the log display reflects invitations too
209 invitedRsvps = EventRsvp.select(EventRsvp, User).join(User).where(EventRsvp.event == eventId)
211 LogEntry = namedtuple('LogEntry', ['createdOn', 'createdBy', 'rsvpLogContent'])
213 allLogs = []
214 allLogs.extend(eventLogs)
216 # Only add invitation logs for non-RSVP events, as for RSVP events, EventRsvp represents RSVPs, not invitations
217 if not event.isRsvpRequired:
218 for rsvp in invitedRsvps:
219 # Provide an explicit invitation action for EventRsvp records
220 allLogs.append(LogEntry(
221 createdOn=rsvp.rsvpTime,
222 createdBy=rsvp.user,
223 rsvpLogContent=f"Added {rsvp.user.fullName} to {getTargetList(event)}"
224 ))
226 allLogs.sort(key=lambda entry: entry.createdOn, reverse=True)
228 return render_template("/events/rsvpLog.html",
229 event = event,
230 allLogs = allLogs)
231 else:
232 abort(403)
234@admin_bp.route('/event/<eventId>/renew', methods=['POST'])
235def renewEvent(eventId):
236 try:
237 formData = request.form
238 try:
239 assert formData['timeStart'] < formData['timeEnd']
240 except AssertionError:
241 flash("End time must be after start time", 'warning')
242 return redirect(url_for('admin.eventDisplay', eventId = eventId))
244 try:
245 if formData.get('dateEnd'):
246 assert formData['dateStart'] < formData['dateEnd']
247 except AssertionError:
248 flash("End date must be after start date", 'warning')
249 return redirect(url_for('admin.eventDisplay', eventId = eventId))
252 priorEvent = model_to_dict(Event.get_by_id(eventId))
253 newEventDict = priorEvent.copy()
254 newEventDict.pop('id')
255 newEventDict.update({
256 'program': int(priorEvent['program']['id']),
257 'term': int(priorEvent['term']['id']),
258 'timeStart': formData['timeStart'],
259 'timeEnd': formData['timeEnd'],
260 'location': formData['location'],
261 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
262 'isRepeating': bool(priorEvent['isRepeating']),
263 'seriesId': priorEvent['seriesId'],
264 })
265 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
266 if message:
267 flash(message, "danger")
268 return redirect(url_for('admin.eventDisplay', eventId = eventId))
270 copyRsvpToNewEvent(priorEvent, newEvent[0])
271 createActivityLog(f'''Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.''')
272 flash("Event successfully renewed.", "success")
273 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
276 except Exception as e:
277 print("Error while trying to renew event:", e)
278 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
279 return redirect(url_for('admin.eventDisplay', eventId = eventId))
283@admin_bp.route('/event/<eventId>/view', methods=['GET'])
284@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
285def eventDisplay(eventId):
286 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
287 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
288 viewer = g.current_user
289 event = Event.get_by_id(eventId)
290 addEventView(viewer,event)
291 # Validate given URL
292 try:
293 event = Event.get_by_id(eventId)
294 invitedCohorts = list(EventCohort.select().where(
295 EventCohort.event == event
296 ))
297 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
298 except DoesNotExist as e:
299 print(f"Unknown event: {eventId}")
300 abort(404)
302 notPermitted = not g.current_user.canManageProgram(event.program)
303 if 'edit' in request.url_rule.rule and notPermitted:
304 abort(403)
306 eventData = model_to_dict(event, recurse=False)
307 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
308 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
310 image = None
311 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
312 for attachment in associatedAttachments:
313 for extension in picurestype:
314 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
315 image = filepaths[attachment.fileName][0]
316 if image:
317 break
320 if request.method == "POST": # Attempt to save form
321 eventData = request.form.copy()
322 try:
323 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
325 except IntegrityError as e:
326 print("Error saving event:", e)
327 savedEvents = False
328 validationErrorMessage = "This combination of settings is not allowed."
329 except Exception as e:
330 print("Error saving event:", e)
331 savedEvents = False
332 validationErrorMessage = "Unknown Error Saving Event. Please try again"
335 if savedEvents:
336 rsvpCohorts = request.form.getlist("cohorts[]")
337 updateEventCohorts(savedEvents[0], rsvpCohorts)
338 flash("Event successfully updated!", "success")
339 return redirect(url_for("admin.eventDisplay", eventId = event.id))
340 else:
341 flash(validationErrorMessage, 'warning')
343 # make sure our data is the same regardless of GET and POST
344 preprocessEventData(eventData)
345 eventData['program'] = event.program
346 userHasRSVPed = checkUserRsvp(g.current_user, event)
347 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
348 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
349 requirements, bonnerCohorts = [], []
351 if eventData['program'] and eventData['program'].isBonnerScholars:
352 requirements = getCertRequirements(Certification.BONNER)
353 rawBonnerCohorts = getBonnerCohorts(limit=5)
354 bonnerCohorts = {}
356 for year, cohort in rawBonnerCohorts.items():
357 if cohort:
358 bonnerCohorts[year] = cohort
360 invitedCohorts = list(EventCohort.select().where(
361 EventCohort.event_id == eventId,
362 ))
363 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
364 else:
365 requirements, bonnerCohorts, invitedYears = [], [], []
367 rule = request.url_rule
369 # Event Edit
370 if 'edit' in rule.rule:
371 return render_template("events/createEvent.html",
372 eventData = eventData,
373 termList = Term.select().order_by(Term.termOrder),
374 event = event,
375 requirements = requirements,
376 bonnerCohorts = bonnerCohorts,
377 invitedYears = invitedYears,
378 userHasRSVPed = userHasRSVPed,
379 isProgramManager = isProgramManager,
380 filepaths = filepaths)
381 # Event View
382 else:
383 # get text representations of dates for html
384 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
385 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
386 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
387 eventCountdown = getCountdownToEvent(event)
390 # Identify the next event in a repeating series
391 if event.seriesId:
392 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId)
393 .where((Event.isCanceled == False) | (Event.id == event.id))
394 .order_by(Event.startDate))
395 eventIndex = eventSeriesList.index(event)
396 if len(eventSeriesList) != (eventIndex + 1):
397 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1]
399 currentEventRsvpAmount = getEventRsvpCount(event.id)
401 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term, includeFutureEvents = False)
403 return render_template("events/eventView.html",
404 eventData=eventData,
405 event=event,
406 userHasRSVPed=userHasRSVPed,
407 programTrainings=userParticipatedTrainingEvents,
408 currentEventRsvpAmount=currentEventRsvpAmount,
409 isProgramManager=isProgramManager,
410 filepaths=filepaths,
411 image=image,
412 pageViewsCount=pageViewsCount,
413 invitedYears=invitedYears,
414 eventCountdown=eventCountdown
415 )
419@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
420def cancelRoute(eventId):
421 if g.current_user.isAdmin:
422 try:
423 cancelEvent(eventId)
424 return redirect(request.referrer)
426 except Exception as e:
427 print('Error while canceling event:', e)
428 return "", 500
430 else:
431 abort(403)
433@admin_bp.route('/profile/undo', methods=['GET'])
434def undoBackgroundCheck():
435 try:
436 username = g.current_user
437 bgCheckId = session['lastDeletedBgCheck']
438 BackgroundCheck.update({BackgroundCheck.deletionDate: None, BackgroundCheck.deletedBy: None}).where(BackgroundCheck.id == bgCheckId).execute()
439 flash("Background Check has been successfully restored.", "success")
440 return redirect (f"/profile/{username}?accordion=background")
441 except Exception as e:
442 print('Error while undoing background check:', e)
443 return "", 500
445@admin_bp.route('/event/undo', methods=['GET'])
446def undoEvent():
447 try:
448 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted
449 for eventId in eventIds:
450 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
451 event = Event.get_or_none(Event.id == eventId)
452 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id))
453 if event.isRepeating:
454 nameCounter = 1
455 for repeatingEvent in repeatingEvents:
456 newEventNameList = repeatingEvent.name.split()
457 newEventNameList[-1] = f"{nameCounter}"
458 newEventNameList = " ".join(newEventNameList)
459 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute()
460 nameCounter += 1
461 flash("Event has been successfully restored.", "success")
462 return redirect(url_for("main.events", selectedTerm=g.current_term))
463 except Exception as e:
464 print('Error while canceling event:', e)
465 return "", 500
467@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
468def deleteRoute(eventId):
469 try:
470 deleteEvent(eventId)
471 session['lastDeletedEvent'] = [eventId]
472 flash("Event successfully deleted.", "success")
473 return redirect(url_for("main.events", selectedTerm=g.current_term))
475 except Exception as e:
476 print('Error while canceling event:', e)
477 return "", 500
479@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
480def deleteEventAndAllFollowingRoute(eventId):
481 try:
482 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
483 flash("Events successfully deleted.", "success")
484 return redirect(url_for("main.events", selectedTerm=g.current_term))
486 except Exception as e:
487 print('Error while canceling event:', e)
488 return "", 500
490@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST'])
491def deleteAllEventsInSeriesRoute(eventId):
492 try:
493 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId)
494 flash("Events successfully deleted.", "success")
495 return redirect(url_for("main.events", selectedTerm=g.current_term))
497 except Exception as e:
498 print('Error while canceling event:', e)
499 return "", 500
501@admin_bp.route('/makeRepeatingEvents', methods=['POST'])
502def addRepeatingEvents():
503 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy()))
504 return json.dumps(repeatingEvents, default=str)
507@admin_bp.route('/userProfile', methods=['POST'])
508def userProfile():
509 volunteerName= request.form.copy()
510 if volunteerName['searchStudentsInput']:
511 username = volunteerName['searchStudentsInput'].strip("()")
512 user=username.split('(')[-1]
513 return redirect(url_for('main.viewUsersProfile', username=user))
514 else:
515 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
516 return redirect(url_for('admin.studentSearchPage'))
518@admin_bp.route('/search_student', methods=['GET'])
519def studentSearchPage():
520 if g.current_user.isAdmin:
521 return render_template("/admin/searchStudentPage.html")
522 abort(403)
524@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
525def activityLogs():
526 if g.current_user.isCeltsAdmin:
527 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
528 return render_template("/admin/activityLogs.html",
529 allLogs = allLogs)
530 else:
531 abort(403)
533@admin_bp.route("/deleteEventFile", methods=["POST"])
534def deleteEventFile():
535 fileData= request.form
536 eventfile=FileHandler(eventId=fileData["databaseId"])
537 eventfile.deleteFile(fileData["fileId"])
538 return ""
540@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
541def addCourseFile():
542 fileData = request.files['addCourseParticipants']
543 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
544 fileData.save(filePath)
545 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
546 os.remove(filePath)
547 return redirect(url_for("admin.manageServiceLearningCourses"))
549@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
550@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
551def manageServiceLearningCourses(term=None):
553 """
554 The SLC management page for admins
555 """
556 if not g.current_user.isCeltsAdmin:
557 abort(403)
559 if request.method == 'POST' and "submitParticipant" in request.form:
560 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
561 flash('Courses and participants saved successfully!', 'success')
562 return redirect(url_for('admin.manageServiceLearningCourses'))
564 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
566 setRedirectTarget(request.full_path)
567 # retrieve and store the courseID of the imported course from a session variable if it exists.
568 # This allows us to export the courseID in the html and use it.
569 courseID = session.get("alterCourseId")
571 if courseID:
572 # delete courseID from the session if it was retrieved, for storage purposes.
573 session.pop("alterCourseId")
574 return render_template('/admin/manageServiceLearningFaculty.html',
575 courseInstructors = getInstructorCourses(),
576 unapprovedCourses = unapprovedCourses(manageTerm),
577 approvedCourses = approvedCourses(manageTerm),
578 importedCourses = getImportedCourses(manageTerm),
579 terms = selectSurroundingTerms(g.current_term),
580 term = manageTerm,
581 cpPreview = session.get('cpPreview', {}),
582 cpPreviewErrors = session.get('cpErrors', []),
583 courseID = courseID
584 )
586 return render_template('/admin/manageServiceLearningFaculty.html',
587 courseInstructors = getInstructorCourses(),
588 unapprovedCourses = unapprovedCourses(manageTerm),
589 approvedCourses = approvedCourses(manageTerm),
590 importedCourses = getImportedCourses(manageTerm),
591 terms = selectSurroundingTerms(g.current_term),
592 term = manageTerm,
593 cpPreview= session.get('cpPreview',{}),
594 cpPreviewErrors = session.get('cpErrors',[])
595 )
597@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
598def getSidebarInformation() -> str:
599 """
600 Get the count of unapproved courses and students interested in the minor for the current term
601 to display in the admin sidebar. It must be returned as a string to be received by the
602 ajax request.
603 """
604 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
605 interestedStudentsCount: int = len(getMinorInterest())
606 return {"unapprovedCoursesCount": unapprovedCoursesCount,
607 "interestedStudentsCount": interestedStudentsCount}
609@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
610def removeFromSession():
611 try:
612 session.pop('cpPreview')
613 except KeyError:
614 pass
616 return ""
618@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
619def alterImportedCourse(courseID):
620 """
621 This route handles a GET and a POST request for the purpose of imported courses.
622 The GET request provides preexisting information of an imported course in a modal.
623 The POST request updates a specific imported course (course name, course abbreviation,
624 hours earned on completion, list of instructors) in the database with new information
625 coming from the imported courses modal.
626 """
627 if request.method == 'GET':
628 try:
629 targetCourse = Course.get_by_id(courseID)
630 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
632 try:
633 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
634 except IndexError: # If a course has no participant, IndexError will be raised
635 serviceHours = 20
637 courseData = model_to_dict(targetCourse, recurse=False)
638 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
639 courseData['hoursEarned'] = serviceHours
641 return jsonify(courseData)
643 except DoesNotExist:
644 flash("Course not found")
645 return jsonify({"error": "Course not found"}), 404
647 if request.method == 'POST':
648 # Update course information in the database
649 courseData = request.form.copy()
650 editImportedCourses(courseData)
651 session['alterCourseId'] = courseID
653 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
656@admin_bp.route("/manageBonner")
657def manageBonner():
658 if not g.current_user.isCeltsAdmin:
659 abort(403)
661 return render_template("/admin/bonnerManagement.html",
662 cohorts=getBonnerCohorts(),
663 events=getBonnerEvents(g.current_term),
664 requirements=getCertRequirements(certification=Certification.BONNER))
666@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
667def updatecohort(year, method, username):
668 if not g.current_user.isCeltsAdmin:
669 abort(403)
671 try:
672 user = User.get_by_id(username)
673 except:
674 abort(500)
676 if method == "add":
677 try:
678 BonnerCohort.create(year=year, user=user)
679 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
680 except IntegrityError as e:
681 # if they already exist, ignore the error
682 flash(f'Error: {user.fullName} already added.', "danger")
683 pass
685 elif method == "remove":
686 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
687 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
688 else:
689 flash(f"Error: {user.fullName} can't be added.", "danger")
690 abort(500)
691 return ""
693@admin_bp.route("/bonnerXls/<startingYear>/<noOfYears>")
694def getBonnerXls(startingYear, noOfYears):
695 if not g.current_user.isCeltsAdmin:
696 abort(403)
697 newfile = makeBonnerXls(startingYear, noOfYears)
698 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
701@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
702def saveRequirements(certid):
703 if not g.current_user.isCeltsAdmin:
704 abort(403)
706 newRequirements = updateCertRequirements(certid, request.get_json())
708 return jsonify([requirement.id for requirement in newRequirements])
711@admin_bp.route("/displayEventFile", methods=["POST"])
712def displayEventFile():
713 fileData = request.form
714 eventfile = FileHandler(eventId=fileData["id"])
715 isChecked = fileData.get('checked') == 'true'
716 eventfile.changeDisplay(fileData['id'], isChecked)
717 return ""
719@admin_bp.route("/handbookSignature", methods=["POST"])
720def handbookSignature():
721 if not (g.current_user.username == request.form.get('studentUsername')):
722 abort(403)
724 signer = g.current_user
725 signer.lastHandbookSignature = datetime.now()
726 signer.signatureTerm = g.current_term
727 signer.save()
729 return ""