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

474 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2026-08-24 19:35 +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 return render_template("events/createEvent.html", 

347 eventData = eventData, 

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

349 event = event, 

350 requirements = requirements, 

351 bonnerCohorts = bonnerCohorts, 

352 invitedYears = invitedYears, 

353 userHasRSVPed = userHasRSVPed, 

354 isProgramManager = isProgramManager, 

355 filepaths = filepaths) 

356 # Event View 

357 else: 

358 # get text representations of dates for html 

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

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

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

362 eventCountdown = getCountdownToEvent(event) 

363 

364 

365 # Identify the next event in a repeating series 

366 if event.seriesId: 

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

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

369 .order_by(Event.startDate)) 

370 eventIndex = eventSeriesList.index(event) 

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

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

373 

374 currentEventRsvpAmount = getEventRsvpCount(event.id) 

375 

376 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term) 

377 

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

379 eventData=eventData, 

380 event=event, 

381 userHasRSVPed=userHasRSVPed, 

382 programTrainings=userParticipatedTrainingEvents, 

383 currentEventRsvpAmount=currentEventRsvpAmount, 

384 isProgramManager=isProgramManager, 

385 filepaths=filepaths, 

386 image=image, 

387 pageViewsCount=pageViewsCount, 

388 invitedYears=invitedYears, 

389 eventCountdown=eventCountdown 

390 ) 

391 

392 

393 

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

395def cancelRoute(eventId): 

396 if g.current_user.isAdmin: 

397 try: 

398 cancelEvent(eventId) 

399 return redirect(request.referrer) 

400 

401 except Exception as e: 

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

403 return "", 500 

404 

405 else: 

406 abort(403) 

407 

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

409def undoBackgroundCheck(): 

410 try: 

411 username = g.current_user 

412 bgCheckId = session['lastDeletedBgCheck'] 

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

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

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

416 except Exception as e: 

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

418 return "", 500 

419 

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

421def undoEvent(): 

422 try: 

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

424 for eventId in eventIds: 

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

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

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

428 if event.isRepeating: 

429 nameCounter = 1 

430 for repeatingEvent in repeatingEvents: 

431 newEventNameList = repeatingEvent.name.split() 

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

433 newEventNameList = " ".join(newEventNameList) 

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

435 nameCounter += 1 

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

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

438 except Exception as e: 

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

440 return "", 500 

441 

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

443def deleteRoute(eventId): 

444 try: 

445 deleteEvent(eventId) 

446 session['lastDeletedEvent'] = [eventId] 

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

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

449 

450 except Exception as e: 

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

452 return "", 500 

453 

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

455def deleteEventAndAllFollowingRoute(eventId): 

456 try: 

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

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

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

460 

461 except Exception as e: 

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

463 return "", 500 

464 

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

466def deleteAllEventsInSeriesRoute(eventId): 

467 try: 

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

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

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

471 

472 except Exception as e: 

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

474 return "", 500 

475 

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

477def addRepeatingEvents(): 

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

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

480 

481 

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

483def userProfile(): 

484 volunteerName= request.form.copy() 

485 if volunteerName['searchStudentsInput']: 

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

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

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

489 else: 

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

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

492 

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

494def studentSearchPage(): 

495 if g.current_user.isAdmin: 

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

497 abort(403) 

498 

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

500def activityLogs(): 

501 if g.current_user.isCeltsAdmin: 

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

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

504 allLogs = allLogs) 

505 else: 

506 abort(403) 

507 

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

509def deleteEventFile(): 

510 fileData= request.form 

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

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

513 return "" 

514 

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

516def addCourseFile(): 

517 fileData = request.files['addCourseParticipants'] 

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

519 fileData.save(filePath) 

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

521 os.remove(filePath) 

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

523 

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

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

526def manageServiceLearningCourses(term=None): 

527 

528 """ 

529 The SLC management page for admins 

530 """ 

531 if not g.current_user.isCeltsAdmin: 

532 abort(403) 

533 

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

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

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

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

538 

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

540 

541 setRedirectTarget(request.full_path) 

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

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

544 courseID = session.get("alterCourseId") 

545 

546 if courseID: 

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

548 session.pop("alterCourseId") 

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

550 courseInstructors = getInstructorCourses(), 

551 unapprovedCourses = unapprovedCourses(manageTerm), 

552 approvedCourses = approvedCourses(manageTerm), 

553 importedCourses = getImportedCourses(manageTerm), 

554 terms = selectSurroundingTerms(g.current_term), 

555 term = manageTerm, 

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

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

558 courseID = courseID 

559 ) 

560 

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

562 courseInstructors = getInstructorCourses(), 

563 unapprovedCourses = unapprovedCourses(manageTerm), 

564 approvedCourses = approvedCourses(manageTerm), 

565 importedCourses = getImportedCourses(manageTerm), 

566 terms = selectSurroundingTerms(g.current_term), 

567 term = manageTerm, 

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

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

570 ) 

571 

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

573def getSidebarInformation() -> str: 

574 """ 

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

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

577 ajax request. 

578 """ 

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

580 interestedStudentsCount: int = len(getMinorInterest()) 

581 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

582 "interestedStudentsCount": interestedStudentsCount} 

583 

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

585def removeFromSession(): 

586 try: 

587 session.pop('cpPreview') 

588 except KeyError: 

589 pass 

590 

591 return "" 

592 

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

594def alterImportedCourse(courseID): 

595 """ 

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

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

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

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

600 coming from the imported courses modal.  

601 """ 

602 if request.method == 'GET': 

603 try: 

604 targetCourse = Course.get_by_id(courseID) 

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

606 

607 try: 

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

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

610 serviceHours = 20 

611 

612 courseData = model_to_dict(targetCourse, recurse=False) 

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

614 courseData['hoursEarned'] = serviceHours 

615 

616 return jsonify(courseData) 

617 

618 except DoesNotExist: 

619 flash("Course not found") 

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

621 

622 if request.method == 'POST': 

623 # Update course information in the database 

624 courseData = request.form.copy() 

625 editImportedCourses(courseData) 

626 session['alterCourseId'] = courseID 

627 

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

629 

630 

631@admin_bp.route("/manageBonner") 

632def manageBonner(): 

633 if not g.current_user.isCeltsAdmin: 

634 abort(403) 

635 

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

637 cohorts=getBonnerCohorts(), 

638 events=getBonnerEvents(g.current_term), 

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

640 

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

642def updatecohort(year, method, username): 

643 if not g.current_user.isCeltsAdmin: 

644 abort(403) 

645 

646 try: 

647 user = User.get_by_id(username) 

648 except: 

649 abort(500) 

650 

651 if method == "add": 

652 try: 

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

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

655 except IntegrityError as e: 

656 # if they already exist, ignore the error 

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

658 pass 

659 

660 elif method == "remove": 

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

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

663 else: 

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

665 abort(500) 

666 return "" 

667 

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

669def getBonnerXls(startingYear, noOfYears): 

670 if not g.current_user.isCeltsAdmin: 

671 abort(403) 

672 newfile = makeBonnerXls(startingYear, noOfYears) 

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

674 

675 

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

677def saveRequirements(certid): 

678 if not g.current_user.isCeltsAdmin: 

679 abort(403) 

680 

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

682 

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

684 

685 

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

687def displayEventFile(): 

688 fileData = request.form 

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

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

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

692 return "" 

693 

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

695def handbookSignature(): 

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

697 abort(403) 

698 

699 signer = g.current_user 

700 signer.lastHandbookSignature = datetime.now() 

701 signer.signatureTerm = g.current_term 

702 signer.save() 

703 

704 return ""