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

432 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-11-19 23:53 +0000

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

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

3from peewee import DoesNotExist, fn, IntegrityError 

4from playhouse.shortcuts import model_to_dict 

5import json 

6from datetime import datetime 

7import os 

8 

9from app import app 

10from app.models.program import Program 

11from app.models.event import Event 

12from app.models.eventRsvp import EventRsvp 

13from app.models.eventParticipant import EventParticipant 

14from app.models.user import User 

15from app.models.course import Course 

16from app.models.courseInstructor import CourseInstructor 

17from app.models.courseParticipant import CourseParticipant 

18from app.models.eventTemplate import EventTemplate 

19from app.models.activityLog import ActivityLog 

20from app.models.eventRsvpLog import EventRsvpLog 

21from app.models.attachmentUpload import AttachmentUpload 

22from app.models.bonnerCohort import BonnerCohort 

23from app.models.certification import Certification 

24from app.models.user import User 

25from app.models.term import Term 

26from app.models.eventViews import EventView 

27from app.models.courseStatus import CourseStatus 

28 

29from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates 

30from app.logic.createLogs import createActivityLog 

31from app.logic.certification import getCertRequirements, updateCertRequirements 

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

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

34from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp 

35from app.logic.minor import getMinorInterest 

36from app.logic.fileHandler import FileHandler 

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

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

39 

40from app.controllers.admin import admin_bp 

41from app.logic.spreadsheet import createSpreadsheet 

42 

43 

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

45def reports(): 

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

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

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

49 

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

51def downloadFile(): 

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

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

54 return send_file(filepath, as_attachment=True) 

55 

56 

57 

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

59def switchUser(): 

60 if app.env == "production": 

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

62 abort(403) 

63 

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

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

66 

67 return redirect(request.referrer) 

68 

69 

70@admin_bp.route('/eventTemplates') 

71def templateSelect(): 

72 if g.current_user.isCeltsAdmin or g.current_user.isCeltsStudentStaff: 

73 allprograms = getAllowedPrograms(g.current_user) 

74 visibleTemplates = getAllowedTemplates(g.current_user) 

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

76 programs=allprograms, 

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

78 templates=visibleTemplates) 

79 else: 

80 abort(403) 

81 

82 

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

84def createEvent(templateid, programid): 

85 if not (g.current_user.isAdmin or g.current_user.isProgramManagerFor(programid)): 

86 abort(403) 

87 

88 # Validate given URL 

89 program = None 

90 try: 

91 template = EventTemplate.get_by_id(templateid) 

92 if programid: 

93 program = Program.get_by_id(programid) 

94 except DoesNotExist as e: 

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

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

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

98 

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

100 eventData = template.templateData 

101 eventData['program'] = program 

102 

103 if request.method == "GET": 

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

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

106 if program: 

107 eventData['location'] = program.defaultLocation 

108 if program.contactName: 

109 eventData['contactName'] = program.contactName 

110 if program.contactEmail: 

111 eventData['contactEmail'] = program.contactEmail 

112 

113 # Try to save the form 

114 if request.method == "POST": 

115 savedEvents = None 

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

117 eventData = preprocessEventData(eventData) 

118 if eventData.get('isSeries'): 

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

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

121 if not succeeded: 

122 for index, validationErrorMessage in failedSavedOfferings: 

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

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

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

126 else: 

127 try: 

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

129 except Exception as e: 

130 print("Failed saving regular event", e) 

131 validationErrorMessage = "Failed to save event." 

132 

133 if savedEvents: 

134 rsvpcohorts = request.form.getlist("cohorts[]") 

135 for year in rsvpcohorts: 

136 rsvpForBonnerCohort(int(year), savedEvents[0].id) 

137 addBonnerCohortToRsvpLog(int(year), savedEvents[0].id) 

138 

139 

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

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

142 

143 

144 if program: 

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

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

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

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

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

150 

151 if len(savedEvents) > 1: 

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

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

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

155 lastEventDate = eventDates[-1] 

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

157 

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

159 

160 else: 

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

162 else: 

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

164 

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

166 else: 

167 flash(validationErrorMessage, 'warning') 

168 

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

170 preprocessEventData(eventData) 

171 isProgramManager = g.current_user.isProgramManagerFor(programid) 

172 

173 futureTerms = selectSurroundingTerms(g.current_term, prevTerms=0) 

174 

175 requirements, bonnerCohorts = [], [] 

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

177 requirements = getCertRequirements(Certification.BONNER) 

178 bonnerCohorts = getBonnerCohorts(limit=5) 

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

180 template = template, 

181 eventData = eventData, 

182 futureTerms = futureTerms, 

183 requirements = requirements, 

184 bonnerCohorts = bonnerCohorts, 

185 isProgramManager = isProgramManager) 

186 

187 

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

189def rsvpLogDisplay(eventId): 

190 event = Event.get_by_id(eventId) 

191 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)): 

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

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

194 event = event, 

195 allLogs = allLogs) 

196 else: 

197 abort(403) 

198 

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

200def renewEvent(eventId): 

201 try: 

202 formData = request.form 

203 try: 

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

205 except AssertionError: 

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

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

208 

209 try: 

210 if formData.get('dateEnd'): 

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

212 except AssertionError: 

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

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

215 

216 

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

218 newEventDict = priorEvent.copy() 

219 newEventDict.pop('id') 

220 newEventDict.update({ 

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

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

223 'timeStart': formData['timeStart'], 

224 'timeEnd': formData['timeEnd'], 

225 'location': formData['location'], 

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

227 'endDate': f'{formData["endDate"][-4:]}-{formData["endDate"][0:-5]}', 

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

229 'seriesId': priorEvent['seriesId'], 

230 }) 

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

232 if message: 

233 flash(message, "danger") 

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

235 

236 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

240 

241 

242 except Exception as e: 

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

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

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

246 

247 

248 

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

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

251def eventDisplay(eventId): 

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

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

254 viewer = g.current_user 

255 event = Event.get_by_id(eventId) 

256 addEventView(viewer,event) 

257 # Validate given URL 

258 try: 

259 event = Event.get_by_id(eventId) 

260 except DoesNotExist as e: 

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

262 abort(404) 

263 

264 notPermitted = not (g.current_user.isCeltsAdmin or g.current_user.isProgramManagerForEvent(event)) 

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

266 abort(403) 

267 

268 eventData = model_to_dict(event, recurse=False) 

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

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

271 

272 image = None 

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

274 for attachment in associatedAttachments: 

275 for extension in picurestype: 

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

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

278 if image: 

279 break 

280 

281 

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

283 eventData = request.form.copy() 

284 try: 

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

286 

287 except Exception as e: 

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

289 savedEvents = False 

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

291 

292 

293 if savedEvents: 

294 rsvpcohorts = request.form.getlist("cohorts[]") 

295 for year in rsvpcohorts: 

296 rsvpForBonnerCohort(int(year), event.id) 

297 addBonnerCohortToRsvpLog(int(year), event.id) 

298 

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

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

301 else: 

302 flash(validationErrorMessage, 'warning') 

303 

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

305 preprocessEventData(eventData) 

306 eventData['program'] = event.program 

307 futureTerms = selectSurroundingTerms(g.current_term) 

308 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

311 requirements, bonnerCohorts = [], [] 

312 

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

314 requirements = getCertRequirements(Certification.BONNER) 

315 bonnerCohorts = getBonnerCohorts(limit=5) 

316 

317 rule = request.url_rule 

318 

319 # Event Edit 

320 if 'edit' in rule.rule: 

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

322 eventData = eventData, 

323 futureTerms=futureTerms, 

324 event = event, 

325 requirements = requirements, 

326 bonnerCohorts = bonnerCohorts, 

327 userHasRSVPed = userHasRSVPed, 

328 isProgramManager = isProgramManager, 

329 filepaths = filepaths) 

330 # Event View 

331 else: 

332 # get text representations of dates for html 

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

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

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

336 eventCountdown = getCountdownToEvent(event) 

337 

338 

339 # Identify the next event in a repeating series 

340 if event.isRepeating: 

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

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

343 .order_by(Event.startDate)) 

344 eventIndex = eventSeriesList.index(event) 

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

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

347 

348 currentEventRsvpAmount = getEventRsvpCount(event.id) 

349 

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

351 

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

353 eventData=eventData, 

354 event=event, 

355 userHasRSVPed=userHasRSVPed, 

356 programTrainings=userParticipatedTrainingEvents, 

357 currentEventRsvpAmount=currentEventRsvpAmount, 

358 isProgramManager=isProgramManager, 

359 filepaths=filepaths, 

360 image=image, 

361 pageViewsCount=pageViewsCount, 

362 eventCountdown=eventCountdown 

363 ) 

364 

365 

366 

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

368def cancelRoute(eventId): 

369 if g.current_user.isAdmin: 

370 try: 

371 cancelEvent(eventId) 

372 return redirect(request.referrer) 

373 

374 except Exception as e: 

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

376 return "", 500 

377 

378 else: 

379 abort(403) 

380 

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

382def undoEvent(): 

383 try: 

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

385 for eventId in eventIds: 

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

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

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

389 if event.isRepeating: 

390 nameCounter = 1 

391 for repeatingEvent in repeatingEvents: 

392 newEventNameList = repeatingEvent.name.split() 

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

394 newEventNameList = " ".join(newEventNameList) 

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

396 nameCounter += 1 

397 flash("Deletion successfully undone.", "success") 

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

399 

400 except Exception as e: 

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

402 return "", 500 

403 

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

405def deleteRoute(eventId): 

406 try: 

407 deleteEvent(eventId) 

408 session['lastDeletedEvent'] = [eventId] 

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

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

411 

412 except Exception as e: 

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

414 return "", 500 

415 

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

417def deleteEventAndAllFollowingRoute(eventId): 

418 try: 

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

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

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

422 

423 except Exception as e: 

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

425 return "", 500 

426 

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

428def deleteAllEventsInSeriesRoute(eventId): 

429 try: 

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

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

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

433 

434 except Exception as e: 

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

436 return "", 500 

437 

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

439def addRepeatingEvents(): 

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

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

442 

443 

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

445def userProfile(): 

446 volunteerName= request.form.copy() 

447 if volunteerName['searchStudentsInput']: 

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

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

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

451 else: 

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

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

454 

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

456def studentSearchPage(): 

457 if g.current_user.isAdmin: 

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

459 abort(403) 

460 

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

462def activityLogs(): 

463 if g.current_user.isCeltsAdmin: 

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

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

466 allLogs = allLogs) 

467 else: 

468 abort(403) 

469 

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

471def deleteEventFile(): 

472 fileData= request.form 

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

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

475 return "" 

476 

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

478def addCourseFile(): 

479 fileData = request.files['addCourseParticipants'] 

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

481 fileData.save(filePath) 

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

483 os.remove(filePath) 

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

485 

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

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

488def manageServiceLearningCourses(term=None): 

489 

490 """ 

491 The SLC management page for admins 

492 """ 

493 if not g.current_user.isCeltsAdmin: 

494 abort(403) 

495 

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

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

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

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

500 

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

502 

503 setRedirectTarget(request.full_path) 

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

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

506 courseID = session.get("alterCourseId") 

507 

508 if courseID: 

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

510 session.pop("alterCourseId") 

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

512 courseInstructors = getInstructorCourses(), 

513 unapprovedCourses = unapprovedCourses(manageTerm), 

514 approvedCourses = approvedCourses(manageTerm), 

515 importedCourses = getImportedCourses(manageTerm), 

516 terms = selectSurroundingTerms(g.current_term), 

517 term = manageTerm, 

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

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

520 courseID = courseID 

521 ) 

522 

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

524 courseInstructors = getInstructorCourses(), 

525 unapprovedCourses = unapprovedCourses(manageTerm), 

526 approvedCourses = approvedCourses(manageTerm), 

527 importedCourses = getImportedCourses(manageTerm), 

528 terms = selectSurroundingTerms(g.current_term), 

529 term = manageTerm, 

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

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

532 ) 

533 

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

535def getSidebarInformation() -> str: 

536 """ 

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

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

539 ajax request. 

540 """ 

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

542 interestedStudentsCount: int = len(getMinorInterest()) 

543 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

544 "interestedStudentsCount": interestedStudentsCount} 

545 

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

547def removeFromSession(): 

548 try: 

549 session.pop('cpPreview') 

550 except KeyError: 

551 pass 

552 

553 return "" 

554 

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

556def alterImportedCourse(courseID): 

557 """ 

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

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

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

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

562 coming from the imported courses modal.  

563 """ 

564 if request.method == 'GET': 

565 try: 

566 targetCourse = Course.get_by_id(courseID) 

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

568 

569 try: 

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

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

572 serviceHours = 20 

573 

574 courseData = model_to_dict(targetCourse, recurse=False) 

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

576 courseData['hoursEarned'] = serviceHours 

577 

578 return jsonify(courseData) 

579 

580 except DoesNotExist: 

581 flash("Course not found") 

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

583 

584 if request.method == 'POST': 

585 # Update course information in the database 

586 courseData = request.form.copy() 

587 editImportedCourses(courseData) 

588 session['alterCourseId'] = courseID 

589 

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

591 

592 

593@admin_bp.route("/manageBonner") 

594def manageBonner(): 

595 if not g.current_user.isCeltsAdmin: 

596 abort(403) 

597 

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

599 cohorts=getBonnerCohorts(), 

600 events=getBonnerEvents(g.current_term), 

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

602 

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

604def updatecohort(year, method, username): 

605 if not g.current_user.isCeltsAdmin: 

606 abort(403) 

607 

608 try: 

609 user = User.get_by_id(username) 

610 except: 

611 abort(500) 

612 

613 if method == "add": 

614 try: 

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

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

617 except IntegrityError as e: 

618 # if they already exist, ignore the error 

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

620 pass 

621 

622 elif method == "remove": 

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

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

625 else: 

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

627 abort(500) 

628 

629 return "" 

630 

631@admin_bp.route("/bonnerxls") 

632def bonnerxls(): 

633 if not g.current_user.isCeltsAdmin: 

634 abort(403) 

635 

636 newfile = makeBonnerXls() 

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

638 

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

640def saveRequirements(certid): 

641 if not g.current_user.isCeltsAdmin: 

642 abort(403) 

643 

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

645 

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

647 

648 

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

650def displayEventFile(): 

651 fileData = request.form 

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

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

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

655 return ""