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

431 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-10-02 17:57 +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 

132 if savedEvents: 

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

134 for year in rsvpcohorts: 

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

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

137 

138 

139 noun = ((eventData.get('isRecurring') or eventData.get('isMultipleOffering')) and "Events" or "Event") # pluralize 

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

141 

142 

143 if program: 

144 if len(savedEvents) > 1 and eventData.get('isRecurring'): 

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

146 

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

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

149 

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

151 

152 if len(savedEvents) > 1: 

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

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

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

156 lastEventDate = eventDates[-1] 

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

158 

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

160 

161 else: 

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

163 else: 

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

165 

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

167 else: 

168 flash(validationErrorMessage, 'warning') 

169 

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

171 preprocessEventData(eventData) 

172 isProgramManager = g.current_user.isProgramManagerFor(programid) 

173 

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

175 

176 requirements, bonnerCohorts = [], [] 

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

178 requirements = getCertRequirements(Certification.BONNER) 

179 bonnerCohorts = getBonnerCohorts(limit=5) 

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

181 template = template, 

182 eventData = eventData, 

183 futureTerms = futureTerms, 

184 requirements = requirements, 

185 bonnerCohorts = bonnerCohorts, 

186 isProgramManager = isProgramManager) 

187 

188 

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

190def rsvpLogDisplay(eventId): 

191 event = Event.get_by_id(eventId) 

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

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

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

195 event = event, 

196 allLogs = allLogs) 

197 else: 

198 abort(403) 

199 

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

201def renewEvent(eventId): 

202 try: 

203 formData = request.form 

204 try: 

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

206 except AssertionError: 

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

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

209 

210 try: 

211 if formData.get('dateEnd'): 

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

213 except AssertionError: 

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

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

216 

217 

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

219 newEventDict = priorEvent.copy() 

220 newEventDict.pop('id') 

221 newEventDict.update({ 

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

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

224 'timeStart': formData['timeStart'], 

225 'timeEnd': formData['timeEnd'], 

226 'location': formData['location'], 

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

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

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

230 'isMultipleOffering': bool(priorEvent['multipleOffeirngId']), 

231 }) 

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

233 if message: 

234 flash(message, "danger") 

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

236 

237 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

241 

242 

243 except Exception as e: 

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

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

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

247 

248 

249 

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

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

252def eventDisplay(eventId): 

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

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

255 viewer = g.current_user 

256 event = Event.get_by_id(eventId) 

257 addEventView(viewer,event) 

258 # Validate given URL 

259 try: 

260 event = Event.get_by_id(eventId) 

261 except DoesNotExist as e: 

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

263 abort(404) 

264 

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

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

267 abort(403) 

268 

269 eventData = model_to_dict(event, recurse=False) 

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

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

272 

273 image = None 

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

275 for attachment in associatedAttachments: 

276 for extension in picurestype: 

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

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

279 if image: 

280 break 

281 

282 

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

284 eventData = request.form.copy() 

285 try: 

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

287 

288 except Exception as e: 

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

290 savedEvents = False 

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

292 

293 

294 if savedEvents: 

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

296 for year in rsvpcohorts: 

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

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

299 

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

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

302 else: 

303 flash(validationErrorMessage, 'warning') 

304 

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

306 preprocessEventData(eventData) 

307 eventData['program'] = event.program 

308 futureTerms = selectSurroundingTerms(g.current_term) 

309 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

312 requirements, bonnerCohorts = [], [] 

313 

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

315 requirements = getCertRequirements(Certification.BONNER) 

316 bonnerCohorts = getBonnerCohorts(limit=5) 

317 

318 rule = request.url_rule 

319 

320 # Event Edit 

321 if 'edit' in rule.rule: 

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

323 eventData = eventData, 

324 futureTerms=futureTerms, 

325 event = event, 

326 requirements = requirements, 

327 bonnerCohorts = bonnerCohorts, 

328 userHasRSVPed = userHasRSVPed, 

329 isProgramManager = isProgramManager, 

330 filepaths = filepaths) 

331 # Event View 

332 else: 

333 # get text representations of dates for html 

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

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

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

337 eventCountdown = getCountdownToEvent(event) 

338 

339 

340 # Identify the next event in a recurring series 

341 if event.recurringId: 

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

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

344 .order_by(Event.startDate)) 

345 eventIndex = eventSeriesList.index(event) 

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

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

348 

349 currentEventRsvpAmount = getEventRsvpCount(event.id) 

350 

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

352 

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

354 eventData=eventData, 

355 event=event, 

356 userHasRSVPed=userHasRSVPed, 

357 programTrainings=userParticipatedTrainingEvents, 

358 currentEventRsvpAmount=currentEventRsvpAmount, 

359 isProgramManager=isProgramManager, 

360 filepaths=filepaths, 

361 image=image, 

362 pageViewsCount=pageViewsCount, 

363 eventCountdown=eventCountdown 

364 ) 

365 

366 

367 

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

369def cancelRoute(eventId): 

370 if g.current_user.isAdmin: 

371 try: 

372 cancelEvent(eventId) 

373 return redirect(request.referrer) 

374 

375 except Exception as e: 

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

377 return "", 500 

378 

379 else: 

380 abort(403) 

381 

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

383def undoEvent(): 

384 try: 

385 events = session['lastDeletedEvent'] 

386 for eventId in events: 

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

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

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

390 if event.recurringId is not None: 

391 nameCounter = 1 

392 for recurringEvent in recurringEvents: 

393 newEventNameList = recurringEvent.name.split() 

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

395 newEventNameList = " ".join(newEventNameList) 

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

397 nameCounter += 1 

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

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

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>/deleteAllRecurring', methods=['POST']) 

428def deleteAllRecurringEventsRoute(eventId): 

429 try: 

430 session["lastDeletedEvent"] = deleteAllRecurringEvents(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('/makeRecurringEvents', methods=['POST']) 

439def addRecurringEvents(): 

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

441 return json.dumps(recurringEvents, 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 ""