Coverage for app/controllers/admin/routes.py: 22%
432 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-11-22 20:53 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-11-22 20:53 +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 'endDate': f'{formData["endDate"][-4:]}-{formData["endDate"][0:-5]}',
230 'isRecurring': bool(priorEvent['recurringId']),
231 'isMultipleOffering': bool(priorEvent['multipleOfferingId']),
232 })
233 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
234 if message:
235 flash(message, "danger")
236 return redirect(url_for('admin.eventDisplay', eventId = eventId))
238 copyRsvpToNewEvent(priorEvent, newEvent[0])
239 createActivityLog(f"Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.")
240 flash("Event successfully renewed.", "success")
241 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
244 except Exception as e:
245 print("Error while trying to renew event:", e)
246 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
247 return redirect(url_for('admin.eventDisplay', eventId = eventId))
251@admin_bp.route('/event/<eventId>/view', methods=['GET'])
252@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
253def eventDisplay(eventId):
254 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
255 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
256 viewer = g.current_user
257 event = Event.get_by_id(eventId)
258 addEventView(viewer,event)
259 # Validate given URL
260 try:
261 event = Event.get_by_id(eventId)
262 except DoesNotExist as e:
263 print(f"Unknown event: {eventId}")
264 abort(404)
266 notPermitted = not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerForEvent(event))
267 if 'edit' in request.url_rule.rule and notPermitted:
268 abort(403)
270 eventData = model_to_dict(event, recurse=False)
271 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
272 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
274 image = None
275 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
276 for attachment in associatedAttachments:
277 for extension in picurestype:
278 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
279 image = filepaths[attachment.fileName][0]
280 if image:
281 break
284 if request.method == "POST": # Attempt to save form
285 eventData = request.form.copy()
286 try:
287 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
289 except Exception as e:
290 print("Error saving event:", e)
291 savedEvents = False
292 validationErrorMessage = "Unknown Error Saving Event. Please try again"
295 if savedEvents:
296 rsvpcohorts = request.form.getlist("cohorts[]")
297 for year in rsvpcohorts:
298 rsvpForBonnerCohort(int(year), event.id)
299 addBonnerCohortToRsvpLog(int(year), event.id)
301 flash("Event successfully updated!", "success")
302 return redirect(url_for("admin.eventDisplay", eventId = event.id))
303 else:
304 flash(validationErrorMessage, 'warning')
306 # make sure our data is the same regardless of GET and POST
307 preprocessEventData(eventData)
308 eventData['program'] = event.program
309 futureTerms = selectSurroundingTerms(g.current_term)
310 userHasRSVPed = checkUserRsvp(g.current_user, event)
311 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
312 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
313 requirements, bonnerCohorts = [], []
315 if eventData['program'] and eventData['program'].isBonnerScholars:
316 requirements = getCertRequirements(Certification.BONNER)
317 bonnerCohorts = getBonnerCohorts(limit=5)
319 rule = request.url_rule
321 # Event Edit
322 if 'edit' in rule.rule:
323 return render_template("events/createEvent.html",
324 eventData = eventData,
325 futureTerms=futureTerms,
326 event = event,
327 requirements = requirements,
328 bonnerCohorts = bonnerCohorts,
329 userHasRSVPed = userHasRSVPed,
330 isProgramManager = isProgramManager,
331 filepaths = filepaths)
332 # Event View
333 else:
334 # get text representations of dates for html
335 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
336 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
337 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
338 eventCountdown = getCountdownToEvent(event)
341 # Identify the next event in a recurring series
342 if event.recurringId:
343 eventSeriesList = list(Event.select().where(Event.recurringId == event.recurringId)
344 .where((Event.isCanceled == False) | (Event.id == event.id))
345 .order_by(Event.startDate))
346 eventIndex = eventSeriesList.index(event)
347 if len(eventSeriesList) != (eventIndex + 1):
348 eventData["nextRecurringEvent"] = eventSeriesList[eventIndex + 1]
350 currentEventRsvpAmount = getEventRsvpCount(event.id)
352 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
354 return render_template("events/eventView.html",
355 eventData=eventData,
356 event=event,
357 userHasRSVPed=userHasRSVPed,
358 programTrainings=userParticipatedTrainingEvents,
359 currentEventRsvpAmount=currentEventRsvpAmount,
360 isProgramManager=isProgramManager,
361 filepaths=filepaths,
362 image=image,
363 pageViewsCount=pageViewsCount,
364 eventCountdown=eventCountdown
365 )
369@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
370def cancelRoute(eventId):
371 if g.current_user.isAdmin:
372 try:
373 cancelEvent(eventId)
374 return redirect(request.referrer)
376 except Exception as e:
377 print('Error while canceling event:', e)
378 return "", 500
380 else:
381 abort(403)
383@admin_bp.route('/event/undo', methods=['GET'])
384def undoEvent():
385 try:
386 events = session['lastDeletedEvent']
387 for eventId in events:
388 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
389 event = Event.get_or_none(Event.id == eventId)
390 recurringEvents = list(Event.select().where((Event.recurringId==event.recurringId) & (Event.deletionDate == None)).order_by(Event.id))
391 if event.recurringId is not None:
392 nameCounter = 1
393 for recurringEvent in recurringEvents:
394 newEventNameList = recurringEvent.name.split()
395 newEventNameList[-1] = f"{nameCounter}"
396 newEventNameList = " ".join(newEventNameList)
397 Event.update({Event.name: newEventNameList}).where(Event.id==recurringEvent.id).execute()
398 nameCounter += 1
399 flash("Deletion successfully undone.", "success")
400 return redirect('/eventsList/' + str(g.current_term))
401 except Exception as e:
402 print('Error while canceling event:', e)
403 return "", 500
405@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
406def deleteRoute(eventId):
407 try:
408 deleteEvent(eventId)
409 session['lastDeletedEvent'] = [eventId]
410 flash("Event successfully deleted.", "success")
411 return redirect(url_for("main.events", selectedTerm=g.current_term))
413 except Exception as e:
414 print('Error while canceling event:', e)
415 return "", 500
417@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
418def deleteEventAndAllFollowingRoute(eventId):
419 try:
420 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
421 flash("Events successfully deleted.", "success")
422 return redirect(url_for("main.events", selectedTerm=g.current_term))
424 except Exception as e:
425 print('Error while canceling event:', e)
426 return "", 500
428@admin_bp.route('/event/<eventId>/deleteAllRecurring', methods=['POST'])
429def deleteAllRecurringEventsRoute(eventId):
430 try:
431 session["lastDeletedEvent"] = deleteAllRecurringEvents(eventId)
432 flash("Events successfully deleted.", "success")
433 return redirect(url_for("main.events", selectedTerm=g.current_term))
435 except Exception as e:
436 print('Error while canceling event:', e)
437 return "", 500
439@admin_bp.route('/makeRecurringEvents', methods=['POST'])
440def addRecurringEvents():
441 recurringEvents = getRecurringEventsData(preprocessEventData(request.form.copy()))
442 return json.dumps(recurringEvents, default=str)
445@admin_bp.route('/userProfile', methods=['POST'])
446def userProfile():
447 volunteerName= request.form.copy()
448 if volunteerName['searchStudentsInput']:
449 username = volunteerName['searchStudentsInput'].strip("()")
450 user=username.split('(')[-1]
451 return redirect(url_for('main.viewUsersProfile', username=user))
452 else:
453 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
454 return redirect(url_for('admin.studentSearchPage'))
456@admin_bp.route('/search_student', methods=['GET'])
457def studentSearchPage():
458 if g.current_user.isAdmin:
459 return render_template("/admin/searchStudentPage.html")
460 abort(403)
462@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
463def activityLogs():
464 if g.current_user.isCeltsAdmin:
465 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
466 return render_template("/admin/activityLogs.html",
467 allLogs = allLogs)
468 else:
469 abort(403)
471@admin_bp.route("/deleteEventFile", methods=["POST"])
472def deleteEventFile():
473 fileData= request.form
474 eventfile=FileHandler(eventId=fileData["databaseId"])
475 eventfile.deleteFile(fileData["fileId"])
476 return ""
478@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
479def addCourseFile():
480 fileData = request.files['addCourseParticipants']
481 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
482 fileData.save(filePath)
483 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
484 os.remove(filePath)
485 return redirect(url_for("admin.manageServiceLearningCourses"))
487@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
488@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
489def manageServiceLearningCourses(term=None):
491 """
492 The SLC management page for admins
493 """
494 if not g.current_user.isCeltsAdmin:
495 abort(403)
497 if request.method == 'POST' and "submitParticipant" in request.form:
498 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
499 flash('Courses and participants saved successfully!', 'success')
500 return redirect(url_for('admin.manageServiceLearningCourses'))
502 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
504 setRedirectTarget(request.full_path)
505 # retrieve and store the courseID of the imported course from a session variable if it exists.
506 # This allows us to export the courseID in the html and use it.
507 courseID = session.get("alterCourseId")
509 if courseID:
510 # delete courseID from the session if it was retrieved, for storage purposes.
511 session.pop("alterCourseId")
512 return render_template('/admin/manageServiceLearningFaculty.html',
513 courseInstructors = getInstructorCourses(),
514 unapprovedCourses = unapprovedCourses(manageTerm),
515 approvedCourses = approvedCourses(manageTerm),
516 importedCourses = getImportedCourses(manageTerm),
517 terms = selectSurroundingTerms(g.current_term),
518 term = manageTerm,
519 cpPreview = session.get('cpPreview', {}),
520 cpPreviewErrors = session.get('cpErrors', []),
521 courseID = courseID
522 )
524 return render_template('/admin/manageServiceLearningFaculty.html',
525 courseInstructors = getInstructorCourses(),
526 unapprovedCourses = unapprovedCourses(manageTerm),
527 approvedCourses = approvedCourses(manageTerm),
528 importedCourses = getImportedCourses(manageTerm),
529 terms = selectSurroundingTerms(g.current_term),
530 term = manageTerm,
531 cpPreview= session.get('cpPreview',{}),
532 cpPreviewErrors = session.get('cpErrors',[])
533 )
535@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
536def getSidebarInformation() -> str:
537 """
538 Get the count of unapproved courses and students interested in the minor for the current term
539 to display in the admin sidebar. It must be returned as a string to be received by the
540 ajax request.
541 """
542 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
543 interestedStudentsCount: int = len(getMinorInterest())
544 return {"unapprovedCoursesCount": unapprovedCoursesCount,
545 "interestedStudentsCount": interestedStudentsCount}
547@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
548def removeFromSession():
549 try:
550 session.pop('cpPreview')
551 except KeyError:
552 pass
554 return ""
556@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
557def alterImportedCourse(courseID):
558 """
559 This route handles a GET and a POST request for the purpose of imported courses.
560 The GET request provides preexisting information of an imported course in a modal.
561 The POST request updates a specific imported course (course name, course abbreviation,
562 hours earned on completion, list of instructors) in the database with new information
563 coming from the imported courses modal.
564 """
565 if request.method == 'GET':
566 try:
567 targetCourse = Course.get_by_id(courseID)
568 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
570 try:
571 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
572 except IndexError: # If a course has no participant, IndexError will be raised
573 serviceHours = 20
575 courseData = model_to_dict(targetCourse, recurse=False)
576 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
577 courseData['hoursEarned'] = serviceHours
579 return jsonify(courseData)
581 except DoesNotExist:
582 flash("Course not found")
583 return jsonify({"error": "Course not found"}), 404
585 if request.method == 'POST':
586 # Update course information in the database
587 courseData = request.form.copy()
588 editImportedCourses(courseData)
589 session['alterCourseId'] = courseID
591 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
594@admin_bp.route("/manageBonner")
595def manageBonner():
596 if not g.current_user.isCeltsAdmin:
597 abort(403)
599 return render_template("/admin/bonnerManagement.html",
600 cohorts=getBonnerCohorts(),
601 events=getBonnerEvents(g.current_term),
602 requirements=getCertRequirements(certification=Certification.BONNER))
604@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
605def updatecohort(year, method, username):
606 if not g.current_user.isCeltsAdmin:
607 abort(403)
609 try:
610 user = User.get_by_id(username)
611 except:
612 abort(500)
614 if method == "add":
615 try:
616 BonnerCohort.create(year=year, user=user)
617 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
618 except IntegrityError as e:
619 # if they already exist, ignore the error
620 flash(f'Error: {user.fullName} already added.', "danger")
621 pass
623 elif method == "remove":
624 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
625 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
626 else:
627 flash(f"Error: {user.fullName} can't be added.", "danger")
628 abort(500)
630 return ""
632@admin_bp.route("/bonnerxls")
633def bonnerxls():
634 if not g.current_user.isCeltsAdmin:
635 abort(403)
637 newfile = makeBonnerXls()
638 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
640@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
641def saveRequirements(certid):
642 if not g.current_user.isCeltsAdmin:
643 abort(403)
645 newRequirements = updateCertRequirements(certid, request.get_json())
647 return jsonify([requirement.id for requirement in newRequirements])
650@admin_bp.route("/displayEventFile", methods=["POST"])
651def displayEventFile():
652 fileData = request.form
653 eventfile = FileHandler(eventId=fileData["id"])
654 isChecked = fileData.get('checked') == 'true'
655 eventfile.changeDisplay(fileData['id'], isChecked)
656 return ""