Coverage for app/controllers/admin/routes.py: 22%
536 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-09-02 19:10 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-09-02 19:10 +0000
1from flask import request, render_template, url_for, g, redirect
2from flask import flash, abort, jsonify, session, send_file
3from peewee import DoesNotExist, fn, IntegrityError
4from playhouse.shortcuts import model_to_dict
5import json
6from datetime import datetime
7import os
9from app import app
10from app.models.backgroundCheck import BackgroundCheck
11from app.models.program import Program
12from app.models.event import Event
13from app.models.eventRsvp import EventRsvp
14from app.models.eventParticipant import EventParticipant
15from app.models.user import User
16from app.models.course import Course
17from app.models.courseInstructor import CourseInstructor
18from app.models.courseParticipant import CourseParticipant
19from app.models.eventTemplate import EventTemplate
20from app.models.activityLog import ActivityLog
21from app.models.eventRsvpLog import EventRsvpLog
22from app.models.attachmentUpload import AttachmentUpload
23from app.models.bonnerCohort import BonnerCohort
24from app.models.eventCohort import EventCohort
25from app.models.certification import Certification
26from app.models.user import User
27from app.models.term import Term
28from app.models.eventViews import EventView
29from app.models.courseStatus import CourseStatus
31from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates
32from app.logic.createLogs import createActivityLog
33from app.logic.certification import getCertRequirements, updateCertRequirements
34from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget
35from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRepeatingEventsData, deleteEventAndAllFollowing, deleteAllEventsInSeries, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewSeriesId, inviteCohortsToEvent, updateEventCohorts
36from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp
37from app.logic.minor import getMinorInterest
38from app.logic.fileHandler import FileHandler
39from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog
40from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses
41from app.logic.participants import sortParticipants
43from app.controllers.admin import admin_bp
44from app.logic.volunteerSpreadsheet import createSpreadsheet
45from app.logic.users import isBannedFromEvent
47from peewee import DoesNotExist, JOIN
48from app.logic.participants import updateEventLabor, getEventParticipants, addParticipantToEvent
49from app.logic.sharedLogic import getEventLengthInHours
50from app.logic.events import getPreviousSeriesEventData, getEventRsvpCount
51from app.logic.users import getBannedUsers
53@admin_bp.route('/admin/reports')
54def reports():
55 academicYears = Term.select(Term.academicYear).distinct().order_by(Term.academicYear.desc())
56 academicYears = list(map(lambda t: t.academicYear, academicYears))
57 return render_template("/admin/reports.html", academicYears=academicYears)
59@admin_bp.route('/admin/reports/download', methods=['POST'])
60def downloadFile():
61 academicYear = request.form.get('academicYear')
62 filepath = os.path.abspath(createSpreadsheet(academicYear))
63 return send_file(filepath, as_attachment=True)
67@admin_bp.route('/switch_user', methods=['POST'])
68def switchUser():
69 if app.env == "production":
70 print(f"An attempt was made to switch to another user by {g.current_user.username}!")
71 abort(403)
73 print(f"Switching user from {g.current_user} to",request.form['newuser'])
74 session['current_user'] = model_to_dict(User.get_by_id(request.form['newuser']))
76 return redirect(request.referrer)
79@admin_bp.route('/eventTemplates')
80def templateSelect():
81 if g.current_user.isCeltsAdmin or g.current_user.isCeltsStudentStaff:
82 allprograms = getAllowedPrograms(g.current_user)
83 visibleTemplates = getAllowedTemplates(g.current_user)
84 return render_template("/events/templateSelector.html",
85 programs=allprograms,
86 celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored),
87 templates=visibleTemplates)
88 else:
89 abort(403)
92@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
93def createEvent(templateid, programid):
94 if not (g.current_user.isAdmin or g.current_user.isProgramManagerFor(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)
128 if eventData.get('isSeries'):
129 eventData['seriesData'] = json.loads(eventData['seriesData'])
130 succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request))
131 if not succeeded:
132 for index, validationErrorMessage in failedSavedOfferings:
133 eventData['seriesData'][index]['isDuplicate'] = True
134 validationErrorMessage = failedSavedOfferings[-1][1] # The last validation error message from the list of offerings if there are multiple
135 print(f"Failed to save offerings {failedSavedOfferings}")
136 else:
137 try:
138 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
139 except Exception as e:
140 print("Failed saving regular event", e)
141 validationErrorMessage = "Failed to save event."
143 if savedEvents:
144 rsvpCohorts = request.form.getlist("cohorts[]")
145 if rsvpCohorts:
146 success, message, invitedCohorts = inviteCohortsToEvent(savedEvents[0], rsvpCohorts)
147 if not success:
148 flash(message, 'warning')
150 noun = ((eventData.get('isSeries')) and "Events" or "Event") # pluralize
151 flash(f"{noun} successfully created!", 'success')
154 if program:
155 if len(savedEvents) > 1 and eventData.get('isRepeating'):
156 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')}.")
157 elif len(savedEvents) >= 1 and eventData.get('isSeries'):
158 eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents]
159 eventList = ', '.join(f"<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>" for event in savedEvents)
161 if len(savedEvents) > 1:
162 #creates list of events created in a multiple series to display in the logs
163 eventList = ', '.join(eventList.split(', ')[:-1]) + f', and ' + eventList.split(', ')[-1]
164 #get last date and stick at the end after 'and' so that it reads like a sentence in admin log
165 lastEventDate = eventDates[-1]
166 eventDates = ', '.join(eventDates[:-1]) + f', and {lastEventDate}'
168 createActivityLog(f"Created series {eventList} for {program.programName}, with start dates of {eventDates}.")
170 else:
171 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')}.")
172 else:
173 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')}.")
175 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id))
176 else:
177 flash(validationErrorMessage, 'warning')
179 # make sure our data is the same regardless of GET or POST
180 preprocessEventData(eventData)
181 isProgramManager = g.current_user.isProgramManagerFor(programid)
183 futureTerms = selectSurroundingTerms(g.current_term, prevTerms=0)
185 requirements, bonnerCohorts = [], []
186 if eventData['program'] is not None and eventData['program'].isBonnerScholars:
187 requirements = getCertRequirements(Certification.BONNER)
188 rawBonnerCohorts = getBonnerCohorts(limit=5)
189 bonnerCohorts = {}
191 for year, cohort in rawBonnerCohorts.items():
192 if cohort:
193 bonnerCohorts[year] = cohort
196 return render_template(f"/events/{template.templateFile}",
197 template = template,
198 eventData = eventData,
199 futureTerms = futureTerms,
200 requirements = requirements,
201 bonnerCohorts = bonnerCohorts,
202 isProgramManager = isProgramManager)
205@admin_bp.route('/event/<eventId>/rsvp', methods=['GET'])
206def rsvpLogDisplay(eventId):
207 event = Event.get_by_id(eventId)
208 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)):
209 allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc())
210 return render_template("/events/rsvpLog.html",
211 event = event,
212 allLogs = allLogs)
213 else:
214 abort(403)
216@admin_bp.route('/event/<eventId>/renew', methods=['POST'])
217def renewEvent(eventId):
218 try:
219 formData = request.form
220 try:
221 assert formData['timeStart'] < formData['timeEnd']
222 except AssertionError:
223 flash("End time must be after start time", 'warning')
224 return redirect(url_for('admin.eventDisplay', eventId = eventId))
226 try:
227 if formData.get('dateEnd'):
228 assert formData['dateStart'] < formData['dateEnd']
229 except AssertionError:
230 flash("End date must be after start date", 'warning')
231 return redirect(url_for('admin.eventDisplay', eventId = eventId))
234 priorEvent = model_to_dict(Event.get_by_id(eventId))
235 newEventDict = priorEvent.copy()
236 newEventDict.pop('id')
237 newEventDict.update({
238 'program': int(priorEvent['program']['id']),
239 'term': int(priorEvent['term']['id']),
240 'timeStart': formData['timeStart'],
241 'timeEnd': formData['timeEnd'],
242 'location': formData['location'],
243 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
244 'isRepeating': bool(priorEvent['isRepeating']),
245 'seriesId': priorEvent['seriesId'],
246 })
247 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
248 if message:
249 flash(message, "danger")
250 return redirect(url_for('admin.eventDisplay', eventId = eventId))
252 copyRsvpToNewEvent(priorEvent, newEvent[0])
253 createActivityLog(f"Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.")
254 flash("Event successfully renewed.", "success")
255 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id))
258 except Exception as e:
259 print("Error while trying to renew event:", e)
260 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger')
261 return redirect(url_for('admin.eventDisplay', eventId = eventId))
265@admin_bp.route('/event/<eventId>/view', methods=['GET'])
266@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST'])
267def eventDisplay(eventId):
268 pageViewsCount = EventView.select().where(EventView.event == eventId).count()
269 if request.method == 'GET' and request.path == f'/event/{eventId}/view':
270 viewer = g.current_user
271 event = Event.get_by_id(eventId)
272 addEventView(viewer,event)
273 # Validate given URL
274 try:
275 event = Event.get_by_id(eventId)
276 invitedCohorts = list(EventCohort.select().where(
277 EventCohort.event == event
278 ))
279 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
280 except DoesNotExist as e:
281 print(f"Unknown event: {eventId}")
282 abort(404)
284 notPermitted = not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerForEvent(event))
285 if 'edit' in request.url_rule.rule and notPermitted:
286 abort(403)
288 eventData = model_to_dict(event, recurse=False)
289 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
290 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
292 image = None
293 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"]
294 for attachment in associatedAttachments:
295 for extension in picurestype:
296 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True):
297 image = filepaths[attachment.fileName][0]
298 if image:
299 break
302 if request.method == "POST": # Attempt to save form
303 eventData = request.form.copy()
304 try:
305 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
307 except Exception as e:
308 print("Error saving event:", e)
309 savedEvents = False
310 validationErrorMessage = "Unknown Error Saving Event. Please try again"
313 if savedEvents:
314 rsvpCohorts = request.form.getlist("cohorts[]")
315 updateEventCohorts(savedEvents[0], rsvpCohorts)
316 flash("Event successfully updated!", "success")
317 return redirect(url_for("admin.eventDisplay", eventId = event.id))
318 else:
319 flash(validationErrorMessage, 'warning')
321 # make sure our data is the same regardless of GET and POST
322 preprocessEventData(eventData)
323 eventData['program'] = event.program
324 futureTerms = selectSurroundingTerms(g.current_term)
325 userHasRSVPed = checkUserRsvp(g.current_user, event)
326 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
327 isProgramManager = g.current_user.isProgramManagerFor(eventData['program'])
328 requirements, bonnerCohorts = [], []
330 if eventData['program'] and eventData['program'].isBonnerScholars:
331 requirements = getCertRequirements(Certification.BONNER)
332 rawBonnerCohorts = getBonnerCohorts(limit=5)
333 bonnerCohorts = {}
335 for year, cohort in rawBonnerCohorts.items():
336 if cohort:
337 bonnerCohorts[year] = cohort
339 invitedCohorts = list(EventCohort.select().where(
340 EventCohort.event_id == eventId,
341 ))
342 invitedYears = [str(cohort.year) for cohort in invitedCohorts]
343 else:
344 requirements, bonnerCohorts, invitedYears = [], [], []
346 rule = request.url_rule
348 # Event Edit
349 if 'edit' in rule.rule:
350 return render_template("events/createEvent.html",
351 eventData = eventData,
352 futureTerms = futureTerms,
353 event = event,
354 requirements = requirements,
355 bonnerCohorts = bonnerCohorts,
356 invitedYears = invitedYears,
357 userHasRSVPed = userHasRSVPed,
358 isProgramManager = isProgramManager,
359 filepaths = filepaths)
360 # Event View
361 else:
362 # get text representations of dates for html
363 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p")
364 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p")
365 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y")
366 eventCountdown = getCountdownToEvent(event)
369 # Identify the next event in a repeating series
370 if event.seriesId:
371 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId)
372 .where((Event.isCanceled == False) | (Event.id == event.id))
373 .order_by(Event.startDate))
374 eventIndex = eventSeriesList.index(event)
375 if len(eventSeriesList) != (eventIndex + 1):
376 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1]
378 currentEventRsvpAmount = getEventRsvpCount(event.id)
380 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
382 return render_template("events/eventView.html",
383 eventData=eventData,
384 event=event,
385 userHasRSVPed=userHasRSVPed,
386 programTrainings=userParticipatedTrainingEvents,
387 currentEventRsvpAmount=currentEventRsvpAmount,
388 isProgramManager=isProgramManager,
389 filepaths=filepaths,
390 image=image,
391 pageViewsCount=pageViewsCount,
392 invitedYears=invitedYears,
393 eventCountdown=eventCountdown
394 )
398@admin_bp.route('/event/<eventId>/cancel', methods=['POST'])
399def cancelRoute(eventId):
400 if g.current_user.isAdmin:
401 try:
402 cancelEvent(eventId)
403 return redirect(request.referrer)
405 except Exception as e:
406 print('Error while canceling event:', e)
407 return "", 500
409 else:
410 abort(403)
412@admin_bp.route('/profile/undo', methods=['GET'])
413def undoBackgroundCheck():
414 try:
415 username = g.current_user
416 bgCheckId = session['lastDeletedBgCheck']
417 BackgroundCheck.update({BackgroundCheck.deletionDate: None, BackgroundCheck.deletedBy: None}).where(BackgroundCheck.id == bgCheckId).execute()
418 flash("Background Check has been successfully restored.", "success")
419 return redirect (f"/profile/{username}?accordion=background")
420 except Exception as e:
421 print('Error while undoing background check:', e)
422 return "", 500
424@admin_bp.route('/event/undo', methods=['GET'])
425def undoEvent():
426 try:
427 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted
428 for eventId in eventIds:
429 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
430 event = Event.get_or_none(Event.id == eventId)
431 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id))
432 if event.isRepeating:
433 nameCounter = 1
434 for repeatingEvent in repeatingEvents:
435 newEventNameList = repeatingEvent.name.split()
436 newEventNameList[-1] = f"{nameCounter}"
437 newEventNameList = " ".join(newEventNameList)
438 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute()
439 nameCounter += 1
440 flash("Event has been successfully restored.", "success")
441 return redirect(url_for("main.events", selectedTerm=g.current_term))
442 except Exception as e:
443 print('Error while canceling event:', e)
444 return "", 500
446@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
447def deleteRoute(eventId):
448 try:
449 deleteEvent(eventId)
450 session['lastDeletedEvent'] = [eventId]
451 flash("Event successfully deleted.", "success")
452 return redirect(url_for("main.events", selectedTerm=g.current_term))
454 except Exception as e:
455 print('Error while canceling event:', e)
456 return "", 500
458@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
459def deleteEventAndAllFollowingRoute(eventId):
460 try:
461 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
462 flash("Events successfully deleted.", "success")
463 return redirect(url_for("main.events", selectedTerm=g.current_term))
465 except Exception as e:
466 print('Error while canceling event:', e)
467 return "", 500
469@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST'])
470def deleteAllEventsInSeriesRoute(eventId):
471 try:
472 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId)
473 flash("Events successfully deleted.", "success")
474 return redirect(url_for("main.events", selectedTerm=g.current_term))
476 except Exception as e:
477 print('Error while canceling event:', e)
478 return "", 500
480@admin_bp.route('/makeRepeatingEvents', methods=['POST'])
481def addRepeatingEvents():
482 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy()))
483 return json.dumps(repeatingEvents, default=str)
486@admin_bp.route('/userProfile', methods=['POST'])
487def userProfile():
488 volunteerName= request.form.copy()
489 if volunteerName['searchStudentsInput']:
490 username = volunteerName['searchStudentsInput'].strip("()")
491 user=username.split('(')[-1]
492 return redirect(url_for('main.viewUsersProfile', username=user))
493 else:
494 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger')
495 return redirect(url_for('admin.studentSearchPage'))
497@admin_bp.route('/search_student', methods=['GET'])
498def studentSearchPage():
499 if g.current_user.isAdmin:
500 return render_template("/admin/searchStudentPage.html")
501 abort(403)
503@admin_bp.route('/activityLogs', methods = ['GET', 'POST'])
504def activityLogs():
505 if g.current_user.isCeltsAdmin:
506 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc())
507 return render_template("/admin/activityLogs.html",
508 allLogs = allLogs)
509 else:
510 abort(403)
512@admin_bp.route("/deleteEventFile", methods=["POST"])
513def deleteEventFile():
514 fileData= request.form
515 eventfile=FileHandler(eventId=fileData["databaseId"])
516 eventfile.deleteFile(fileData["fileId"])
517 return ""
519@admin_bp.route("/uploadCourseParticipant", methods= ["POST"])
520def addCourseFile():
521 fileData = request.files['addCourseParticipants']
522 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename)
523 fileData.save(filePath)
524 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath)
525 os.remove(filePath)
526 return redirect(url_for("admin.manageServiceLearningCourses"))
528@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST'])
529@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST'])
530def manageServiceLearningCourses(term=None):
532 """
533 The SLC management page for admins
534 """
535 if not g.current_user.isCeltsAdmin:
536 abort(403)
538 if request.method == 'POST' and "submitParticipant" in request.form:
539 saveCourseParticipantsToDatabase(session.pop('cpPreview', {}))
540 flash('Courses and participants saved successfully!', 'success')
541 return redirect(url_for('admin.manageServiceLearningCourses'))
543 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
545 setRedirectTarget(request.full_path)
546 # retrieve and store the courseID of the imported course from a session variable if it exists.
547 # This allows us to export the courseID in the html and use it.
548 courseID = session.get("alterCourseId")
550 if courseID:
551 # delete courseID from the session if it was retrieved, for storage purposes.
552 session.pop("alterCourseId")
553 return render_template('/admin/manageServiceLearningFaculty.html',
554 courseInstructors = getInstructorCourses(),
555 unapprovedCourses = unapprovedCourses(manageTerm),
556 approvedCourses = approvedCourses(manageTerm),
557 importedCourses = getImportedCourses(manageTerm),
558 terms = selectSurroundingTerms(g.current_term),
559 term = manageTerm,
560 cpPreview = session.get('cpPreview', {}),
561 cpPreviewErrors = session.get('cpErrors', []),
562 courseID = courseID
563 )
565 return render_template('/admin/manageServiceLearningFaculty.html',
566 courseInstructors = getInstructorCourses(),
567 unapprovedCourses = unapprovedCourses(manageTerm),
568 approvedCourses = approvedCourses(manageTerm),
569 importedCourses = getImportedCourses(manageTerm),
570 terms = selectSurroundingTerms(g.current_term),
571 term = manageTerm,
572 cpPreview= session.get('cpPreview',{}),
573 cpPreviewErrors = session.get('cpErrors',[])
574 )
576@admin_bp.route('/admin/getSidebarInformation', methods=['GET'])
577def getSidebarInformation() -> str:
578 """
579 Get the count of unapproved courses and students interested in the minor for the current term
580 to display in the admin sidebar. It must be returned as a string to be received by the
581 ajax request.
582 """
583 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term))
584 interestedStudentsCount: int = len(getMinorInterest())
585 return {"unapprovedCoursesCount": unapprovedCoursesCount,
586 "interestedStudentsCount": interestedStudentsCount}
588@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
589def removeFromSession():
590 try:
591 session.pop('cpPreview')
592 except KeyError:
593 pass
595 return ""
597@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET'])
598def alterImportedCourse(courseID):
599 """
600 This route handles a GET and a POST request for the purpose of imported courses.
601 The GET request provides preexisting information of an imported course in a modal.
602 The POST request updates a specific imported course (course name, course abbreviation,
603 hours earned on completion, list of instructors) in the database with new information
604 coming from the imported courses modal.
605 """
606 if request.method == 'GET':
607 try:
608 targetCourse = Course.get_by_id(courseID)
609 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse)
611 try:
612 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned
613 except IndexError: # If a course has no participant, IndexError will be raised
614 serviceHours = 20
616 courseData = model_to_dict(targetCourse, recurse=False)
617 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors]
618 courseData['hoursEarned'] = serviceHours
620 return jsonify(courseData)
622 except DoesNotExist:
623 flash("Course not found")
624 return jsonify({"error": "Course not found"}), 404
626 if request.method == 'POST':
627 # Update course information in the database
628 courseData = request.form.copy()
629 editImportedCourses(courseData)
630 session['alterCourseId'] = courseID
632 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId']))
635@admin_bp.route("/manageBonner")
636def manageBonner():
637 if not g.current_user.isCeltsAdmin:
638 abort(403)
640 return render_template("/admin/bonnerManagement.html",
641 cohorts=getBonnerCohorts(),
642 events=getBonnerEvents(g.current_term),
643 requirements=getCertRequirements(certification=Certification.BONNER))
645@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"])
646def updatecohort(year, method, username):
647 if not g.current_user.isCeltsAdmin:
648 abort(403)
650 try:
651 user = User.get_by_id(username)
652 except:
653 abort(500)
655 if method == "add":
656 try:
657 BonnerCohort.create(year=year, user=user)
658 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success")
659 except IntegrityError as e:
660 # if they already exist, ignore the error
661 flash(f'Error: {user.fullName} already added.', "danger")
662 pass
664 elif method == "remove":
665 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute()
666 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success")
667 else:
668 flash(f"Error: {user.fullName} can't be added.", "danger")
669 abort(500)
670 return ""
672@admin_bp.route("/bonnerXls/<startingYear>/<noOfYears>")
673def getBonnerXls(startingYear, noOfYears):
674 if not g.current_user.isCeltsAdmin:
675 abort(403)
676 newfile = makeBonnerXls(startingYear, noOfYears)
677 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
680@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
681def saveRequirements(certid):
682 if not g.current_user.isCeltsAdmin:
683 abort(403)
685 newRequirements = updateCertRequirements(certid, request.get_json())
687 return jsonify([requirement.id for requirement in newRequirements])
690@admin_bp.route("/displayEventFile", methods=["POST"])
691def displayEventFile():
692 fileData = request.form
693 eventfile = FileHandler(eventId=fileData["id"])
694 isChecked = fileData.get('checked') == 'true'
695 eventfile.changeDisplay(fileData['id'], isChecked)
696 return ""
700@admin_bp.route('/event/<eventID>/manage_labor', methods=['GET', 'POST'])
701def manageLaborPage(eventID):
702 try:
703 event = Event.get_by_id(eventID)
704 except DoesNotExist as e:
705 print(f"No event found for {eventID}", e)
706 abort(404)
708 if request.method == "POST":
709 laborUpdated = updateEventLabor(request.form)
711 # error handling depending on the boolean returned from updateEventLabor
712 if laborUpdated:
713 flash("Labor table succesfully updated", "success")
714 else:
715 flash("Error adding labor", "danger")
716 return redirect(url_for("admin.manageLaborPage", eventID=eventID))
718 # ------------ GET request ------------
719 elif request.method == "GET":
720 if not (g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerForEvent(event))):
721 abort(403)
723 # ------- Grab the different lists of participants -------
725 bannedUsersForProgram = [bannedUser.user for bannedUser in getBannedUsers(event.program)]
727 eventLaborData, eventLabor = sortParticipants(event, True)
729 allRelevantUsers = list(set(participant.user for participant in (eventLabor + eventLaborData)))
730 # ----------- Get miscellaneous data -----------
731 eventLengthInHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate)
732 repeatingLabors = getPreviousSeriesEventData(event.seriesId)
734 return render_template("/events/manageLabor.html",
735 eventLaborData = eventLaborData,
736 eventLabor = eventLabor,
737 eventLength = eventLengthInHours,
738 event = event,
739 repeatingLabors = repeatingLabors,
740 bannedUsersForProgram = bannedUsersForProgram,)
743@admin_bp.route('/removeLaborFromEvent', methods = ['POST'])
744def removeLaborFromEvent():
745 user = request.form.get('username')
746 eventID = request.form.get('eventId')
747 if g.current_user.isAdmin:
748 (EventParticipant.delete().where(EventParticipant.user==user, EventParticipant.event==eventID)).execute()
749 flash("Student successfully removed", "success")
750 return ""
752@admin_bp.route('/addLaborToEvent/<eventId>', methods = ['POST'])
753def addLaborToEvent(eventId):
754 event = Event.get_by_id(eventId)
755 successfullyAddedLabor = False
756 usernameList = request.form.getlist("selectedLabor[]")
757 alreadyAddedList = []
758 addedSuccessfullyList = []
759 errorList = []
761 for user in usernameList:
762 userObj = User.get_by_id(user)
763 successfullyAddedLabor = addParticipantToEvent(userObj, event, True)
764 if successfullyAddedLabor == "already in":
765 alreadyAddedList.append(userObj.fullName)
766 else:
767 if successfullyAddedLabor:
768 addedSuccessfullyList.append(userObj.fullName)
769 else:
770 errorList.append(userObj.fullName)
773 studentLabor = ""
774 if alreadyAddedList:
775 studentLabor = ", ".join(vol for vol in alreadyAddedList)
776 flash(f"{studentLabor} was already added to this event.", "warning")
778 if addedSuccessfullyList:
779 studentLabor = ", ".join(vol for vol in addedSuccessfullyList)
780 flash(f"{studentLabor} added successfully.", "success")
782 if errorList:
783 studentLabor = ", ".join(vol for vol in errorList)
784 flash(f"Error when adding {studentLabor} to event.", "danger")
786 if 'ajax' in request.form and request.form['ajax']:
787 return ''
789 return redirect(url_for('admin.manageLaborPage', eventID = eventId))
791@admin_bp.route("/event/<int:event_id>/scannerentry", methods=["GET", "POST"])
792def eventKioskStatus(event_id):
793 referer = request.referrer
794 is_labor = False
796 if referer and "manage_labor" in referer:
797 is_labor = True
798 elif referer and "manage_volunteers" in referer:
799 is_labor = False
801 event = Event.get_by_id(event_id)
802 return render_template("/events/eventKiosk.html", is_labor=is_labor, event=event)
804@admin_bp.route('/addLaborToEvent/<username>/<eventId>/isBanned', methods = ['GET'])
805def isLaborBanned(username, eventId):
806 return {"banned":1} if isBannedFromEvent(username, eventId) else {"banned":0}