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

457 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2025-07-22 20:03 +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.backgroundCheck import BackgroundCheck 

11from app.models.program import Program 

12from app.models.event import Event 

13from app.models.eventRsvp import EventRsvp 

14from app.models.eventParticipant import EventParticipant 

15from app.models.user import User 

16from app.models.course import Course 

17from app.models.courseInstructor import CourseInstructor 

18from app.models.courseParticipant import CourseParticipant 

19from app.models.eventTemplate import EventTemplate 

20from app.models.activityLog import ActivityLog 

21from app.models.eventRsvpLog import EventRsvpLog 

22from app.models.attachmentUpload import AttachmentUpload 

23from app.models.bonnerCohort import BonnerCohort 

24from app.models.eventCohort import EventCohort 

25from app.models.certification import Certification 

26from app.models.user import User 

27from app.models.term import Term 

28from app.models.eventViews import EventView 

29from app.models.courseStatus import CourseStatus 

30 

31from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates 

32from app.logic.createLogs import createActivityLog 

33from app.logic.certification import getCertRequirements, updateCertRequirements 

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

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

36from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp 

37from app.logic.minor import getMinorInterest 

38from app.logic.fileHandler import FileHandler 

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

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

41 

42from app.controllers.admin import admin_bp 

43from app.logic.volunteerSpreadsheet import createSpreadsheet 

44 

45 

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

47def reports(): 

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

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

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

51 

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

53def downloadFile(): 

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

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

56 return send_file(filepath, as_attachment=True) 

57 

58 

59 

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

61def switchUser(): 

62 if app.env == "production": 

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

64 abort(403) 

65 

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

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

68 

69 return redirect(request.referrer) 

70 

71 

72@admin_bp.route('/eventTemplates') 

73def templateSelect(): 

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

75 allprograms = getAllowedPrograms(g.current_user) 

76 visibleTemplates = getAllowedTemplates(g.current_user) 

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

78 programs=allprograms, 

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

80 templates=visibleTemplates) 

81 else: 

82 abort(403) 

83 

84 

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

86def createEvent(templateid, programid): 

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

88 abort(403) 

89 

90 # Validate given URL 

91 program = None 

92 try: 

93 template = EventTemplate.get_by_id(templateid) 

94 if programid: 

95 program = Program.get_by_id(programid) 

96 except DoesNotExist as e: 

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

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

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

100 

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

102 eventData = template.templateData 

103 eventData['program'] = program 

104 

105 if request.method == "GET": 

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

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

108 if program: 

109 eventData['location'] = program.defaultLocation 

110 if program.contactName: 

111 eventData['contactName'] = program.contactName 

112 if program.contactEmail: 

113 eventData['contactEmail'] = program.contactEmail 

114 

115 # Try to save the form 

116 if request.method == "POST": 

117 savedEvents = None 

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

119 eventData = preprocessEventData(eventData) 

120 

121 if eventData.get('isSeries'): 

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

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

124 if not succeeded: 

125 for index, validationErrorMessage in failedSavedOfferings: 

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

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

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

129 else: 

130 try: 

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

132 except Exception as e: 

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

134 validationErrorMessage = "Failed to save event." 

135 

136 if savedEvents: 

137 rsvpCohorts = request.form.getlist("cohorts[]") 

138 if rsvpCohorts: 

139 success, message, invitedCohorts = inviteCohortsToEvent(savedEvents[0], rsvpCohorts) 

140 if not success: 

141 flash(message, 'warning') 

142 

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

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

145 

146 

147 if program: 

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

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

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

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

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

153 

154 if len(savedEvents) > 1: 

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

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

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

158 lastEventDate = eventDates[-1] 

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

160 

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

162 

163 else: 

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

165 else: 

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

167 

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

169 else: 

170 flash(validationErrorMessage, 'warning') 

171 

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

173 preprocessEventData(eventData) 

174 isProgramManager = g.current_user.isProgramManagerFor(programid) 

175 

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

177 

178 requirements, bonnerCohorts = [], [] 

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

180 requirements = getCertRequirements(Certification.BONNER) 

181 rawBonnerCohorts = getBonnerCohorts(limit=5) 

182 bonnerCohorts = {} 

183 

184 for year, cohort in rawBonnerCohorts.items(): 

185 if cohort: 

186 bonnerCohorts[year] = cohort 

187 

188 

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

190 template = template, 

191 eventData = eventData, 

192 futureTerms = futureTerms, 

193 requirements = requirements, 

194 bonnerCohorts = bonnerCohorts, 

195 isProgramManager = isProgramManager) 

196 

197 

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

199def rsvpLogDisplay(eventId): 

200 event = Event.get_by_id(eventId) 

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

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

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

204 event = event, 

205 allLogs = allLogs) 

206 else: 

207 abort(403) 

208 

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

210def renewEvent(eventId): 

211 try: 

212 formData = request.form 

213 try: 

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

215 except AssertionError: 

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

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

218 

219 try: 

220 if formData.get('dateEnd'): 

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

222 except AssertionError: 

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

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

225 

226 

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

228 newEventDict = priorEvent.copy() 

229 newEventDict.pop('id') 

230 newEventDict.update({ 

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

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

233 'timeStart': formData['timeStart'], 

234 'timeEnd': formData['timeEnd'], 

235 'location': formData['location'], 

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

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

238 'seriesId': priorEvent['seriesId'], 

239 }) 

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

241 if message: 

242 flash(message, "danger") 

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

244 

245 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

249 

250 

251 except Exception as e: 

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

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

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

255 

256 

257 

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

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

260def eventDisplay(eventId): 

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

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

263 viewer = g.current_user 

264 event = Event.get_by_id(eventId) 

265 addEventView(viewer,event) 

266 # Validate given URL 

267 try: 

268 event = Event.get_by_id(eventId) 

269 invitedCohorts = list(EventCohort.select().where( 

270 EventCohort.event == event 

271 )) 

272 invitedYears = [str(cohort.year) for cohort in invitedCohorts] 

273 except DoesNotExist as e: 

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

275 abort(404) 

276 

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

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

279 abort(403) 

280 

281 eventData = model_to_dict(event, recurse=False) 

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

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

284 

285 image = None 

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

287 for attachment in associatedAttachments: 

288 for extension in picurestype: 

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

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

291 if image: 

292 break 

293 

294 

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

296 eventData = request.form.copy() 

297 try: 

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

299 

300 except Exception as e: 

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

302 savedEvents = False 

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

304 

305 

306 if savedEvents: 

307 rsvpCohorts = request.form.getlist("cohorts[]") 

308 updateEventCohorts(savedEvents[0], rsvpCohorts) 

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

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

311 else: 

312 flash(validationErrorMessage, 'warning') 

313 

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

315 preprocessEventData(eventData) 

316 eventData['program'] = event.program 

317 futureTerms = selectSurroundingTerms(g.current_term) 

318 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

321 requirements, bonnerCohorts = [], [] 

322 

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

324 requirements = getCertRequirements(Certification.BONNER) 

325 rawBonnerCohorts = getBonnerCohorts(limit=5) 

326 bonnerCohorts = {} 

327 

328 for year, cohort in rawBonnerCohorts.items(): 

329 if cohort: 

330 bonnerCohorts[year] = cohort 

331 

332 invitedCohorts = list(EventCohort.select().where( 

333 EventCohort.event_id == eventId, 

334 )) 

335 invitedYears = [str(cohort.year) for cohort in invitedCohorts] 

336 else: 

337 requirements, bonnerCohorts, invitedYears = [], [], [] 

338 

339 rule = request.url_rule 

340 

341 # Event Edit 

342 if 'edit' in rule.rule: 

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

344 eventData = eventData, 

345 futureTerms = futureTerms, 

346 event = event, 

347 requirements = requirements, 

348 bonnerCohorts = bonnerCohorts, 

349 invitedYears = invitedYears, 

350 userHasRSVPed = userHasRSVPed, 

351 isProgramManager = isProgramManager, 

352 filepaths = filepaths) 

353 # Event View 

354 else: 

355 # get text representations of dates for html 

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

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

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

359 eventCountdown = getCountdownToEvent(event) 

360 

361 

362 # Identify the next event in a repeating series 

363 if event.seriesId: 

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

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

366 .order_by(Event.startDate)) 

367 eventIndex = eventSeriesList.index(event) 

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

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

370 

371 currentEventRsvpAmount = getEventRsvpCount(event.id) 

372 

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

374 

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

376 eventData=eventData, 

377 event=event, 

378 userHasRSVPed=userHasRSVPed, 

379 programTrainings=userParticipatedTrainingEvents, 

380 currentEventRsvpAmount=currentEventRsvpAmount, 

381 isProgramManager=isProgramManager, 

382 filepaths=filepaths, 

383 image=image, 

384 pageViewsCount=pageViewsCount, 

385 invitedYears=invitedYears, 

386 eventCountdown=eventCountdown 

387 ) 

388 

389 

390 

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

392def cancelRoute(eventId): 

393 if g.current_user.isAdmin: 

394 try: 

395 cancelEvent(eventId) 

396 return redirect(request.referrer) 

397 

398 except Exception as e: 

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

400 return "", 500 

401 

402 else: 

403 abort(403) 

404 

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

406def undoBackgroundCheck(): 

407 try: 

408 username = g.current_user 

409 bgCheckId = session['lastDeletedBgCheck'] 

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

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

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

413 except Exception as e: 

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

415 return "", 500 

416 

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

418def undoEvent(): 

419 try: 

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

421 for eventId in eventIds: 

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

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

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

425 if event.isRepeating: 

426 nameCounter = 1 

427 for repeatingEvent in repeatingEvents: 

428 newEventNameList = repeatingEvent.name.split() 

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

430 newEventNameList = " ".join(newEventNameList) 

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

432 nameCounter += 1 

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

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

435 except Exception as e: 

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

437 return "", 500 

438 

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

440def deleteRoute(eventId): 

441 try: 

442 deleteEvent(eventId) 

443 session['lastDeletedEvent'] = [eventId] 

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

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

446 

447 except Exception as e: 

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

449 return "", 500 

450 

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

452def deleteEventAndAllFollowingRoute(eventId): 

453 try: 

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

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

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

457 

458 except Exception as e: 

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

460 return "", 500 

461 

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

463def deleteAllEventsInSeriesRoute(eventId): 

464 try: 

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

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

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

468 

469 except Exception as e: 

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

471 return "", 500 

472 

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

474def addRepeatingEvents(): 

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

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

477 

478 

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

480def userProfile(): 

481 volunteerName= request.form.copy() 

482 if volunteerName['searchStudentsInput']: 

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

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

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

486 else: 

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

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

489 

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

491def studentSearchPage(): 

492 if g.current_user.isAdmin: 

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

494 abort(403) 

495 

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

497def activityLogs(): 

498 if g.current_user.isCeltsAdmin: 

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

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

501 allLogs = allLogs) 

502 else: 

503 abort(403) 

504 

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

506def deleteEventFile(): 

507 fileData= request.form 

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

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

510 return "" 

511 

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

513def addCourseFile(): 

514 fileData = request.files['addCourseParticipants'] 

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

516 fileData.save(filePath) 

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

518 os.remove(filePath) 

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

520 

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

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

523def manageServiceLearningCourses(term=None): 

524 

525 """ 

526 The SLC management page for admins 

527 """ 

528 if not g.current_user.isCeltsAdmin: 

529 abort(403) 

530 

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

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

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

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

535 

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

537 

538 setRedirectTarget(request.full_path) 

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

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

541 courseID = session.get("alterCourseId") 

542 

543 if courseID: 

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

545 session.pop("alterCourseId") 

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

547 courseInstructors = getInstructorCourses(), 

548 unapprovedCourses = unapprovedCourses(manageTerm), 

549 approvedCourses = approvedCourses(manageTerm), 

550 importedCourses = getImportedCourses(manageTerm), 

551 terms = selectSurroundingTerms(g.current_term), 

552 term = manageTerm, 

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

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

555 courseID = courseID 

556 ) 

557 

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

559 courseInstructors = getInstructorCourses(), 

560 unapprovedCourses = unapprovedCourses(manageTerm), 

561 approvedCourses = approvedCourses(manageTerm), 

562 importedCourses = getImportedCourses(manageTerm), 

563 terms = selectSurroundingTerms(g.current_term), 

564 term = manageTerm, 

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

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

567 ) 

568 

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

570def getSidebarInformation() -> str: 

571 """ 

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

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

574 ajax request. 

575 """ 

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

577 interestedStudentsCount: int = len(getMinorInterest()) 

578 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

579 "interestedStudentsCount": interestedStudentsCount} 

580 

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

582def removeFromSession(): 

583 try: 

584 session.pop('cpPreview') 

585 except KeyError: 

586 pass 

587 

588 return "" 

589 

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

591def alterImportedCourse(courseID): 

592 """ 

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

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

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

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

597 coming from the imported courses modal.  

598 """ 

599 if request.method == 'GET': 

600 try: 

601 targetCourse = Course.get_by_id(courseID) 

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

603 

604 try: 

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

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

607 serviceHours = 20 

608 

609 courseData = model_to_dict(targetCourse, recurse=False) 

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

611 courseData['hoursEarned'] = serviceHours 

612 

613 return jsonify(courseData) 

614 

615 except DoesNotExist: 

616 flash("Course not found") 

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

618 

619 if request.method == 'POST': 

620 # Update course information in the database 

621 courseData = request.form.copy() 

622 editImportedCourses(courseData) 

623 session['alterCourseId'] = courseID 

624 

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

626 

627 

628@admin_bp.route("/manageBonner") 

629def manageBonner(): 

630 if not g.current_user.isCeltsAdmin: 

631 abort(403) 

632 

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

634 cohorts=getBonnerCohorts(), 

635 events=getBonnerEvents(g.current_term), 

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

637 

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

639def updatecohort(year, method, username): 

640 if not g.current_user.isCeltsAdmin: 

641 abort(403) 

642 

643 try: 

644 user = User.get_by_id(username) 

645 except: 

646 abort(500) 

647 

648 if method == "add": 

649 try: 

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

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

652 except IntegrityError as e: 

653 # if they already exist, ignore the error 

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

655 pass 

656 

657 elif method == "remove": 

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

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

660 else: 

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

662 abort(500) 

663 return "" 

664 

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

666def getBonnerXls(startingYear, noOfYears): 

667 if not g.current_user.isCeltsAdmin: 

668 abort(403) 

669 newfile = makeBonnerXls(startingYear, noOfYears) 

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

671 

672 

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

674def saveRequirements(certid): 

675 if not g.current_user.isCeltsAdmin: 

676 abort(403) 

677 

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

679 

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

681 

682 

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

684def displayEventFile(): 

685 fileData = request.form 

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

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

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

689 return ""