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

432 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-11-22 21:05 +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, getRecurringEventsData, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId 

34from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp 

35from app.logic.minor import getMinorInterest 

36from app.logic.fileHandler import FileHandler 

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

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

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('isMultipleOffering'): 

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

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

121 if not succeeded: 

122 for index, validationErrorMessage in failedSavedOfferings: 

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

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

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

126 else: 

127 try: 

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

129 except Exception as e: 

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

131 validationErrorMessage = "Failed to save event." 

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('isRecurring') or eventData.get('isMultipleOffering')) 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('isRecurring'): 

146 createActivityLog(f"Created a recurring event, <a href=\"{url_for('admin.eventDisplay', eventId = savedEvents[0].id)}\">{savedEvents[0].name}</a>, for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}. The last event in the series will be on {datetime.strftime(savedEvents[-1].startDate, '%m/%d/%Y')}.") 

147 

148 elif len(savedEvents) >= 1 and eventData.get('isMultipleOffering'): 

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

150 

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

152 

153 if len(savedEvents) > 1: 

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

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

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

157 lastEventDate = eventDates[-1] 

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

159 

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

161 

162 else: 

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

164 else: 

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

166 

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

168 else: 

169 flash(validationErrorMessage, 'warning') 

170 

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

172 preprocessEventData(eventData) 

173 isProgramManager = g.current_user.isProgramManagerFor(programid) 

174 

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

176 

177 requirements, bonnerCohorts = [], [] 

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

179 requirements = getCertRequirements(Certification.BONNER) 

180 bonnerCohorts = getBonnerCohorts(limit=5) 

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

182 template = template, 

183 eventData = eventData, 

184 futureTerms = futureTerms, 

185 requirements = requirements, 

186 bonnerCohorts = bonnerCohorts, 

187 isProgramManager = isProgramManager) 

188 

189 

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

191def rsvpLogDisplay(eventId): 

192 event = Event.get_by_id(eventId) 

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

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

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

196 event = event, 

197 allLogs = allLogs) 

198 else: 

199 abort(403) 

200 

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

202def renewEvent(eventId): 

203 try: 

204 formData = request.form 

205 try: 

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

207 except AssertionError: 

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

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

210 

211 try: 

212 if formData.get('dateEnd'): 

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

214 except AssertionError: 

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

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

217 

218 

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

220 newEventDict = priorEvent.copy() 

221 newEventDict.pop('id') 

222 newEventDict.update({ 

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

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

225 'timeStart': formData['timeStart'], 

226 'timeEnd': formData['timeEnd'], 

227 'location': formData['location'], 

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

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

230 'isRecurring': bool(priorEvent['recurringId']), 

231 'isMultipleOffering': bool(priorEvent['multipleOfferingId']), 

232 }) 

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

234 if message: 

235 flash(message, "danger") 

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

237 

238 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

242 

243 

244 except Exception as e: 

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

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

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

248 

249 

250 

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

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

253def eventDisplay(eventId): 

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

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

256 viewer = g.current_user 

257 event = Event.get_by_id(eventId) 

258 addEventView(viewer,event) 

259 # Validate given URL 

260 try: 

261 event = Event.get_by_id(eventId) 

262 except DoesNotExist as e: 

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

264 abort(404) 

265 

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

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

268 abort(403) 

269 

270 eventData = model_to_dict(event, recurse=False) 

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

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

273 

274 image = None 

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

276 for attachment in associatedAttachments: 

277 for extension in picurestype: 

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

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

280 if image: 

281 break 

282 

283 

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

285 eventData = request.form.copy() 

286 try: 

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

288 

289 except Exception as e: 

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

291 savedEvents = False 

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

293 

294 

295 if savedEvents: 

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

297 for year in rsvpcohorts: 

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

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

300 

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

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

303 else: 

304 flash(validationErrorMessage, 'warning') 

305 

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

307 preprocessEventData(eventData) 

308 eventData['program'] = event.program 

309 futureTerms = selectSurroundingTerms(g.current_term) 

310 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

313 requirements, bonnerCohorts = [], [] 

314 

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

316 requirements = getCertRequirements(Certification.BONNER) 

317 bonnerCohorts = getBonnerCohorts(limit=5) 

318 

319 rule = request.url_rule 

320 

321 # Event Edit 

322 if 'edit' in rule.rule: 

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

324 eventData = eventData, 

325 futureTerms=futureTerms, 

326 event = event, 

327 requirements = requirements, 

328 bonnerCohorts = bonnerCohorts, 

329 userHasRSVPed = userHasRSVPed, 

330 isProgramManager = isProgramManager, 

331 filepaths = filepaths) 

332 # Event View 

333 else: 

334 # get text representations of dates for html 

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

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

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

338 eventCountdown = getCountdownToEvent(event) 

339 

340 

341 # Identify the next event in a recurring series 

342 if event.recurringId: 

343 eventSeriesList = list(Event.select().where(Event.recurringId == event.recurringId) 

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

345 .order_by(Event.startDate)) 

346 eventIndex = eventSeriesList.index(event) 

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

348 eventData["nextRecurringEvent"] = eventSeriesList[eventIndex + 1] 

349 

350 currentEventRsvpAmount = getEventRsvpCount(event.id) 

351 

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

353 

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

355 eventData=eventData, 

356 event=event, 

357 userHasRSVPed=userHasRSVPed, 

358 programTrainings=userParticipatedTrainingEvents, 

359 currentEventRsvpAmount=currentEventRsvpAmount, 

360 isProgramManager=isProgramManager, 

361 filepaths=filepaths, 

362 image=image, 

363 pageViewsCount=pageViewsCount, 

364 eventCountdown=eventCountdown 

365 ) 

366 

367 

368 

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

370def cancelRoute(eventId): 

371 if g.current_user.isAdmin: 

372 try: 

373 cancelEvent(eventId) 

374 return redirect(request.referrer) 

375 

376 except Exception as e: 

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

378 return "", 500 

379 

380 else: 

381 abort(403) 

382 

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

384def undoEvent(): 

385 try: 

386 events = session['lastDeletedEvent'] 

387 for eventId in events: 

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

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

390 recurringEvents = list(Event.select().where((Event.recurringId==event.recurringId) & (Event.deletionDate == None)).order_by(Event.id)) 

391 if event.recurringId is not None: 

392 nameCounter = 1 

393 for recurringEvent in recurringEvents: 

394 newEventNameList = recurringEvent.name.split() 

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

396 newEventNameList = " ".join(newEventNameList) 

397 Event.update({Event.name: newEventNameList}).where(Event.id==recurringEvent.id).execute() 

398 nameCounter += 1 

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

400 return redirect('/eventsList/' + str(g.current_term)) 

401 except Exception as e: 

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

403 return "", 500 

404 

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

406def deleteRoute(eventId): 

407 try: 

408 deleteEvent(eventId) 

409 session['lastDeletedEvent'] = [eventId] 

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

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

412 

413 except Exception as e: 

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

415 return "", 500 

416 

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

418def deleteEventAndAllFollowingRoute(eventId): 

419 try: 

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

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

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

423 

424 except Exception as e: 

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

426 return "", 500 

427 

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

429def deleteAllRecurringEventsRoute(eventId): 

430 try: 

431 session["lastDeletedEvent"] = deleteAllRecurringEvents(eventId) 

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

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

434 

435 except Exception as e: 

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

437 return "", 500 

438 

439@admin_bp.route('/makeRecurringEvents', methods=['POST']) 

440def addRecurringEvents(): 

441 recurringEvents = getRecurringEventsData(preprocessEventData(request.form.copy())) 

442 return json.dumps(recurringEvents, default=str) 

443 

444 

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

446def userProfile(): 

447 volunteerName= request.form.copy() 

448 if volunteerName['searchStudentsInput']: 

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

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

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

452 else: 

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

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

455 

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

457def studentSearchPage(): 

458 if g.current_user.isAdmin: 

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

460 abort(403) 

461 

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

463def activityLogs(): 

464 if g.current_user.isCeltsAdmin: 

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

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

467 allLogs = allLogs) 

468 else: 

469 abort(403) 

470 

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

472def deleteEventFile(): 

473 fileData= request.form 

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

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

476 return "" 

477 

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

479def addCourseFile(): 

480 fileData = request.files['addCourseParticipants'] 

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

482 fileData.save(filePath) 

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

484 os.remove(filePath) 

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

486 

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

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

489def manageServiceLearningCourses(term=None): 

490 

491 """ 

492 The SLC management page for admins 

493 """ 

494 if not g.current_user.isCeltsAdmin: 

495 abort(403) 

496 

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

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

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

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

501 

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

503 

504 setRedirectTarget(request.full_path) 

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

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

507 courseID = session.get("alterCourseId") 

508 

509 if courseID: 

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

511 session.pop("alterCourseId") 

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

513 courseInstructors = getInstructorCourses(), 

514 unapprovedCourses = unapprovedCourses(manageTerm), 

515 approvedCourses = approvedCourses(manageTerm), 

516 importedCourses = getImportedCourses(manageTerm), 

517 terms = selectSurroundingTerms(g.current_term), 

518 term = manageTerm, 

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

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

521 courseID = courseID 

522 ) 

523 

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

525 courseInstructors = getInstructorCourses(), 

526 unapprovedCourses = unapprovedCourses(manageTerm), 

527 approvedCourses = approvedCourses(manageTerm), 

528 importedCourses = getImportedCourses(manageTerm), 

529 terms = selectSurroundingTerms(g.current_term), 

530 term = manageTerm, 

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

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

533 ) 

534 

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

536def getSidebarInformation() -> str: 

537 """ 

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

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

540 ajax request. 

541 """ 

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

543 interestedStudentsCount: int = len(getMinorInterest()) 

544 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

545 "interestedStudentsCount": interestedStudentsCount} 

546 

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

548def removeFromSession(): 

549 try: 

550 session.pop('cpPreview') 

551 except KeyError: 

552 pass 

553 

554 return "" 

555 

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

557def alterImportedCourse(courseID): 

558 """ 

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

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

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

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

563 coming from the imported courses modal.  

564 """ 

565 if request.method == 'GET': 

566 try: 

567 targetCourse = Course.get_by_id(courseID) 

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

569 

570 try: 

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

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

573 serviceHours = 20 

574 

575 courseData = model_to_dict(targetCourse, recurse=False) 

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

577 courseData['hoursEarned'] = serviceHours 

578 

579 return jsonify(courseData) 

580 

581 except DoesNotExist: 

582 flash("Course not found") 

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

584 

585 if request.method == 'POST': 

586 # Update course information in the database 

587 courseData = request.form.copy() 

588 editImportedCourses(courseData) 

589 session['alterCourseId'] = courseID 

590 

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

592 

593 

594@admin_bp.route("/manageBonner") 

595def manageBonner(): 

596 if not g.current_user.isCeltsAdmin: 

597 abort(403) 

598 

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

600 cohorts=getBonnerCohorts(), 

601 events=getBonnerEvents(g.current_term), 

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

603 

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

605def updatecohort(year, method, username): 

606 if not g.current_user.isCeltsAdmin: 

607 abort(403) 

608 

609 try: 

610 user = User.get_by_id(username) 

611 except: 

612 abort(500) 

613 

614 if method == "add": 

615 try: 

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

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

618 except IntegrityError as e: 

619 # if they already exist, ignore the error 

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

621 pass 

622 

623 elif method == "remove": 

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

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

626 else: 

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

628 abort(500) 

629 

630 return "" 

631 

632@admin_bp.route("/bonnerxls") 

633def bonnerxls(): 

634 if not g.current_user.isCeltsAdmin: 

635 abort(403) 

636 

637 newfile = makeBonnerXls() 

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

639 

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

641def saveRequirements(certid): 

642 if not g.current_user.isCeltsAdmin: 

643 abort(403) 

644 

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

646 

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

648 

649 

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

651def displayEventFile(): 

652 fileData = request.form 

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

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

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

656 return ""