Coverage for app/controllers/admin/routes.py: 22%
432 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-11-23 03:00 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-11-23 03:00 +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, getRecurringEventsData, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId
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('isMultipleOffering'):
119 eventData['multipleOfferingData'] = json.loads(eventData['multipleOfferingData'])
120 succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request))
121 if not succeeded:
122 for index, validationErrorMessage in failedSavedOfferings:
123 eventData['multipleOfferingData'][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('isRecurring') or eventData.get('isMultipleOffering')) and "Events" or "Event") # pluralize
141 flash(f"{noun} successfully created!", 'success')
144 if program:
145 if len(savedEvents) > 1 and eventData.get('isRecurring'):
146 createActivityLog(f"Created a recurring 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')}. 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('isMultipleOffering'):
149 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 events {eventList} for {program.programName}, with start dates of {eventDates}.")
162 else:
163 createActivityLog(f"Created events <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a> for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.")
164 else:
165 createActivityLog(f"Created a non-program event, <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a>, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.")
167 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id))
168 else:
169 flash(validationErrorMessage, 'warning')
171 # make sure our data is the same regardless of GET or POST
172 preprocessEventData(eventData)
173 isProgramManager = g.current_user.isProgramManagerFor(programid)
175 futureTerms = selectSurroundingTerms(g.current_term, prevTerms=0)
177 requirements, bonnerCohorts = [], []
178 if eventData['program'] is not None and eventData['program'].isBonnerScholars:
179 requirements = getCertRequirements(Certification.BONNER)
180 bonnerCohorts = getBonnerCohorts(limit=5)
181 return render_template(f"/events/{template.templateFile}",
182 template = template,
183 eventData = eventData,
184 futureTerms = futureTerms,
185 requirements = requirements,
186 bonnerCohorts = bonnerCohorts,
187 isProgramManager = isProgramManager)
190@admin_bp.route('/event/<eventId>/rsvp', methods=['GET'])
191def rsvpLogDisplay(eventId):
192 event = Event.get_by_id(eventId)
193 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)):
194 allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc())
195 return render_template("/events/rsvpLog.html",
196 event = event,
197 allLogs = allLogs)
198 else:
199 abort(403)
201@admin_bp.route('/event/<eventId>/renew', methods=['POST'])
202def renewEvent(eventId):
203 try:
204 formData = request.form
205 try:
206 assert formData['timeStart'] < formData['timeEnd']
207 except AssertionError:
208 flash("End time must be after start time", 'warning')
209 return redirect(url_for('admin.eventDisplay', eventId = eventId))
211 try:
212 if formData.get('dateEnd'):
213 assert formData['dateStart'] < formData['dateEnd']
214 except AssertionError:
215 flash("End date must be after start date", 'warning')
216 return redirect(url_for('admin.eventDisplay', eventId = eventId))
219 priorEvent = model_to_dict(Event.get_by_id(eventId))
220 newEventDict = priorEvent.copy()
221 newEventDict.pop('id')
222 newEventDict.update({
223 'program': int(priorEvent['program']['id']),
224 'term': int(priorEvent['term']['id']),
225 'timeStart': formData['timeStart'],
226 'timeEnd': formData['timeEnd'],
227 'location': formData['location'],
228 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
229 'isRecurring': bool(priorEvent['recurringId']),
230 'isMultipleOffering': bool(priorEvent['multipleOfferingId']),
231 })
232 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
233 if message:
234 flash(message, "danger")
235 return redirect(url_for('admin.eventDisplay', eventId = eventId))
237 copyRsvpToNewEvent(priorEvent, newEvent[0])
238 createActivityLog(f"Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.")
239 flash("Event successfully renewed.", "success")
240 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
243 except Exception as e:
244 print("Error while trying to renew event:", e)
245 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
246 return redirect(url_for('admin.eventDisplay', eventId = eventId))
250@admin_bp.route('/event/<eventId>/view', methods=['GET'])
251@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
252def eventDisplay(eventId):
253 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
254 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
255 viewer = g.current_user
256 event = Event.get_by_id(eventId)
257 addEventView(viewer,event)
258 # Validate given URL
259 try:
260 event = Event.get_by_id(eventId)
261 except DoesNotExist as e:
262 print(f"Unknown event: {eventId}")
263 abort(404)
265 notPermitted = not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerForEvent(event))
266 if 'edit' in request.url_rule.rule and notPermitted:
267 abort(403)
269 eventData = model_to_dict(event, recurse=False)
270 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
271 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
273 image = None
274 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
275 for attachment in associatedAttachments:
276 for extension in picurestype:
277 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
278 image = filepaths[attachment.fileName][0]
279 if image:
280 break
283 if request.method == "POST": # Attempt to save form
284 eventData = request.form.copy()
285 try:
286 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
288 except Exception as e:
289 print("Error saving event:", e)
290 savedEvents = False
291 validationErrorMessage = "Unknown Error Saving Event. Please try again"
294 if savedEvents:
295 rsvpcohorts = request.form.getlist("cohorts[]")
296 for year in rsvpcohorts:
297 rsvpForBonnerCohort(int(year), event.id)
298 addBonnerCohortToRsvpLog(int(year), event.id)
300 flash("Event successfully updated!", "success")
301 return redirect(url_for("admin.eventDisplay", eventId = event.id))
302 else:
303 flash(validationErrorMessage, 'warning')
305 # make sure our data is the same regardless of GET and POST
306 preprocessEventData(eventData)
307 eventData['program'] = event.program
308 futureTerms = selectSurroundingTerms(g.current_term)
309 userHasRSVPed = checkUserRsvp(g.current_user, event)
310 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
311 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
312 requirements, bonnerCohorts = [], []
314 if eventData['program'] and eventData['program'].isBonnerScholars:
315 requirements = getCertRequirements(Certification.BONNER)
316 bonnerCohorts = getBonnerCohorts(limit=5)
318 rule = request.url_rule
320 # Event Edit
321 if 'edit' in rule.rule:
322 return render_template("events/createEvent.html",
323 eventData = eventData,
324 futureTerms=futureTerms,
325 event = event,
326 requirements = requirements,
327 bonnerCohorts = bonnerCohorts,
328 userHasRSVPed = userHasRSVPed,
329 isProgramManager = isProgramManager,
330 filepaths = filepaths)
331 # Event View
332 else:
333 # get text representations of dates for html
334 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
335 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
336 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
337 eventCountdown = getCountdownToEvent(event)
340 # Identify the next event in a recurring series
341 if event.recurringId:
342 eventSeriesList = list(Event.select().where(Event.recurringId == event.recurringId)
343 .where((Event.isCanceled == False) | (Event.id == event.id))
344 .order_by(Event.startDate))
345 eventIndex = eventSeriesList.index(event)
346 if len(eventSeriesList) != (eventIndex + 1):
347 eventData["nextRecurringEvent"] = eventSeriesList[eventIndex + 1]
349 currentEventRsvpAmount = getEventRsvpCount(event.id)
351 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
353 return render_template("events/eventView.html",
354 eventData=eventData,
355 event=event,
356 userHasRSVPed=userHasRSVPed,
357 programTrainings=userParticipatedTrainingEvents,
358 currentEventRsvpAmount=currentEventRsvpAmount,
359 isProgramManager=isProgramManager,
360 filepaths=filepaths,
361 image=image,
362 pageViewsCount=pageViewsCount,
363 eventCountdown=eventCountdown
364 )
368@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
369def cancelRoute(eventId):
370 if g.current_user.isAdmin:
371 try:
372 cancelEvent(eventId)
373 return redirect(request.referrer)
375 except Exception as e:
376 print('Error while canceling event:', e)
377 return "", 500
379 else:
380 abort(403)
382@admin_bp.route('/event/undo', methods=['GET'])
383def undoEvent():
384 try:
385 events = session['lastDeletedEvent']
386 for eventId in events:
387 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
388 event = Event.get_or_none(Event.id == eventId)
389 recurringEvents = list(Event.select().where((Event.recurringId==event.recurringId) & (Event.deletionDate == None)).order_by(Event.id))
390 if event.recurringId is not None:
391 nameCounter = 1
392 for recurringEvent in recurringEvents:
393 newEventNameList = recurringEvent.name.split()
394 newEventNameList[-1] = f"{nameCounter}"
395 newEventNameList = " ".join(newEventNameList)
396 Event.update({Event.name: newEventNameList}).where(Event.id==recurringEvent.id).execute()
397 nameCounter += 1
398 flash("Deletion successfully undone.", "success")
399 return redirect('/eventsList/' + str(g.current_term))
400 except Exception as e:
401 print('Error while canceling event:', e)
402 return "", 500
404@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
405def deleteRoute(eventId):
406 try:
407 deleteEvent(eventId)
408 session['lastDeletedEvent'] = [eventId]
409 flash("Event successfully deleted.", "success")
410 return redirect(url_for("main.events", selectedTerm=g.current_term))
412 except Exception as e:
413 print('Error while canceling event:', e)
414 return "", 500
416@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
417def deleteEventAndAllFollowingRoute(eventId):
418 try:
419 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
420 flash("Events successfully deleted.", "success")
421 return redirect(url_for("main.events", selectedTerm=g.current_term))
423 except Exception as e:
424 print('Error while canceling event:', e)
425 return "", 500
427@admin_bp.route('/event/<eventId>/deleteAllRecurring', methods=['POST'])
428def deleteAllRecurringEventsRoute(eventId):
429 try:
430 session["lastDeletedEvent"] = deleteAllRecurringEvents(eventId)
431 flash("Events successfully deleted.", "success")
432 return redirect(url_for("main.events", selectedTerm=g.current_term))
434 except Exception as e:
435 print('Error while canceling event:', e)
436 return "", 500
438@admin_bp.route('/makeRecurringEvents', methods=['POST'])
439def addRecurringEvents():
440 recurringEvents = getRecurringEventsData(preprocessEventData(request.form.copy()))
441 return json.dumps(recurringEvents, default=str)
444@admin_bp.route('/userProfile', methods=['POST'])
445def userProfile():
446 volunteerName= request.form.copy()
447 if volunteerName['searchStudentsInput']:
448 username = volunteerName['searchStudentsInput'].strip("()")
449 user=username.split('(')[-1]
450 return redirect(url_for('main.viewUsersProfile', username=user))
451 else:
452 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
453 return redirect(url_for('admin.studentSearchPage'))
455@admin_bp.route('/search_student', methods=['GET'])
456def studentSearchPage():
457 if g.current_user.isAdmin:
458 return render_template("/admin/searchStudentPage.html")
459 abort(403)
461@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
462def activityLogs():
463 if g.current_user.isCeltsAdmin:
464 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
465 return render_template("/admin/activityLogs.html",
466 allLogs = allLogs)
467 else:
468 abort(403)
470@admin_bp.route("/deleteEventFile", methods=["POST"])
471def deleteEventFile():
472 fileData= request.form
473 eventfile=FileHandler(eventId=fileData["databaseId"])
474 eventfile.deleteFile(fileData["fileId"])
475 return ""
477@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
478def addCourseFile():
479 fileData = request.files['addCourseParticipants']
480 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
481 fileData.save(filePath)
482 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
483 os.remove(filePath)
484 return redirect(url_for("admin.manageServiceLearningCourses"))
486@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
487@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
488def manageServiceLearningCourses(term=None):
490 """
491 The SLC management page for admins
492 """
493 if not g.current_user.isCeltsAdmin:
494 abort(403)
496 if request.method == 'POST' and "submitParticipant" in request.form:
497 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
498 flash('Courses and participants saved successfully!', 'success')
499 return redirect(url_for('admin.manageServiceLearningCourses'))
501 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
503 setRedirectTarget(request.full_path)
504 # retrieve and store the courseID of the imported course from a session variable if it exists.
505 # This allows us to export the courseID in the html and use it.
506 courseID = session.get("alterCourseId")
508 if courseID:
509 # delete courseID from the session if it was retrieved, for storage purposes.
510 session.pop("alterCourseId")
511 return render_template('/admin/manageServiceLearningFaculty.html',
512 courseInstructors = getInstructorCourses(),
513 unapprovedCourses = unapprovedCourses(manageTerm),
514 approvedCourses = approvedCourses(manageTerm),
515 importedCourses = getImportedCourses(manageTerm),
516 terms = selectSurroundingTerms(g.current_term),
517 term = manageTerm,
518 cpPreview = session.get('cpPreview', {}),
519 cpPreviewErrors = session.get('cpErrors', []),
520 courseID = courseID
521 )
523 return render_template('/admin/manageServiceLearningFaculty.html',
524 courseInstructors = getInstructorCourses(),
525 unapprovedCourses = unapprovedCourses(manageTerm),
526 approvedCourses = approvedCourses(manageTerm),
527 importedCourses = getImportedCourses(manageTerm),
528 terms = selectSurroundingTerms(g.current_term),
529 term = manageTerm,
530 cpPreview= session.get('cpPreview',{}),
531 cpPreviewErrors = session.get('cpErrors',[])
532 )
534@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
535def getSidebarInformation() -> str:
536 """
537 Get the count of unapproved courses and students interested in the minor for the current term
538 to display in the admin sidebar. It must be returned as a string to be received by the
539 ajax request.
540 """
541 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
542 interestedStudentsCount: int = len(getMinorInterest())
543 return {"unapprovedCoursesCount": unapprovedCoursesCount,
544 "interestedStudentsCount": interestedStudentsCount}
546@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
547def removeFromSession():
548 try:
549 session.pop('cpPreview')
550 except KeyError:
551 pass
553 return ""
555@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
556def alterImportedCourse(courseID):
557 """
558 This route handles a GET and a POST request for the purpose of imported courses.
559 The GET request provides preexisting information of an imported course in a modal.
560 The POST request updates a specific imported course (course name, course abbreviation,
561 hours earned on completion, list of instructors) in the database with new information
562 coming from the imported courses modal.
563 """
564 if request.method == 'GET':
565 try:
566 targetCourse = Course.get_by_id(courseID)
567 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
569 try:
570 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
571 except IndexError: # If a course has no participant, IndexError will be raised
572 serviceHours = 20
574 courseData = model_to_dict(targetCourse, recurse=False)
575 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
576 courseData['hoursEarned'] = serviceHours
578 return jsonify(courseData)
580 except DoesNotExist:
581 flash("Course not found")
582 return jsonify({"error": "Course not found"}), 404
584 if request.method == 'POST':
585 # Update course information in the database
586 courseData = request.form.copy()
587 editImportedCourses(courseData)
588 session['alterCourseId'] = courseID
590 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
593@admin_bp.route("/manageBonner")
594def manageBonner():
595 if not g.current_user.isCeltsAdmin:
596 abort(403)
598 return render_template("/admin/bonnerManagement.html",
599 cohorts=getBonnerCohorts(),
600 events=getBonnerEvents(g.current_term),
601 requirements=getCertRequirements(certification=Certification.BONNER))
603@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
604def updatecohort(year, method, username):
605 if not g.current_user.isCeltsAdmin:
606 abort(403)
608 try:
609 user = User.get_by_id(username)
610 except:
611 abort(500)
613 if method == "add":
614 try:
615 BonnerCohort.create(year=year, user=user)
616 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
617 except IntegrityError as e:
618 # if they already exist, ignore the error
619 flash(f'Error: {user.fullName} already added.', "danger")
620 pass
622 elif method == "remove":
623 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
624 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
625 else:
626 flash(f"Error: {user.fullName} can't be added.", "danger")
627 abort(500)
629 return ""
631@admin_bp.route("/bonnerxls")
632def bonnerxls():
633 if not g.current_user.isCeltsAdmin:
634 abort(403)
636 newfile = makeBonnerXls()
637 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
639@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
640def saveRequirements(certid):
641 if not g.current_user.isCeltsAdmin:
642 abort(403)
644 newRequirements = updateCertRequirements(certid, request.get_json())
646 return jsonify([requirement.id for requirement in newRequirements])
649@admin_bp.route("/displayEventFile", methods=["POST"])
650def displayEventFile():
651 fileData = request.form
652 eventfile = FileHandler(eventId=fileData["id"])
653 isChecked = fileData.get('checked') == 'true'
654 eventfile.changeDisplay(fileData['id'], isChecked)
655 return ""