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

432 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2025-01-29 20:22 +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 'isRepeating': bool(priorEvent['isRepeating']), 

228 'seriesId': priorEvent['seriesId'], 

229 }) 

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

231 if message: 

232 flash(message, "danger") 

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

234 

235 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

239 

240 

241 except Exception as e: 

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

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

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

245 

246 

247 

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

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

250def eventDisplay(eventId): 

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

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

253 viewer = g.current_user 

254 event = Event.get_by_id(eventId) 

255 addEventView(viewer,event) 

256 # Validate given URL 

257 try: 

258 event = Event.get_by_id(eventId) 

259 except DoesNotExist as e: 

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

261 abort(404) 

262 

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

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

265 abort(403) 

266 

267 eventData = model_to_dict(event, recurse=False) 

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

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

270 

271 image = None 

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

273 for attachment in associatedAttachments: 

274 for extension in picurestype: 

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

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

277 if image: 

278 break 

279 

280 

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

282 eventData = request.form.copy() 

283 try: 

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

285 

286 except Exception as e: 

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

288 savedEvents = False 

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

290 

291 

292 if savedEvents: 

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

294 for year in rsvpcohorts: 

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

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

297 

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

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

300 else: 

301 flash(validationErrorMessage, 'warning') 

302 

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

304 preprocessEventData(eventData) 

305 eventData['program'] = event.program 

306 futureTerms = selectSurroundingTerms(g.current_term) 

307 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

310 requirements, bonnerCohorts = [], [] 

311 

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

313 requirements = getCertRequirements(Certification.BONNER) 

314 bonnerCohorts = getBonnerCohorts(limit=5) 

315 

316 rule = request.url_rule 

317 

318 # Event Edit 

319 if 'edit' in rule.rule: 

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

321 eventData = eventData, 

322 futureTerms=futureTerms, 

323 event = event, 

324 requirements = requirements, 

325 bonnerCohorts = bonnerCohorts, 

326 userHasRSVPed = userHasRSVPed, 

327 isProgramManager = isProgramManager, 

328 filepaths = filepaths) 

329 # Event View 

330 else: 

331 # get text representations of dates for html 

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

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

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

335 eventCountdown = getCountdownToEvent(event) 

336 

337 

338 # Identify the next event in a repeating series 

339 if event.seriesId: 

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

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

342 .order_by(Event.startDate)) 

343 eventIndex = eventSeriesList.index(event) 

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

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

346 

347 currentEventRsvpAmount = getEventRsvpCount(event.id) 

348 

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

350 

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

352 eventData=eventData, 

353 event=event, 

354 userHasRSVPed=userHasRSVPed, 

355 programTrainings=userParticipatedTrainingEvents, 

356 currentEventRsvpAmount=currentEventRsvpAmount, 

357 isProgramManager=isProgramManager, 

358 filepaths=filepaths, 

359 image=image, 

360 pageViewsCount=pageViewsCount, 

361 eventCountdown=eventCountdown 

362 ) 

363 

364 

365 

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

367def cancelRoute(eventId): 

368 if g.current_user.isAdmin: 

369 try: 

370 cancelEvent(eventId) 

371 return redirect(request.referrer) 

372 

373 except Exception as e: 

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

375 return "", 500 

376 

377 else: 

378 abort(403) 

379 

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

381def undoEvent(): 

382 try: 

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

384 for eventId in eventIds: 

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

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

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

388 if event.isRepeating: 

389 nameCounter = 1 

390 for repeatingEvent in repeatingEvents: 

391 newEventNameList = repeatingEvent.name.split() 

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

393 newEventNameList = " ".join(newEventNameList) 

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

395 nameCounter += 1 

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

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

398 

399 except Exception as e: 

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

401 return "", 500 

402 

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

404def deleteRoute(eventId): 

405 try: 

406 deleteEvent(eventId) 

407 session['lastDeletedEvent'] = [eventId] 

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

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

410 

411 except Exception as e: 

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

413 return "", 500 

414 

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

416def deleteEventAndAllFollowingRoute(eventId): 

417 try: 

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

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

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

421 

422 except Exception as e: 

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

424 return "", 500 

425 

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

427def deleteAllEventsInSeriesRoute(eventId): 

428 try: 

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

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

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

432 

433 except Exception as e: 

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

435 return "", 500 

436 

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

438def addRepeatingEvents(): 

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

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

441 

442 

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

444def userProfile(): 

445 volunteerName= request.form.copy() 

446 if volunteerName['searchStudentsInput']: 

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

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

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

450 else: 

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

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

453 

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

455def studentSearchPage(): 

456 if g.current_user.isAdmin: 

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

458 abort(403) 

459 

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

461def activityLogs(): 

462 if g.current_user.isCeltsAdmin: 

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

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

465 allLogs = allLogs) 

466 else: 

467 abort(403) 

468 

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

470def deleteEventFile(): 

471 fileData= request.form 

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

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

474 return "" 

475 

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

477def addCourseFile(): 

478 fileData = request.files['addCourseParticipants'] 

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

480 fileData.save(filePath) 

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

482 os.remove(filePath) 

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

484 

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

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

487def manageServiceLearningCourses(term=None): 

488 

489 """ 

490 The SLC management page for admins 

491 """ 

492 if not g.current_user.isCeltsAdmin: 

493 abort(403) 

494 

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

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

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

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

499 

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

501 

502 setRedirectTarget(request.full_path) 

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

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

505 courseID = session.get("alterCourseId") 

506 

507 if courseID: 

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

509 session.pop("alterCourseId") 

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

511 courseInstructors = getInstructorCourses(), 

512 unapprovedCourses = unapprovedCourses(manageTerm), 

513 approvedCourses = approvedCourses(manageTerm), 

514 importedCourses = getImportedCourses(manageTerm), 

515 terms = selectSurroundingTerms(g.current_term), 

516 term = manageTerm, 

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

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

519 courseID = courseID 

520 ) 

521 

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

523 courseInstructors = getInstructorCourses(), 

524 unapprovedCourses = unapprovedCourses(manageTerm), 

525 approvedCourses = approvedCourses(manageTerm), 

526 importedCourses = getImportedCourses(manageTerm), 

527 terms = selectSurroundingTerms(g.current_term), 

528 term = manageTerm, 

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

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

531 ) 

532 

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

534def getSidebarInformation() -> str: 

535 """ 

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

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

538 ajax request. 

539 """ 

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

541 interestedStudentsCount: int = len(getMinorInterest()) 

542 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

543 "interestedStudentsCount": interestedStudentsCount} 

544 

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

546def removeFromSession(): 

547 try: 

548 session.pop('cpPreview') 

549 except KeyError: 

550 pass 

551 

552 return "" 

553 

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

555def alterImportedCourse(courseID): 

556 """ 

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

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

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

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

561 coming from the imported courses modal.  

562 """ 

563 if request.method == 'GET': 

564 try: 

565 targetCourse = Course.get_by_id(courseID) 

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

567 

568 try: 

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

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

571 serviceHours = 20 

572 

573 courseData = model_to_dict(targetCourse, recurse=False) 

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

575 courseData['hoursEarned'] = serviceHours 

576 

577 return jsonify(courseData) 

578 

579 except DoesNotExist: 

580 flash("Course not found") 

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

582 

583 if request.method == 'POST': 

584 # Update course information in the database 

585 courseData = request.form.copy() 

586 editImportedCourses(courseData) 

587 session['alterCourseId'] = courseID 

588 

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

590 

591 

592@admin_bp.route("/manageBonner") 

593def manageBonner(): 

594 if not g.current_user.isCeltsAdmin: 

595 abort(403) 

596 

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

598 cohorts=getBonnerCohorts(), 

599 events=getBonnerEvents(g.current_term), 

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

601 

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

603def updatecohort(year, method, username): 

604 if not g.current_user.isCeltsAdmin: 

605 abort(403) 

606 

607 try: 

608 user = User.get_by_id(username) 

609 except: 

610 abort(500) 

611 

612 if method == "add": 

613 try: 

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

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

616 except IntegrityError as e: 

617 # if they already exist, ignore the error 

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

619 pass 

620 

621 elif method == "remove": 

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

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

624 else: 

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

626 abort(500) 

627 

628 return "" 

629 

630@admin_bp.route("/bonnerxls") 

631def bonnerxls(): 

632 if not g.current_user.isCeltsAdmin: 

633 abort(403) 

634 

635 newfile = makeBonnerXls() 

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

637 

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

639def saveRequirements(certid): 

640 if not g.current_user.isCeltsAdmin: 

641 abort(403) 

642 

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

644 

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

646 

647 

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

649def displayEventFile(): 

650 fileData = request.form 

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

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

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

654 return ""