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

461 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2026-07-08 20:07 +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 programs = getAllowedPrograms(g.current_user) 

75 if not programs: 

76 abort(403) 

77 visibleTemplates = getAllowedTemplates(g.current_user) 

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

79 programs=programs, 

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

81 templates=visibleTemplates) 

82 

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

84def createEvent(templateid, programid): 

85 if not (g.current_user.isCeltsAdmin 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 

119 if eventData.get('isSeries'): 

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

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

122 if not succeeded: 

123 for index, validationErrorMessage in failedSavedOfferings: 

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

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

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

127 else: 

128 try: 

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

130 except Exception as e: 

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

132 validationErrorMessage = "Failed to save event." 

133 

134 if savedEvents: 

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

136 if rsvpCohorts: 

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

138 if not success: 

139 flash(message, 'warning') 

140 

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

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

143 

144 

145 if program: 

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

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

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

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

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 series {eventList} for {program.programName}, with start dates of {eventDates}.") 

160 

161 else: 

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

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 requirements, bonnerCohorts = [], [] 

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

176 requirements = getCertRequirements(Certification.BONNER) 

177 rawBonnerCohorts = getBonnerCohorts(limit=5) 

178 bonnerCohorts = {} 

179 

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

181 if cohort: 

182 bonnerCohorts[year] = cohort 

183 

184 

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

186 template = template, 

187 eventData = eventData, 

188 termList = selectSurroundingTerms(g.current_term, prevTerms=0), 

189 requirements = requirements, 

190 bonnerCohorts = bonnerCohorts, 

191 isProgramManager = isProgramManager) 

192 

193 

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

195def rsvpLogDisplay(eventId): 

196 event = Event.get_by_id(eventId) 

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

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

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

200 event = event, 

201 allLogs = allLogs) 

202 else: 

203 abort(403) 

204 

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

206def renewEvent(eventId): 

207 try: 

208 formData = request.form 

209 try: 

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

211 except AssertionError: 

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

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

214 

215 try: 

216 if formData.get('dateEnd'): 

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

218 except AssertionError: 

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

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

221 

222 

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

224 newEventDict = priorEvent.copy() 

225 newEventDict.pop('id') 

226 newEventDict.update({ 

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

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

229 'timeStart': formData['timeStart'], 

230 'timeEnd': formData['timeEnd'], 

231 'location': formData['location'], 

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

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

234 'seriesId': priorEvent['seriesId'], 

235 }) 

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

237 if message: 

238 flash(message, "danger") 

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

240 

241 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

245 

246 

247 except Exception as e: 

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

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

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

251 

252 

253 

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

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

256def eventDisplay(eventId): 

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

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

259 viewer = g.current_user 

260 event = Event.get_by_id(eventId) 

261 addEventView(viewer,event) 

262 # Validate given URL 

263 try: 

264 event = Event.get_by_id(eventId) 

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

266 EventCohort.event == event 

267 )) 

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

269 except DoesNotExist as e: 

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

271 abort(404) 

272 

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

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

275 abort(403) 

276 

277 eventData = model_to_dict(event, recurse=False) 

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

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

280 

281 image = None 

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

283 for attachment in associatedAttachments: 

284 for extension in picurestype: 

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

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

287 if image: 

288 break 

289 

290 

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

292 eventData = request.form.copy() 

293 try: 

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

295 

296 except Exception as e: 

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

298 savedEvents = False 

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

300 

301 

302 if savedEvents: 

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

304 updateEventCohorts(savedEvents[0], rsvpCohorts) 

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

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

307 else: 

308 flash(validationErrorMessage, 'warning') 

309 

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

311 preprocessEventData(eventData) 

312 eventData['program'] = event.program 

313 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

316 requirements, bonnerCohorts = [], [] 

317 

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

319 requirements = getCertRequirements(Certification.BONNER) 

320 rawBonnerCohorts = getBonnerCohorts(limit=5) 

321 bonnerCohorts = {} 

322 

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

324 if cohort: 

325 bonnerCohorts[year] = cohort 

326 

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

328 EventCohort.event_id == eventId, 

329 )) 

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

331 else: 

332 requirements, bonnerCohorts, invitedYears = [], [], [] 

333 

334 rule = request.url_rule 

335 

336 # Event Edit 

337 if 'edit' in rule.rule: 

338 event.isCampusGroupsSynced = False 

339 event.save(only=[Event.isCampusGroupsSynced]) 

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

341 eventData = eventData, 

342 termList = Term.select().order_by(Term.termOrder), 

343 event = event, 

344 requirements = requirements, 

345 bonnerCohorts = bonnerCohorts, 

346 invitedYears = invitedYears, 

347 userHasRSVPed = userHasRSVPed, 

348 isProgramManager = isProgramManager, 

349 filepaths = filepaths) 

350 # Event View 

351 else: 

352 # get text representations of dates for html 

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

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

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

356 eventCountdown = getCountdownToEvent(event) 

357 

358 

359 # Identify the next event in a repeating series 

360 if event.seriesId: 

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

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

363 .order_by(Event.startDate)) 

364 eventIndex = eventSeriesList.index(event) 

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

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

367 

368 currentEventRsvpAmount = getEventRsvpCount(event.id) 

369 

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

371 

372 # CampusGroups integration  

373 if event.campusGroupsId: 

374 eventData["campusGroupsURL"] = event.campusGroupsURL 

375 eventData["campusGroupsId"] = event.campusGroupsId 

376 eventData["isCampusGroupsSynced"] = event.isCampusGroupsSynced 

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

378 eventData=eventData, 

379 event=event, 

380 userHasRSVPed=userHasRSVPed, 

381 programTrainings=userParticipatedTrainingEvents, 

382 currentEventRsvpAmount=currentEventRsvpAmount, 

383 isProgramManager=isProgramManager, 

384 filepaths=filepaths, 

385 image=image, 

386 pageViewsCount=pageViewsCount, 

387 invitedYears=invitedYears, 

388 eventCountdown=eventCountdown 

389 ) 

390 

391 

392 

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

394def cancelRoute(eventId): 

395 if g.current_user.isAdmin: 

396 try: 

397 cancelEvent(eventId) 

398 return redirect(request.referrer) 

399 

400 except Exception as e: 

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

402 return "", 500 

403 

404 else: 

405 abort(403) 

406 

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

408def undoBackgroundCheck(): 

409 try: 

410 username = g.current_user 

411 bgCheckId = session['lastDeletedBgCheck'] 

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

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

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

415 except Exception as e: 

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

417 return "", 500 

418 

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

420def undoEvent(): 

421 try: 

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

423 for eventId in eventIds: 

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

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

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

427 if event.isRepeating: 

428 nameCounter = 1 

429 for repeatingEvent in repeatingEvents: 

430 newEventNameList = repeatingEvent.name.split() 

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

432 newEventNameList = " ".join(newEventNameList) 

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

434 nameCounter += 1 

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

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

437 except Exception as e: 

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

439 return "", 500 

440 

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

442def deleteRoute(eventId): 

443 try: 

444 deleteEvent(eventId) 

445 session['lastDeletedEvent'] = [eventId] 

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

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

448 

449 except Exception as e: 

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

451 return "", 500 

452 

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

454def deleteEventAndAllFollowingRoute(eventId): 

455 try: 

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

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

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

459 

460 except Exception as e: 

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

462 return "", 500 

463 

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

465def deleteAllEventsInSeriesRoute(eventId): 

466 try: 

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

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

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

470 

471 except Exception as e: 

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

473 return "", 500 

474 

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

476def addRepeatingEvents(): 

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

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

479 

480 

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

482def userProfile(): 

483 volunteerName= request.form.copy() 

484 if volunteerName['searchStudentsInput']: 

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

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

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

488 else: 

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

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

491 

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

493def studentSearchPage(): 

494 if g.current_user.isAdmin: 

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

496 abort(403) 

497 

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

499def activityLogs(): 

500 if g.current_user.isCeltsAdmin: 

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

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

503 allLogs = allLogs) 

504 else: 

505 abort(403) 

506 

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

508def deleteEventFile(): 

509 fileData= request.form 

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

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

512 return "" 

513 

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

515def addCourseFile(): 

516 fileData = request.files['addCourseParticipants'] 

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

518 fileData.save(filePath) 

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

520 os.remove(filePath) 

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

522 

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

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

525def manageServiceLearningCourses(term=None): 

526 

527 """ 

528 The SLC management page for admins 

529 """ 

530 if not g.current_user.isCeltsAdmin: 

531 abort(403) 

532 

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

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

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

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

537 

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

539 

540 setRedirectTarget(request.full_path) 

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

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

543 courseID = session.get("alterCourseId") 

544 

545 if courseID: 

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

547 session.pop("alterCourseId") 

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

549 courseInstructors = getInstructorCourses(), 

550 unapprovedCourses = unapprovedCourses(manageTerm), 

551 approvedCourses = approvedCourses(manageTerm), 

552 importedCourses = getImportedCourses(manageTerm), 

553 terms = selectSurroundingTerms(g.current_term), 

554 term = manageTerm, 

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

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

557 courseID = courseID 

558 ) 

559 

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

561 courseInstructors = getInstructorCourses(), 

562 unapprovedCourses = unapprovedCourses(manageTerm), 

563 approvedCourses = approvedCourses(manageTerm), 

564 importedCourses = getImportedCourses(manageTerm), 

565 terms = selectSurroundingTerms(g.current_term), 

566 term = manageTerm, 

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

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

569 ) 

570 

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

572def getSidebarInformation() -> str: 

573 """ 

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

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

576 ajax request. 

577 """ 

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

579 interestedStudentsCount: int = len(getMinorInterest()) 

580 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

581 "interestedStudentsCount": interestedStudentsCount} 

582 

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

584def removeFromSession(): 

585 try: 

586 session.pop('cpPreview') 

587 except KeyError: 

588 pass 

589 

590 return "" 

591 

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

593def alterImportedCourse(courseID): 

594 """ 

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

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

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

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

599 coming from the imported courses modal.  

600 """ 

601 if request.method == 'GET': 

602 try: 

603 targetCourse = Course.get_by_id(courseID) 

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

605 

606 try: 

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

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

609 serviceHours = 20 

610 

611 courseData = model_to_dict(targetCourse, recurse=False) 

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

613 courseData['hoursEarned'] = serviceHours 

614 

615 return jsonify(courseData) 

616 

617 except DoesNotExist: 

618 flash("Course not found") 

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

620 

621 if request.method == 'POST': 

622 # Update course information in the database 

623 courseData = request.form.copy() 

624 editImportedCourses(courseData) 

625 session['alterCourseId'] = courseID 

626 

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

628 

629 

630@admin_bp.route("/manageBonner") 

631def manageBonner(): 

632 if not g.current_user.isCeltsAdmin: 

633 abort(403) 

634 

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

636 cohorts=getBonnerCohorts(), 

637 events=getBonnerEvents(g.current_term), 

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

639 

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

641def updatecohort(year, method, username): 

642 if not g.current_user.isCeltsAdmin: 

643 abort(403) 

644 

645 try: 

646 user = User.get_by_id(username) 

647 except: 

648 abort(500) 

649 

650 if method == "add": 

651 try: 

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

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

654 except IntegrityError as e: 

655 # if they already exist, ignore the error 

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

657 pass 

658 

659 elif method == "remove": 

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

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

662 else: 

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

664 abort(500) 

665 return "" 

666 

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

668def getBonnerXls(startingYear, noOfYears): 

669 if not g.current_user.isCeltsAdmin: 

670 abort(403) 

671 newfile = makeBonnerXls(startingYear, noOfYears) 

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

673 

674 

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

676def saveRequirements(certid): 

677 if not g.current_user.isCeltsAdmin: 

678 abort(403) 

679 

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

681 

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

683 

684 

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

686def displayEventFile(): 

687 fileData = request.form 

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

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

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

691 return ""