Coverage for app/controllers/admin/routes.py: 21%

480 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2026-09-22 13:52 +0000

1from flask import request, render_template, url_for, g, redirect 

2from flask import flash, abort, jsonify, session, send_file 

3from peewee import DoesNotExist, IntegrityError 

4from playhouse.shortcuts import model_to_dict 

5import json 

6from datetime import datetime 

7import os 

8 

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 

30from app.models.programManager import ProgramManager 

31 

32from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates 

33from app.logic.createLogs import createActivityLog 

34from app.logic.certification import getCertRequirements, updateCertRequirements 

35from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget 

36from app.logic.events import attemptSaveMultipleOfferings, cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, getRepeatingEventsData, deleteEventAndAllFollowing, deleteAllEventsInSeries, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewSeriesId, inviteCohortsToEvent, updateEventCohorts 

37from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp 

38from app.logic.minor import getMinorInterest 

39from app.logic.fileHandler import FileHandler 

40from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog 

41from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses 

42 

43from app.controllers.admin import admin_bp 

44from app.logic.volunteerSpreadsheet import createSpreadsheet 

45 

46 

47@admin_bp.route('/admin/reports') 

48def reports(): 

49 academicYears = Term.select(Term.academicYear).distinct().order_by(Term.academicYear.desc()) 

50 academicYears = list(map(lambda t: t.academicYear, academicYears)) 

51 return render_template("/admin/reports.html", academicYears=academicYears) 

52 

53@admin_bp.route('/admin/reports/download', methods=['POST']) 

54def downloadFile(): 

55 academicYear = request.form.get('academicYear') 

56 filepath = os.path.abspath(createSpreadsheet(academicYear)) 

57 return send_file(filepath, as_attachment=True) 

58 

59 

60 

61@admin_bp.route('/switch_user', methods=['POST']) 

62def switchUser(): 

63 if app.env == "production": 

64 print(f"An attempt was made to switch to another user by {g.current_user.username}!") 

65 abort(403) 

66 

67 print(f"Switching user from {g.current_user} to",request.form['newuser']) 

68 session['current_user'] = model_to_dict(User.get_by_id(request.form['newuser'])) 

69 

70 return redirect(request.referrer) 

71 

72 

73@admin_bp.route('/eventTemplates') 

74def templateSelect(): 

75 programs = getAllowedPrograms(g.current_user) 

76 if not programs: 

77 abort(403) 

78 visibleTemplates = getAllowedTemplates(g.current_user) 

79 

80 # Create extravaganza QR code if it doesn't exist 

81 imgPath = app.config["files"]["image_path"] + "/extravaganza_qr.png" 

82 if not os.path.exists(imgPath): 

83 import qrcode 

84 img = qrcode.make(request.url.split("/")[-2] + "/extravaganza") 

85 img.save(imgPath) 

86 return render_template("/events/templateSelector.html", 

87 programs=programs, 

88 celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored), 

89 templates=visibleTemplates) 

90 

91@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST']) 

92def createEvent(templateid, programid): 

93 if not g.current_user.canManageProgram(programid): 

94 abort(403) 

95 

96 # Validate given URL 

97 program = None 

98 try: 

99 template = EventTemplate.get_by_id(templateid) 

100 if programid: 

101 program = Program.get_by_id(programid) 

102 except DoesNotExist as e: 

103 print("Invalid template or program id:", e) 

104 flash("There was an error with your selection. Please try again or contact Systems Support.", "danger") 

105 return redirect(url_for("admin.program_picker")) 

106 

107 # Get the data from the form or from the template 

108 eventData = template.templateData 

109 eventData['program'] = program 

110 

111 if request.method == "GET": 

112 eventData['contactName'] = "CELTS Admin" 

113 eventData['contactEmail'] = app.config['celts_admin_contact'] 

114 if program: 

115 eventData['location'] = program.defaultLocation 

116 if program.contactName: 

117 eventData['contactName'] = program.contactName 

118 if program.contactEmail: 

119 eventData['contactEmail'] = program.contactEmail 

120 

121 # Try to save the form 

122 if request.method == "POST": 

123 savedEvents = None 

124 eventData.update(request.form.copy()) 

125 eventData = preprocessEventData(eventData) 

126 if eventData.get('isSeries'): 

127 eventData['seriesData'] = json.loads(eventData['seriesData']) 

128 succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) 

129 if not succeeded: 

130 for index, validationErrorMessage in failedSavedOfferings: 

131 eventData['seriesData'][index]['isDuplicate'] = True 

132 validationErrorMessage = failedSavedOfferings[-1][1] # The last validation error message from the list of offerings if there are multiple 

133 print(f"Failed to save offerings {failedSavedOfferings}") 

134 else: 

135 try: 

136 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request)) 

137 except Exception as e: 

138 print("Failed saving regular event: ", e) 

139 validationErrorMessage = "Failed to save event." 

140 if savedEvents: 

141 rsvpCohorts = request.form.getlist("cohorts[]") 

142 if rsvpCohorts: 

143 success, message, invitedCohorts = inviteCohortsToEvent(savedEvents[0], rsvpCohorts) 

144 if not success: 

145 flash(message, 'warning') 

146 

147 noun = ((eventData.get('isSeries')) and "Events" or "Event") # pluralize 

148 flash(f"{noun} successfully created!", 'success') 

149 

150 

151 if program: 

152 if len(savedEvents) > 1 and eventData.get('isRepeating'): 

153 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')}.''') 

154 elif len(savedEvents) >= 1 and eventData.get('isSeries'): 

155 eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents] 

156 eventList = ', '.join(f'''<a href=\"{url_for('admin.eventDisplay', eventId=event.id)}\">{event.name}</a>''' for event in savedEvents) 

157 

158 if len(savedEvents) > 1: 

159 #creates list of events created in a multiple series to display in the logs 

160 eventList = ', '.join(eventList.split(', ')[:-1]) + f', and ' + eventList.split(', ')[-1] 

161 #get last date and stick at the end after 'and' so that it reads like a sentence in admin log 

162 lastEventDate = eventDates[-1] 

163 eventDates = ', '.join(eventDates[:-1]) + f', and {lastEventDate}' 

164 

165 createActivityLog(f"Created series {eventList} for {program.programName}, with start dates of {eventDates}.") 

166 

167 else: 

168 createActivityLog('''Created event <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a> for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.''') 

169 else: 

170 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')}.''') 

171 

172 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id)) 

173 else: 

174 flash(validationErrorMessage, 'warning') 

175 

176 # make sure our data is the same regardless of GET or POST 

177 preprocessEventData(eventData) 

178 isProgramManager = g.current_user.isProgramManagerFor(programid) 

179 

180 requirements, bonnerCohorts = [], [] 

181 if eventData['program'] is not None and eventData['program'].isBonnerScholars: 

182 requirements = getCertRequirements(Certification.BONNER) 

183 rawBonnerCohorts = getBonnerCohorts(limit=5) 

184 bonnerCohorts = {} 

185 

186 for year, cohort in rawBonnerCohorts.items(): 

187 if cohort: 

188 bonnerCohorts[year] = cohort 

189 return render_template(f"/events/{template.templateFile}", 

190 template = template, 

191 eventData = eventData, 

192 termList = selectSurroundingTerms(g.current_term, prevTerms=0), 

193 requirements = requirements, 

194 bonnerCohorts = bonnerCohorts, 

195 isProgramManager = isProgramManager) 

196 

197 

198@admin_bp.route('/event/<eventId>/rsvp', methods=['GET']) 

199def rsvpLogDisplay(eventId): 

200 event = Event.get_by_id(eventId) 

201 if g.current_user.canManageProgram(event.program): 

202 allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc()) 

203 return render_template("/events/rsvpLog.html", 

204 event = event, 

205 allLogs = allLogs) 

206 else: 

207 abort(403) 

208 

209@admin_bp.route('/event/<eventId>/renew', methods=['POST']) 

210def renewEvent(eventId): 

211 try: 

212 formData = request.form 

213 try: 

214 assert formData['timeStart'] < formData['timeEnd'] 

215 except AssertionError: 

216 flash("End time must be after start time", 'warning') 

217 return redirect(url_for('admin.eventDisplay', eventId = eventId)) 

218 

219 try: 

220 if formData.get('dateEnd'): 

221 assert formData['dateStart'] < formData['dateEnd'] 

222 except AssertionError: 

223 flash("End date must be after start date", 'warning') 

224 return redirect(url_for('admin.eventDisplay', eventId = eventId)) 

225 

226 

227 priorEvent = model_to_dict(Event.get_by_id(eventId)) 

228 newEventDict = priorEvent.copy() 

229 newEventDict.pop('id') 

230 newEventDict.update({ 

231 'program': int(priorEvent['program']['id']), 

232 'term': int(priorEvent['term']['id']), 

233 'timeStart': formData['timeStart'], 

234 'timeEnd': formData['timeEnd'], 

235 'location': formData['location'], 

236 'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}', 

237 'isRepeating': bool(priorEvent['isRepeating']), 

238 'seriesId': priorEvent['seriesId'], 

239 }) 

240 newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True) 

241 if message: 

242 flash(message, "danger") 

243 return redirect(url_for('admin.eventDisplay', eventId = eventId)) 

244 

245 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

246 createActivityLog(f'''Renewed {priorEvent['name']} as <a href='event/{newEvent[0].id}/view'>{newEvent[0].name}</a>.''') 

247 flash("Event successfully renewed.", "success") 

248 return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id)) 

249 

250 

251 except Exception as e: 

252 print("Error while trying to renew event:", e) 

253 flash("There was an error renewing the event. Please try again or contact Systems Support.", 'danger') 

254 return redirect(url_for('admin.eventDisplay', eventId = eventId)) 

255 

256 

257 

258@admin_bp.route('/event/<eventId>/view', methods=['GET']) 

259@admin_bp.route('/event/<eventId>/edit', methods=['GET','POST']) 

260def eventDisplay(eventId): 

261 pageViewsCount = EventView.select().where(EventView.event == eventId).count() 

262 if request.method == 'GET' and request.path == f'/event/{eventId}/view': 

263 viewer = g.current_user 

264 event = Event.get_by_id(eventId) 

265 addEventView(viewer,event) 

266 # Validate given URL 

267 try: 

268 event = Event.get_by_id(eventId) 

269 invitedCohorts = list(EventCohort.select().where( 

270 EventCohort.event == event 

271 )) 

272 invitedYears = [str(cohort.year) for cohort in invitedCohorts] 

273 except DoesNotExist as e: 

274 print(f"Unknown event: {eventId}") 

275 abort(404) 

276 

277 notPermitted = not g.current_user.canManageProgram(event.program) 

278 if 'edit' in request.url_rule.rule and notPermitted: 

279 abort(403) 

280 

281 eventData = model_to_dict(event, recurse=False) 

282 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event) 

283 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments) 

284 

285 image = None 

286 picurestype = [".jpeg", ".png", ".gif", ".jpg", ".svg", ".webp"] 

287 for attachment in associatedAttachments: 

288 for extension in picurestype: 

289 if (attachment.fileName.endswith(extension) and attachment.isDisplayed == True): 

290 image = filepaths[attachment.fileName][0] 

291 if image: 

292 break 

293 

294 

295 if request.method == "POST": # Attempt to save form 

296 eventData = request.form.copy() 

297 try: 

298 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request)) 

299 

300 except IntegrityError as e: 

301 print("Error saving event:", e) 

302 savedEvents = False 

303 validationErrorMessage = "This combination of settings is not allowed." 

304 except Exception as e: 

305 print("Error saving event:", e) 

306 savedEvents = False 

307 validationErrorMessage = "Unknown Error Saving Event. Please try again" 

308 

309 

310 if savedEvents: 

311 rsvpCohorts = request.form.getlist("cohorts[]") 

312 updateEventCohorts(savedEvents[0], rsvpCohorts) 

313 flash("Event successfully updated!", "success") 

314 return redirect(url_for("admin.eventDisplay", eventId = event.id)) 

315 else: 

316 flash(validationErrorMessage, 'warning') 

317 

318 # make sure our data is the same regardless of GET and POST 

319 preprocessEventData(eventData) 

320 eventData['program'] = event.program 

321 userHasRSVPed = checkUserRsvp(g.current_user, event) 

322 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments) 

323 isProgramManager = g.current_user.isProgramManagerFor(eventData['program']) 

324 requirements, bonnerCohorts = [], [] 

325 

326 if eventData['program'] and eventData['program'].isBonnerScholars: 

327 requirements = getCertRequirements(Certification.BONNER) 

328 rawBonnerCohorts = getBonnerCohorts(limit=5) 

329 bonnerCohorts = {} 

330 

331 for year, cohort in rawBonnerCohorts.items(): 

332 if cohort: 

333 bonnerCohorts[year] = cohort 

334 

335 invitedCohorts = list(EventCohort.select().where( 

336 EventCohort.event_id == eventId, 

337 )) 

338 invitedYears = [str(cohort.year) for cohort in invitedCohorts] 

339 else: 

340 requirements, bonnerCohorts, invitedYears = [], [], [] 

341 

342 rule = request.url_rule 

343 

344 # Event Edit 

345 if 'edit' in rule.rule: 

346 event.isCampusGroupsSynced = False 

347 event.save(only=[Event.isCampusGroupsSynced]) 

348 return render_template("events/createEvent.html", 

349 eventData = eventData, 

350 termList = Term.select().order_by(Term.termOrder), 

351 event = event, 

352 requirements = requirements, 

353 bonnerCohorts = bonnerCohorts, 

354 invitedYears = invitedYears, 

355 userHasRSVPed = userHasRSVPed, 

356 isProgramManager = isProgramManager, 

357 filepaths = filepaths) 

358 # Event View 

359 else: 

360 # get text representations of dates for html 

361 eventData['timeStart'] = event.timeStart.strftime("%-I:%M %p") 

362 eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p") 

363 eventData['startDate'] = event.startDate.strftime("%m/%d/%Y") 

364 eventCountdown = getCountdownToEvent(event) 

365 

366 

367 # Identify the next event in a repeating series 

368 if event.seriesId: 

369 eventSeriesList = list(Event.select().where(Event.seriesId == event.seriesId) 

370 .where((Event.isCanceled == False) | (Event.id == event.id)) 

371 .order_by(Event.startDate)) 

372 eventIndex = eventSeriesList.index(event) 

373 if len(eventSeriesList) != (eventIndex + 1): 

374 eventData["nextRepeatingEvent"] = eventSeriesList[eventIndex + 1] 

375 

376 currentEventRsvpAmount = getEventRsvpCount(event.id) 

377 

378 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term, includeFutureEvents = False) 

379 

380 # CampusGroups integration  

381 if event.campusGroupsId: 

382 eventData["campusGroupsURL"] = event.campusGroupsURL 

383 eventData["campusGroupsId"] = event.campusGroupsId 

384 eventData["isCampusGroupsSynced"] = event.isCampusGroupsSynced 

385 return render_template("events/eventView.html", 

386 eventData=eventData, 

387 event=event, 

388 userHasRSVPed=userHasRSVPed, 

389 programTrainings=userParticipatedTrainingEvents, 

390 currentEventRsvpAmount=currentEventRsvpAmount, 

391 isProgramManager=isProgramManager, 

392 filepaths=filepaths, 

393 image=image, 

394 pageViewsCount=pageViewsCount, 

395 invitedYears=invitedYears, 

396 eventCountdown=eventCountdown 

397 ) 

398 

399 

400 

401@admin_bp.route('/event/<eventId>/cancel', methods=['POST']) 

402def cancelRoute(eventId): 

403 if g.current_user.isAdmin: 

404 try: 

405 cancelEvent(eventId) 

406 return redirect(request.referrer) 

407 

408 except Exception as e: 

409 print('Error while canceling event:', e) 

410 return "", 500 

411 

412 else: 

413 abort(403) 

414 

415@admin_bp.route('/profile/undo', methods=['GET']) 

416def undoBackgroundCheck(): 

417 try: 

418 username = g.current_user 

419 bgCheckId = session['lastDeletedBgCheck'] 

420 BackgroundCheck.update({BackgroundCheck.deletionDate: None, BackgroundCheck.deletedBy: None}).where(BackgroundCheck.id == bgCheckId).execute() 

421 flash("Background Check has been successfully restored.", "success") 

422 return redirect (f"/profile/{username}?accordion=background") 

423 except Exception as e: 

424 print('Error while undoing background check:', e) 

425 return "", 500 

426 

427@admin_bp.route('/event/undo', methods=['GET']) 

428def undoEvent(): 

429 try: 

430 eventIds = session['lastDeletedEvent'] #list of Ids of the events that got deleted 

431 for eventId in eventIds: 

432 Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute() 

433 event = Event.get_or_none(Event.id == eventId) 

434 repeatingEvents = list(Event.select().where((Event.seriesId == event.seriesId) & (Event.isRepeating) & (Event.deletionDate == None)).order_by(Event.id)) 

435 if event.isRepeating: 

436 nameCounter = 1 

437 for repeatingEvent in repeatingEvents: 

438 newEventNameList = repeatingEvent.name.split() 

439 newEventNameList[-1] = f"{nameCounter}" 

440 newEventNameList = " ".join(newEventNameList) 

441 Event.update({Event.name: newEventNameList}).where(Event.id==repeatingEvent.id).execute() 

442 nameCounter += 1 

443 flash("Event has been successfully restored.", "success") 

444 return redirect(url_for("main.events", selectedTerm=g.current_term)) 

445 except Exception as e: 

446 print('Error while canceling event:', e) 

447 return "", 500 

448 

449@admin_bp.route('/event/<eventId>/delete', methods=['POST']) 

450def deleteRoute(eventId): 

451 try: 

452 deleteEvent(eventId) 

453 session['lastDeletedEvent'] = [eventId] 

454 flash("Event successfully deleted.", "success") 

455 return redirect(url_for("main.events", selectedTerm=g.current_term)) 

456 

457 except Exception as e: 

458 print('Error while canceling event:', e) 

459 return "", 500 

460 

461@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST']) 

462def deleteEventAndAllFollowingRoute(eventId): 

463 try: 

464 session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId) 

465 flash("Events successfully deleted.", "success") 

466 return redirect(url_for("main.events", selectedTerm=g.current_term)) 

467 

468 except Exception as e: 

469 print('Error while canceling event:', e) 

470 return "", 500 

471 

472@admin_bp.route('/event/<eventId>/deleteAllEventsInSeries', methods=['POST']) 

473def deleteAllEventsInSeriesRoute(eventId): 

474 try: 

475 session["lastDeletedEvent"] = deleteAllEventsInSeries(eventId) 

476 flash("Events successfully deleted.", "success") 

477 return redirect(url_for("main.events", selectedTerm=g.current_term)) 

478 

479 except Exception as e: 

480 print('Error while canceling event:', e) 

481 return "", 500 

482 

483@admin_bp.route('/makeRepeatingEvents', methods=['POST']) 

484def addRepeatingEvents(): 

485 repeatingEvents = getRepeatingEventsData(preprocessEventData(request.form.copy())) 

486 return json.dumps(repeatingEvents, default=str) 

487 

488 

489@admin_bp.route('/userProfile', methods=['POST']) 

490def userProfile(): 

491 volunteerName= request.form.copy() 

492 if volunteerName['searchStudentsInput']: 

493 username = volunteerName['searchStudentsInput'].strip("()") 

494 user=username.split('(')[-1] 

495 return redirect(url_for('main.viewUsersProfile', username=user)) 

496 else: 

497 flash(f"Please enter the first name or the username of the student you would like to search for.", category='danger') 

498 return redirect(url_for('admin.studentSearchPage')) 

499 

500@admin_bp.route('/search_student', methods=['GET']) 

501def studentSearchPage(): 

502 if g.current_user.isAdmin: 

503 return render_template("/admin/searchStudentPage.html") 

504 abort(403) 

505 

506@admin_bp.route('/activityLogs', methods = ['GET', 'POST']) 

507def activityLogs(): 

508 if g.current_user.isCeltsAdmin: 

509 allLogs = ActivityLog.select(ActivityLog, User).join(User).order_by(ActivityLog.createdOn.desc()) 

510 return render_template("/admin/activityLogs.html", 

511 allLogs = allLogs) 

512 else: 

513 abort(403) 

514 

515@admin_bp.route("/deleteEventFile", methods=["POST"]) 

516def deleteEventFile(): 

517 fileData= request.form 

518 eventfile=FileHandler(eventId=fileData["databaseId"]) 

519 eventfile.deleteFile(fileData["fileId"]) 

520 return "" 

521 

522@admin_bp.route("/uploadCourseParticipant", methods= ["POST"]) 

523def addCourseFile(): 

524 fileData = request.files['addCourseParticipants'] 

525 filePath = os.path.join(app.config["files"]["base_path"], fileData.filename) 

526 fileData.save(filePath) 

527 (session['cpPreview'], session['cpErrors']) = parseUploadedFile(filePath) 

528 os.remove(filePath) 

529 return redirect(url_for("admin.manageServiceLearningCourses")) 

530 

531@admin_bp.route('/manageServiceLearning', methods = ['GET', 'POST']) 

532@admin_bp.route('/manageServiceLearning/<term>', methods = ['GET', 'POST']) 

533def manageServiceLearningCourses(term=None): 

534 

535 """ 

536 The SLC management page for admins 

537 """ 

538 if not g.current_user.isCeltsAdmin: 

539 abort(403) 

540 

541 if request.method == 'POST' and "submitParticipant" in request.form: 

542 saveCourseParticipantsToDatabase(session.pop('cpPreview', {})) 

543 flash('Courses and participants saved successfully!', 'success') 

544 return redirect(url_for('admin.manageServiceLearningCourses')) 

545 

546 manageTerm = Term.get_or_none(Term.id == term) or g.current_term 

547 

548 setRedirectTarget(request.full_path) 

549 # retrieve and store the courseID of the imported course from a session variable if it exists.  

550 # This allows us to export the courseID in the html and use it. 

551 courseID = session.get("alterCourseId") 

552 

553 if courseID: 

554 # delete courseID from the session if it was retrieved, for storage purposes. 

555 session.pop("alterCourseId") 

556 return render_template('/admin/manageServiceLearningFaculty.html', 

557 courseInstructors = getInstructorCourses(), 

558 unapprovedCourses = unapprovedCourses(manageTerm), 

559 approvedCourses = approvedCourses(manageTerm), 

560 importedCourses = getImportedCourses(manageTerm), 

561 terms = selectSurroundingTerms(g.current_term), 

562 term = manageTerm, 

563 cpPreview = session.get('cpPreview', {}), 

564 cpPreviewErrors = session.get('cpErrors', []), 

565 courseID = courseID 

566 ) 

567 

568 return render_template('/admin/manageServiceLearningFaculty.html', 

569 courseInstructors = getInstructorCourses(), 

570 unapprovedCourses = unapprovedCourses(manageTerm), 

571 approvedCourses = approvedCourses(manageTerm), 

572 importedCourses = getImportedCourses(manageTerm), 

573 terms = selectSurroundingTerms(g.current_term), 

574 term = manageTerm, 

575 cpPreview= session.get('cpPreview',{}), 

576 cpPreviewErrors = session.get('cpErrors',[]) 

577 ) 

578 

579@admin_bp.route('/admin/getSidebarInformation', methods=['GET']) 

580def getSidebarInformation() -> str: 

581 """ 

582 Get the count of unapproved courses and students interested in the minor for the current term  

583 to display in the admin sidebar. It must be returned as a string to be received by the 

584 ajax request. 

585 """ 

586 unapprovedCoursesCount: int = len(unapprovedCourses(g.current_term)) 

587 interestedStudentsCount: int = len(getMinorInterest()) 

588 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

589 "interestedStudentsCount": interestedStudentsCount} 

590 

591@admin_bp.route("/deleteUploadedFile", methods= ["POST"]) 

592def removeFromSession(): 

593 try: 

594 session.pop('cpPreview') 

595 except KeyError: 

596 pass 

597 

598 return "" 

599 

600@admin_bp.route('/manageServiceLearning/imported/<courseID>', methods = ['POST', 'GET']) 

601def alterImportedCourse(courseID): 

602 """ 

603 This route handles a GET and a POST request for the purpose of imported courses.  

604 The GET request provides preexisting information of an imported course in a modal.  

605 The POST request updates a specific imported course (course name, course abbreviation,  

606 hours earned on completion, list of instructors) in the database with new information  

607 coming from the imported courses modal.  

608 """ 

609 if request.method == 'GET': 

610 try: 

611 targetCourse = Course.get_by_id(courseID) 

612 targetInstructors = CourseInstructor.select().where(CourseInstructor.course == targetCourse) 

613 

614 try: 

615 serviceHours = list(CourseParticipant.select().where(CourseParticipant.course_id == targetCourse.id))[0].hoursEarned 

616 except IndexError: # If a course has no participant, IndexError will be raised 

617 serviceHours = 20 

618 

619 courseData = model_to_dict(targetCourse, recurse=False) 

620 courseData['instructors'] = [model_to_dict(instructor.user) for instructor in targetInstructors] 

621 courseData['hoursEarned'] = serviceHours 

622 

623 return jsonify(courseData) 

624 

625 except DoesNotExist: 

626 flash("Course not found") 

627 return jsonify({"error": "Course not found"}), 404 

628 

629 if request.method == 'POST': 

630 # Update course information in the database 

631 courseData = request.form.copy() 

632 editImportedCourses(courseData) 

633 session['alterCourseId'] = courseID 

634 

635 return redirect(url_for("admin.manageServiceLearningCourses", term=courseData['termId'])) 

636 

637 

638@admin_bp.route("/manageBonner") 

639def manageBonner(): 

640 if not g.current_user.isCeltsAdmin: 

641 abort(403) 

642 

643 return render_template("/admin/bonnerManagement.html", 

644 cohorts=getBonnerCohorts(), 

645 events=getBonnerEvents(g.current_term), 

646 requirements=getCertRequirements(certification=Certification.BONNER)) 

647 

648@admin_bp.route("/bonner/<year>/<method>/<username>", methods=["POST"]) 

649def updatecohort(year, method, username): 

650 if not g.current_user.isCeltsAdmin: 

651 abort(403) 

652 

653 try: 

654 user = User.get_by_id(username) 

655 except: 

656 abort(500) 

657 

658 if method == "add": 

659 try: 

660 BonnerCohort.create(year=year, user=user) 

661 flash(f"Successfully added {user.fullName} to {year} Bonner Cohort.", "success") 

662 except IntegrityError as e: 

663 # if they already exist, ignore the error 

664 flash(f'Error: {user.fullName} already added.', "danger") 

665 pass 

666 

667 elif method == "remove": 

668 BonnerCohort.delete().where(BonnerCohort.user == user, BonnerCohort.year == year).execute() 

669 flash(f"Successfully removed {user.fullName} from {year} Bonner Cohort.", "success") 

670 else: 

671 flash(f"Error: {user.fullName} can't be added.", "danger") 

672 abort(500) 

673 return "" 

674 

675@admin_bp.route("/bonnerXls/<startingYear>/<noOfYears>") 

676def getBonnerXls(startingYear, noOfYears): 

677 if not g.current_user.isCeltsAdmin: 

678 abort(403) 

679 newfile = makeBonnerXls(startingYear, noOfYears) 

680 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True) 

681 

682 

683@admin_bp.route("/saveRequirements/<certid>", methods=["POST"]) 

684def saveRequirements(certid): 

685 if not g.current_user.isCeltsAdmin: 

686 abort(403) 

687 

688 newRequirements = updateCertRequirements(certid, request.get_json()) 

689 

690 return jsonify([requirement.id for requirement in newRequirements]) 

691 

692 

693@admin_bp.route("/displayEventFile", methods=["POST"]) 

694def displayEventFile(): 

695 fileData = request.form 

696 eventfile = FileHandler(eventId=fileData["id"]) 

697 isChecked = fileData.get('checked') == 'true' 

698 eventfile.changeDisplay(fileData['id'], isChecked) 

699 return "" 

700 

701@admin_bp.route("/handbookSignature", methods=["POST"]) 

702def handbookSignature(): 

703 if not (g.current_user.username == request.form.get('studentUsername')): 

704 abort(403) 

705 

706 signer = g.current_user 

707 signer.lastHandbookSignature = datetime.now() 

708 signer.signatureTerm = g.current_term 

709 signer.save() 

710 

711 return ""