Coverage for app/controllers/admin/routes.py: 22%
432 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-01-29 20:22 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2025-01-29 20:22 +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.program import Program
11from app.models.event import Event
12from app.models.eventRsvp import EventRsvp
13from app.models.eventParticipant import EventParticipant
14from app.models.user import User
15from app.models.course import Course
16from app.models.courseInstructor import CourseInstructor
17from app.models.courseParticipant import CourseParticipant
18from app.models.eventTemplate import EventTemplate
19from app.models.activityLog import ActivityLog
20from app.models.eventRsvpLog import EventRsvpLog
21from app.models.attachmentUpload import AttachmentUpload
22from app.models.bonnerCohort import BonnerCohort
23from app.models.certification import Certification
24from app.models.user import User
25from app.models.term import Term
26from app.models.eventViews import EventView
27from app.models.courseStatus import CourseStatus
29from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates
30from app.logic.createLogs import createActivityLog
31from app.logic.certification import getCertRequirements, updateCertRequirements
32from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget
33from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRepeatingEventsData, deleteEventAndAllFollowing, deleteAllEventsInSeries, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewSeriesId
34from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp
35from app.logic.minor import getMinorInterest
36from app.logic.fileHandler import FileHandler
37from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog
38from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses
40from app.controllers.admin import admin_bp
41from app.logic.spreadsheet import createSpreadsheet
44@admin_bp.route('/admin/reports')
45def reports():
46 academicYears = Term.select(Term.academicYear).distinct().order_by(Term.academicYear.desc())
47 academicYears = list(map(lambda t: t.academicYear, academicYears))
48 return render_template("/admin/reports.html", academicYears=academicYears)
50@admin_bp.route('/admin/reports/download', methods=['POST'])
51def downloadFile():
52 academicYear = request.form.get('academicYear')
53 filepath = os.path.abspath(createSpreadsheet(academicYear))
54 return send_file(filepath, as_attachment=True)
58@admin_bp.route('/switch_user', methods=['POST'])
59def switchUser():
60 if app.env == "production":
61 print(f"An attempt was made to switch to another user by {g.current_user.username}!")
62 abort(403)
64 print(f"Switching user from {g.current_user} to",request.form['newuser'])
65 session['current_user'] = model_to_dict(User.get_by_id(request.form['newuser']))
67 return redirect(request.referrer)
70@admin_bp.route('/eventTemplates')
71def templateSelect():
72 if g.current_user.isCeltsAdmin or g.current_user.isCeltsStudentStaff:
73 allprograms = getAllowedPrograms(g.current_user)
74 visibleTemplates = getAllowedTemplates(g.current_user)
75 return render_template("/events/templateSelector.html",
76 programs=allprograms,
77 celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored),
78 templates=visibleTemplates)
79 else:
80 abort(403)
83@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
84def createEvent(templateid, programid):
85 if not (g.current_user.isAdmin 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)
118 if eventData.get('isSeries'):
119 eventData['seriesData'] = json.loads(eventData['seriesData'])
120 succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request))
121 if not succeeded:
122 for index, validationErrorMessage in failedSavedOfferings:
123 eventData['seriesData'][index]['isDuplicate'] = True
124 validationErrorMessage = failedSavedOfferings[-1][1] # The last validation error message from the list of offerings if there are multiple
125 print(f"Failed to save offerings {failedSavedOfferings}")
126 else:
127 try:
128 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
129 except Exception as e:
130 print("Failed saving regular event", e)
131 validationErrorMessage = "Failed to save event."
133 if savedEvents:
134 rsvpcohorts = request.form.getlist("cohorts[]")
135 for year in rsvpcohorts:
136 rsvpForBonnerCohort(int(year), savedEvents[0].id)
137 addBonnerCohortToRsvpLog(int(year), savedEvents[0].id)
140 noun = ((eventData.get('isSeries')) and "Events" or "Event") # pluralize
141 flash(f"{noun} successfully created!", 'success')
144 if program:
145 if len(savedEvents) > 1 and eventData.get('isRepeating'):
146 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')}.")
147 elif len(savedEvents) >= 1 and eventData.get('isSeries'):
148 eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents]
149 eventList = ', '.join(f"<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>" for event in savedEvents)
151 if len(savedEvents) > 1:
152 #creates list of events created in a multiple series to display in the logs
153 eventList = ', '.join(eventList.split(', ')[:-1]) + f', and ' + eventList.split(', ')[-1]
154 #get last date and stick at the end after 'and' so that it reads like a sentence in admin log
155 lastEventDate = eventDates[-1]
156 eventDates = ', '.join(eventDates[:-1]) + f', and {lastEventDate}'
158 createActivityLog(f"Created series {eventList} for {program.programName}, with start dates of {eventDates}.")
160 else:
161 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')}.")
162 else:
163 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')}.")
165 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id))
166 else:
167 flash(validationErrorMessage, 'warning')
169 # make sure our data is the same regardless of GET or POST
170 preprocessEventData(eventData)
171 isProgramManager = g.current_user.isProgramManagerFor(programid)
173 futureTerms = selectSurroundingTerms(g.current_term, prevTerms=0)
175 requirements, bonnerCohorts = [], []
176 if eventData['program'] is not None and eventData['program'].isBonnerScholars:
177 requirements = getCertRequirements(Certification.BONNER)
178 bonnerCohorts = getBonnerCohorts(limit=5)
179 return render_template(f"/events/{template.templateFile}",
180 template = template,
181 eventData = eventData,
182 futureTerms = futureTerms,
183 requirements = requirements,
184 bonnerCohorts = bonnerCohorts,
185 isProgramManager = isProgramManager)
188@admin_bp.route('/event/<eventId>/rsvp', methods=['GET'])
189def rsvpLogDisplay(eventId):
190 event = Event.get_by_id(eventId)
191 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)):
192 allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc())
193 return render_template("/events/rsvpLog.html",
194 event = event,
195 allLogs = allLogs)
196 else:
197 abort(403)
199@admin_bp.route('/event/<eventId>/renew', methods=['POST'])
200def renewEvent(eventId):
201 try:
202 formData = request.form
203 try:
204 assert formData['timeStart'] < formData['timeEnd']
205 except AssertionError:
206 flash("End time must be after start time", 'warning')
207 return redirect(url_for('admin.eventDisplay', eventId = eventId))
209 try:
210 if formData.get('dateEnd'):
211 assert formData['dateStart'] < formData['dateEnd']
212 except AssertionError:
213 flash("End date must be after start date", 'warning')
214 return redirect(url_for('admin.eventDisplay', eventId = eventId))
217 priorEvent = model_to_dict(Event.get_by_id(eventId))
218 newEventDict = priorEvent.copy()
219 newEventDict.pop('id')
220 newEventDict.update({
221 'program': int(priorEvent['program']['id']),
222 'term': int(priorEvent['term']['id']),
223 'timeStart': formData['timeStart'],
224 'timeEnd': formData['timeEnd'],
225 'location': formData['location'],
226 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
227 'isRepeating': bool(priorEvent['isRepeating']),
228 'seriesId': priorEvent['seriesId'],
229 })
230 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
231 if message:
232 flash(message, "danger")
233 return redirect(url_for('admin.eventDisplay', eventId = eventId))
235 copyRsvpToNewEvent(priorEvent, newEvent[0])
236 createActivityLog(f"Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.")
237 flash("Event successfully renewed.", "success")
238 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
241 except Exception as e:
242 print("Error while trying to renew event:", e)
243 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
244 return redirect(url_for('admin.eventDisplay', eventId = eventId))
248@admin_bp.route('/event/<eventId>/view', methods=['GET'])
249@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
250def eventDisplay(eventId):
251 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
252 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
253 viewer = g.current_user
254 event = Event.get_by_id(eventId)
255 addEventView(viewer,event)
256 # Validate given URL
257 try:
258 event = Event.get_by_id(eventId)
259 except DoesNotExist as e:
260 print(f"Unknown event: {eventId}")
261 abort(404)
263 notPermitted = not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerForEvent(event))
264 if 'edit' in request.url_rule.rule and notPermitted:
265 abort(403)
267 eventData = model_to_dict(event, recurse=False)
268 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
269 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
271 image = None
272 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
273 for attachment in associatedAttachments:
274 for extension in picurestype:
275 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
276 image = filepaths[attachment.fileName][0]
277 if image:
278 break
281 if request.method == "POST": # Attempt to save form
282 eventData = request.form.copy()
283 try:
284 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
286 except Exception as e:
287 print("Error saving event:", e)
288 savedEvents = False
289 validationErrorMessage = "Unknown Error Saving Event. Please try again"
292 if savedEvents:
293 rsvpcohorts = request.form.getlist("cohorts[]")
294 for year in rsvpcohorts:
295 rsvpForBonnerCohort(int(year), event.id)
296 addBonnerCohortToRsvpLog(int(year), event.id)
298 flash("Event successfully updated!", "success")
299 return redirect(url_for("admin.eventDisplay", eventId = event.id))
300 else:
301 flash(validationErrorMessage, 'warning')
303 # make sure our data is the same regardless of GET and POST
304 preprocessEventData(eventData)
305 eventData['program'] = event.program
306 futureTerms = selectSurroundingTerms(g.current_term)
307 userHasRSVPed = checkUserRsvp(g.current_user, event)
308 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
309 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
310 requirements, bonnerCohorts = [], []
312 if eventData['program'] and eventData['program'].isBonnerScholars:
313 requirements = getCertRequirements(Certification.BONNER)
314 bonnerCohorts = getBonnerCohorts(limit=5)
316 rule = request.url_rule
318 # Event Edit
319 if 'edit' in rule.rule:
320 return render_template("events/createEvent.html",
321 eventData = eventData,
322 futureTerms=futureTerms,
323 event = event,
324 requirements = requirements,
325 bonnerCohorts = bonnerCohorts,
326 userHasRSVPed = userHasRSVPed,
327 isProgramManager = isProgramManager,
328 filepaths = filepaths)
329 # Event View
330 else:
331 # get text representations of dates for html
332 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
333 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
334 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
335 eventCountdown = getCountdownToEvent(event)
338 # Identify the next event in a repeating series
339 if event.seriesId:
340 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId)
341 .where((Event.isCanceled == False) | (Event.id == event.id))
342 .order_by(Event.startDate))
343 eventIndex = eventSeriesList.index(event)
344 if len(eventSeriesList) != (eventIndex + 1):
345 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1]
347 currentEventRsvpAmount = getEventRsvpCount(event.id)
349 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
351 return render_template("events/eventView.html",
352 eventData=eventData,
353 event=event,
354 userHasRSVPed=userHasRSVPed,
355 programTrainings=userParticipatedTrainingEvents,
356 currentEventRsvpAmount=currentEventRsvpAmount,
357 isProgramManager=isProgramManager,
358 filepaths=filepaths,
359 image=image,
360 pageViewsCount=pageViewsCount,
361 eventCountdown=eventCountdown
362 )
366@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
367def cancelRoute(eventId):
368 if g.current_user.isAdmin:
369 try:
370 cancelEvent(eventId)
371 return redirect(request.referrer)
373 except Exception as e:
374 print('Error while canceling event:', e)
375 return "", 500
377 else:
378 abort(403)
380@admin_bp.route('/event/undo', methods=['GET'])
381def undoEvent():
382 try:
383 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted
384 for eventId in eventIds:
385 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
386 event = Event.get_or_none(Event.id == eventId)
387 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id))
388 if event.isRepeating:
389 nameCounter = 1
390 for repeatingEvent in repeatingEvents:
391 newEventNameList = repeatingEvent.name.split()
392 newEventNameList[-1] = f"{nameCounter}"
393 newEventNameList = " ".join(newEventNameList)
394 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute()
395 nameCounter += 1
396 flash("Deletion successfully undone.", "success")
397 return redirect(url_for("main.events", selectedTerm=g.current_term))
399 except Exception as e:
400 print('Error while canceling event:', e)
401 return "", 500
403@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
404def deleteRoute(eventId):
405 try:
406 deleteEvent(eventId)
407 session['lastDeletedEvent'] = [eventId]
408 flash("Event successfully deleted.", "success")
409 return redirect(url_for("main.events", selectedTerm=g.current_term))
411 except Exception as e:
412 print('Error while canceling event:', e)
413 return "", 500
415@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
416def deleteEventAndAllFollowingRoute(eventId):
417 try:
418 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
419 flash("Events successfully deleted.", "success")
420 return redirect(url_for("main.events", selectedTerm=g.current_term))
422 except Exception as e:
423 print('Error while canceling event:', e)
424 return "", 500
426@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST'])
427def deleteAllEventsInSeriesRoute(eventId):
428 try:
429 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId)
430 flash("Events successfully deleted.", "success")
431 return redirect(url_for("main.events", selectedTerm=g.current_term))
433 except Exception as e:
434 print('Error while canceling event:', e)
435 return "", 500
437@admin_bp.route('/makeRepeatingEvents', methods=['POST'])
438def addRepeatingEvents():
439 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy()))
440 return json.dumps(repeatingEvents, default=str)
443@admin_bp.route('/userProfile', methods=['POST'])
444def userProfile():
445 volunteerName= request.form.copy()
446 if volunteerName['searchStudentsInput']:
447 username = volunteerName['searchStudentsInput'].strip("()")
448 user=username.split('(')[-1]
449 return redirect(url_for('main.viewUsersProfile', username=user))
450 else:
451 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
452 return redirect(url_for('admin.studentSearchPage'))
454@admin_bp.route('/search_student', methods=['GET'])
455def studentSearchPage():
456 if g.current_user.isAdmin:
457 return render_template("/admin/searchStudentPage.html")
458 abort(403)
460@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
461def activityLogs():
462 if g.current_user.isCeltsAdmin:
463 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
464 return render_template("/admin/activityLogs.html",
465 allLogs = allLogs)
466 else:
467 abort(403)
469@admin_bp.route("/deleteEventFile", methods=["POST"])
470def deleteEventFile():
471 fileData= request.form
472 eventfile=FileHandler(eventId=fileData["databaseId"])
473 eventfile.deleteFile(fileData["fileId"])
474 return ""
476@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
477def addCourseFile():
478 fileData = request.files['addCourseParticipants']
479 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
480 fileData.save(filePath)
481 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
482 os.remove(filePath)
483 return redirect(url_for("admin.manageServiceLearningCourses"))
485@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
486@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
487def manageServiceLearningCourses(term=None):
489 """
490 The SLC management page for admins
491 """
492 if not g.current_user.isCeltsAdmin:
493 abort(403)
495 if request.method == 'POST' and "submitParticipant" in request.form:
496 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
497 flash('Courses and participants saved successfully!', 'success')
498 return redirect(url_for('admin.manageServiceLearningCourses'))
500 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
502 setRedirectTarget(request.full_path)
503 # retrieve and store the courseID of the imported course from a session variable if it exists.
504 # This allows us to export the courseID in the html and use it.
505 courseID = session.get("alterCourseId")
507 if courseID:
508 # delete courseID from the session if it was retrieved, for storage purposes.
509 session.pop("alterCourseId")
510 return render_template('/admin/manageServiceLearningFaculty.html',
511 courseInstructors = getInstructorCourses(),
512 unapprovedCourses = unapprovedCourses(manageTerm),
513 approvedCourses = approvedCourses(manageTerm),
514 importedCourses = getImportedCourses(manageTerm),
515 terms = selectSurroundingTerms(g.current_term),
516 term = manageTerm,
517 cpPreview = session.get('cpPreview', {}),
518 cpPreviewErrors = session.get('cpErrors', []),
519 courseID = courseID
520 )
522 return render_template('/admin/manageServiceLearningFaculty.html',
523 courseInstructors = getInstructorCourses(),
524 unapprovedCourses = unapprovedCourses(manageTerm),
525 approvedCourses = approvedCourses(manageTerm),
526 importedCourses = getImportedCourses(manageTerm),
527 terms = selectSurroundingTerms(g.current_term),
528 term = manageTerm,
529 cpPreview= session.get('cpPreview',{}),
530 cpPreviewErrors = session.get('cpErrors',[])
531 )
533@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
534def getSidebarInformation() -> str:
535 """
536 Get the count of unapproved courses and students interested in the minor for the current term
537 to display in the admin sidebar. It must be returned as a string to be received by the
538 ajax request.
539 """
540 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
541 interestedStudentsCount: int = len(getMinorInterest())
542 return {"unapprovedCoursesCount": unapprovedCoursesCount,
543 "interestedStudentsCount": interestedStudentsCount}
545@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
546def removeFromSession():
547 try:
548 session.pop('cpPreview')
549 except KeyError:
550 pass
552 return ""
554@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
555def alterImportedCourse(courseID):
556 """
557 This route handles a GET and a POST request for the purpose of imported courses.
558 The GET request provides preexisting information of an imported course in a modal.
559 The POST request updates a specific imported course (course name, course abbreviation,
560 hours earned on completion, list of instructors) in the database with new information
561 coming from the imported courses modal.
562 """
563 if request.method == 'GET':
564 try:
565 targetCourse = Course.get_by_id(courseID)
566 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
568 try:
569 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
570 except IndexError: # If a course has no participant, IndexError will be raised
571 serviceHours = 20
573 courseData = model_to_dict(targetCourse, recurse=False)
574 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
575 courseData['hoursEarned'] = serviceHours
577 return jsonify(courseData)
579 except DoesNotExist:
580 flash("Course not found")
581 return jsonify({"error": "Course not found"}), 404
583 if request.method == 'POST':
584 # Update course information in the database
585 courseData = request.form.copy()
586 editImportedCourses(courseData)
587 session['alterCourseId'] = courseID
589 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
592@admin_bp.route("/manageBonner")
593def manageBonner():
594 if not g.current_user.isCeltsAdmin:
595 abort(403)
597 return render_template("/admin/bonnerManagement.html",
598 cohorts=getBonnerCohorts(),
599 events=getBonnerEvents(g.current_term),
600 requirements=getCertRequirements(certification=Certification.BONNER))
602@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
603def updatecohort(year, method, username):
604 if not g.current_user.isCeltsAdmin:
605 abort(403)
607 try:
608 user = User.get_by_id(username)
609 except:
610 abort(500)
612 if method == "add":
613 try:
614 BonnerCohort.create(year=year, user=user)
615 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
616 except IntegrityError as e:
617 # if they already exist, ignore the error
618 flash(f'Error: {user.fullName} already added.', "danger")
619 pass
621 elif method == "remove":
622 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
623 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
624 else:
625 flash(f"Error: {user.fullName} can't be added.", "danger")
626 abort(500)
628 return ""
630@admin_bp.route("/bonnerxls")
631def bonnerxls():
632 if not g.current_user.isCeltsAdmin:
633 abort(403)
635 newfile = makeBonnerXls()
636 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
638@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
639def saveRequirements(certid):
640 if not g.current_user.isCeltsAdmin:
641 abort(403)
643 newRequirements = updateCertRequirements(certid, request.get_json())
645 return jsonify([requirement.id for requirement in newRequirements])
648@admin_bp.route("/displayEventFile", methods=["POST"])
649def displayEventFile():
650 fileData = request.form
651 eventfile = FileHandler(eventId=fileData["id"])
652 isChecked = fileData.get('checked') == 'true'
653 eventfile.changeDisplay(fileData['id'], isChecked)
654 return ""