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

536 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-09-02 19:10 +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 

41from app.logic.participants import sortParticipants 

42 

43from app.controllers.admin import admin_bp 

44from app.logic.volunteerSpreadsheet import createSpreadsheet 

45from app.logic.users import isBannedFromEvent 

46 

47from peewee import DoesNotExist, JOIN 

48from app.logic.participants import updateEventLabor, getEventParticipants, addParticipantToEvent 

49from app.logic.sharedLogic import getEventLengthInHours 

50from app.logic.events import getPreviousSeriesEventData, getEventRsvpCount 

51from app.logic.users import getBannedUsers 

52 

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

54def reports(): 

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

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

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

58 

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

60def downloadFile(): 

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

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

63 return send_file(filepath, as_attachment=True) 

64 

65 

66 

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

68def switchUser(): 

69 if app.env == "production": 

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

71 abort(403) 

72 

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

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

75 

76 return redirect(request.referrer) 

77 

78 

79@admin_bp.route('/eventTemplates') 

80def templateSelect(): 

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

82 allprograms = getAllowedPrograms(g.current_user) 

83 visibleTemplates = getAllowedTemplates(g.current_user) 

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

85 programs=allprograms, 

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

87 templates=visibleTemplates) 

88 else: 

89 abort(403) 

90 

91 

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

93def createEvent(templateid, programid): 

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

95 abort(403) 

96 

97 # Validate given URL 

98 program = None 

99 try: 

100 template = EventTemplate.get_by_id(templateid) 

101 if programid: 

102 program = Program.get_by_id(programid) 

103 except DoesNotExist as e: 

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

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

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

107 

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

109 eventData = template.templateData 

110 eventData['program'] = program 

111 

112 if request.method == "GET": 

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

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

115 if program: 

116 eventData['location'] = program.defaultLocation 

117 if program.contactName: 

118 eventData['contactName'] = program.contactName 

119 if program.contactEmail: 

120 eventData['contactEmail'] = program.contactEmail 

121 

122 # Try to save the form 

123 if request.method == "POST": 

124 savedEvents = None 

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

126 eventData = preprocessEventData(eventData) 

127 

128 if eventData.get('isSeries'): 

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

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

131 if not succeeded: 

132 for index, validationErrorMessage in failedSavedOfferings: 

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

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

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

136 else: 

137 try: 

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

139 except Exception as e: 

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

141 validationErrorMessage = "Failed to save event." 

142 

143 if savedEvents: 

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

145 if rsvpCohorts: 

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

147 if not success: 

148 flash(message, 'warning') 

149 

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

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

152 

153 

154 if program: 

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

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

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

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

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

160 

161 if len(savedEvents) > 1: 

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

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

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

165 lastEventDate = eventDates[-1] 

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

167 

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

169 

170 else: 

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

172 else: 

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

174 

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

176 else: 

177 flash(validationErrorMessage, 'warning') 

178 

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

180 preprocessEventData(eventData) 

181 isProgramManager = g.current_user.isProgramManagerFor(programid) 

182 

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

184 

185 requirements, bonnerCohorts = [], [] 

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

187 requirements = getCertRequirements(Certification.BONNER) 

188 rawBonnerCohorts = getBonnerCohorts(limit=5) 

189 bonnerCohorts = {} 

190 

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

192 if cohort: 

193 bonnerCohorts[year] = cohort 

194 

195 

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

197 template = template, 

198 eventData = eventData, 

199 futureTerms = futureTerms, 

200 requirements = requirements, 

201 bonnerCohorts = bonnerCohorts, 

202 isProgramManager = isProgramManager) 

203 

204 

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

206def rsvpLogDisplay(eventId): 

207 event = Event.get_by_id(eventId) 

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

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

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

211 event = event, 

212 allLogs = allLogs) 

213 else: 

214 abort(403) 

215 

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

217def renewEvent(eventId): 

218 try: 

219 formData = request.form 

220 try: 

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

222 except AssertionError: 

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

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

225 

226 try: 

227 if formData.get('dateEnd'): 

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

229 except AssertionError: 

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

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

232 

233 

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

235 newEventDict = priorEvent.copy() 

236 newEventDict.pop('id') 

237 newEventDict.update({ 

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

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

240 'timeStart': formData['timeStart'], 

241 'timeEnd': formData['timeEnd'], 

242 'location': formData['location'], 

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

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

245 'seriesId': priorEvent['seriesId'], 

246 }) 

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

248 if message: 

249 flash(message, "danger") 

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

251 

252 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

256 

257 

258 except Exception as e: 

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

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

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

262 

263 

264 

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

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

267def eventDisplay(eventId): 

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

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

270 viewer = g.current_user 

271 event = Event.get_by_id(eventId) 

272 addEventView(viewer,event) 

273 # Validate given URL 

274 try: 

275 event = Event.get_by_id(eventId) 

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

277 EventCohort.event == event 

278 )) 

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

280 except DoesNotExist as e: 

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

282 abort(404) 

283 

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

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

286 abort(403) 

287 

288 eventData = model_to_dict(event, recurse=False) 

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

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

291 

292 image = None 

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

294 for attachment in associatedAttachments: 

295 for extension in picurestype: 

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

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

298 if image: 

299 break 

300 

301 

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

303 eventData = request.form.copy() 

304 try: 

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

306 

307 except Exception as e: 

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

309 savedEvents = False 

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

311 

312 

313 if savedEvents: 

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

315 updateEventCohorts(savedEvents[0], rsvpCohorts) 

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

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

318 else: 

319 flash(validationErrorMessage, 'warning') 

320 

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

322 preprocessEventData(eventData) 

323 eventData['program'] = event.program 

324 futureTerms = selectSurroundingTerms(g.current_term) 

325 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

328 requirements, bonnerCohorts = [], [] 

329 

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

331 requirements = getCertRequirements(Certification.BONNER) 

332 rawBonnerCohorts = getBonnerCohorts(limit=5) 

333 bonnerCohorts = {} 

334 

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

336 if cohort: 

337 bonnerCohorts[year] = cohort 

338 

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

340 EventCohort.event_id == eventId, 

341 )) 

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

343 else: 

344 requirements, bonnerCohorts, invitedYears = [], [], [] 

345 

346 rule = request.url_rule 

347 

348 # Event Edit 

349 if 'edit' in rule.rule: 

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

351 eventData = eventData, 

352 futureTerms = futureTerms, 

353 event = event, 

354 requirements = requirements, 

355 bonnerCohorts = bonnerCohorts, 

356 invitedYears = invitedYears, 

357 userHasRSVPed = userHasRSVPed, 

358 isProgramManager = isProgramManager, 

359 filepaths = filepaths) 

360 # Event View 

361 else: 

362 # get text representations of dates for html 

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

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

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

366 eventCountdown = getCountdownToEvent(event) 

367 

368 

369 # Identify the next event in a repeating series 

370 if event.seriesId: 

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

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

373 .order_by(Event.startDate)) 

374 eventIndex = eventSeriesList.index(event) 

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

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

377 

378 currentEventRsvpAmount = getEventRsvpCount(event.id) 

379 

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

381 

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

383 eventData=eventData, 

384 event=event, 

385 userHasRSVPed=userHasRSVPed, 

386 programTrainings=userParticipatedTrainingEvents, 

387 currentEventRsvpAmount=currentEventRsvpAmount, 

388 isProgramManager=isProgramManager, 

389 filepaths=filepaths, 

390 image=image, 

391 pageViewsCount=pageViewsCount, 

392 invitedYears=invitedYears, 

393 eventCountdown=eventCountdown 

394 ) 

395 

396 

397 

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

399def cancelRoute(eventId): 

400 if g.current_user.isAdmin: 

401 try: 

402 cancelEvent(eventId) 

403 return redirect(request.referrer) 

404 

405 except Exception as e: 

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

407 return "", 500 

408 

409 else: 

410 abort(403) 

411 

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

413def undoBackgroundCheck(): 

414 try: 

415 username = g.current_user 

416 bgCheckId = session['lastDeletedBgCheck'] 

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

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

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

420 except Exception as e: 

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

422 return "", 500 

423 

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

425def undoEvent(): 

426 try: 

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

428 for eventId in eventIds: 

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

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

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

432 if event.isRepeating: 

433 nameCounter = 1 

434 for repeatingEvent in repeatingEvents: 

435 newEventNameList = repeatingEvent.name.split() 

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

437 newEventNameList = " ".join(newEventNameList) 

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

439 nameCounter += 1 

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

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

442 except Exception as e: 

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

444 return "", 500 

445 

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

447def deleteRoute(eventId): 

448 try: 

449 deleteEvent(eventId) 

450 session['lastDeletedEvent'] = [eventId] 

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

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

453 

454 except Exception as e: 

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

456 return "", 500 

457 

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

459def deleteEventAndAllFollowingRoute(eventId): 

460 try: 

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

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

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

464 

465 except Exception as e: 

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

467 return "", 500 

468 

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

470def deleteAllEventsInSeriesRoute(eventId): 

471 try: 

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

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

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

475 

476 except Exception as e: 

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

478 return "", 500 

479 

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

481def addRepeatingEvents(): 

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

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

484 

485 

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

487def userProfile(): 

488 volunteerName= request.form.copy() 

489 if volunteerName['searchStudentsInput']: 

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

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

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

493 else: 

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

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

496 

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

498def studentSearchPage(): 

499 if g.current_user.isAdmin: 

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

501 abort(403) 

502 

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

504def activityLogs(): 

505 if g.current_user.isCeltsAdmin: 

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

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

508 allLogs = allLogs) 

509 else: 

510 abort(403) 

511 

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

513def deleteEventFile(): 

514 fileData= request.form 

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

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

517 return "" 

518 

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

520def addCourseFile(): 

521 fileData = request.files['addCourseParticipants'] 

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

523 fileData.save(filePath) 

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

525 os.remove(filePath) 

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

527 

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

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

530def manageServiceLearningCourses(term=None): 

531 

532 """ 

533 The SLC management page for admins 

534 """ 

535 if not g.current_user.isCeltsAdmin: 

536 abort(403) 

537 

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

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

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

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

542 

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

544 

545 setRedirectTarget(request.full_path) 

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

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

548 courseID = session.get("alterCourseId") 

549 

550 if courseID: 

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

552 session.pop("alterCourseId") 

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

554 courseInstructors = getInstructorCourses(), 

555 unapprovedCourses = unapprovedCourses(manageTerm), 

556 approvedCourses = approvedCourses(manageTerm), 

557 importedCourses = getImportedCourses(manageTerm), 

558 terms = selectSurroundingTerms(g.current_term), 

559 term = manageTerm, 

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

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

562 courseID = courseID 

563 ) 

564 

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

566 courseInstructors = getInstructorCourses(), 

567 unapprovedCourses = unapprovedCourses(manageTerm), 

568 approvedCourses = approvedCourses(manageTerm), 

569 importedCourses = getImportedCourses(manageTerm), 

570 terms = selectSurroundingTerms(g.current_term), 

571 term = manageTerm, 

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

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

574 ) 

575 

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

577def getSidebarInformation() -> str: 

578 """ 

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

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

581 ajax request. 

582 """ 

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

584 interestedStudentsCount: int = len(getMinorInterest()) 

585 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

586 "interestedStudentsCount": interestedStudentsCount} 

587 

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

589def removeFromSession(): 

590 try: 

591 session.pop('cpPreview') 

592 except KeyError: 

593 pass 

594 

595 return "" 

596 

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

598def alterImportedCourse(courseID): 

599 """ 

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

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

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

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

604 coming from the imported courses modal.  

605 """ 

606 if request.method == 'GET': 

607 try: 

608 targetCourse = Course.get_by_id(courseID) 

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

610 

611 try: 

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

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

614 serviceHours = 20 

615 

616 courseData = model_to_dict(targetCourse, recurse=False) 

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

618 courseData['hoursEarned'] = serviceHours 

619 

620 return jsonify(courseData) 

621 

622 except DoesNotExist: 

623 flash("Course not found") 

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

625 

626 if request.method == 'POST': 

627 # Update course information in the database 

628 courseData = request.form.copy() 

629 editImportedCourses(courseData) 

630 session['alterCourseId'] = courseID 

631 

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

633 

634 

635@admin_bp.route("/manageBonner") 

636def manageBonner(): 

637 if not g.current_user.isCeltsAdmin: 

638 abort(403) 

639 

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

641 cohorts=getBonnerCohorts(), 

642 events=getBonnerEvents(g.current_term), 

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

644 

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

646def updatecohort(year, method, username): 

647 if not g.current_user.isCeltsAdmin: 

648 abort(403) 

649 

650 try: 

651 user = User.get_by_id(username) 

652 except: 

653 abort(500) 

654 

655 if method == "add": 

656 try: 

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

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

659 except IntegrityError as e: 

660 # if they already exist, ignore the error 

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

662 pass 

663 

664 elif method == "remove": 

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

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

667 else: 

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

669 abort(500) 

670 return "" 

671 

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

673def getBonnerXls(startingYear, noOfYears): 

674 if not g.current_user.isCeltsAdmin: 

675 abort(403) 

676 newfile = makeBonnerXls(startingYear, noOfYears) 

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

678 

679 

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

681def saveRequirements(certid): 

682 if not g.current_user.isCeltsAdmin: 

683 abort(403) 

684 

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

686 

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

688 

689 

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

691def displayEventFile(): 

692 fileData = request.form 

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

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

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

696 return "" 

697 

698 

699 

700@admin_bp.route('/event/<eventID>/manage_labor', methods=['GET', 'POST']) 

701def manageLaborPage(eventID): 

702 try: 

703 event = Event.get_by_id(eventID) 

704 except DoesNotExist as e: 

705 print(f"No event found for {eventID}", e) 

706 abort(404) 

707 

708 if request.method == "POST": 

709 laborUpdated = updateEventLabor(request.form) 

710 

711 # error handling depending on the boolean returned from updateEventLabor 

712 if laborUpdated: 

713 flash("Labor table succesfully updated", "success") 

714 else: 

715 flash("Error adding labor", "danger") 

716 return redirect(url_for("admin.manageLaborPage", eventID=eventID)) 

717 

718 # ------------ GET request ------------ 

719 elif request.method == "GET": 

720 if not (g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerForEvent(event))): 

721 abort(403) 

722 

723 # ------- Grab the different lists of participants ------- 

724 

725 bannedUsersForProgram = [bannedUser.user for bannedUser in getBannedUsers(event.program)] 

726 

727 eventLaborData, eventLabor = sortParticipants(event, True) 

728 

729 allRelevantUsers = list(set(participant.user for participant in (eventLabor + eventLaborData))) 

730 # ----------- Get miscellaneous data ----------- 

731 eventLengthInHours = getEventLengthInHours(event.timeStart, event.timeEnd, event.startDate) 

732 repeatingLabors = getPreviousSeriesEventData(event.seriesId) 

733 

734 return render_template("/events/manageLabor.html", 

735 eventLaborData = eventLaborData, 

736 eventLabor = eventLabor, 

737 eventLength = eventLengthInHours, 

738 event = event, 

739 repeatingLabors = repeatingLabors, 

740 bannedUsersForProgram = bannedUsersForProgram,) 

741 

742 

743@admin_bp.route('/removeLaborFromEvent', methods = ['POST']) 

744def removeLaborFromEvent(): 

745 user = request.form.get('username') 

746 eventID = request.form.get('eventId') 

747 if g.current_user.isAdmin: 

748 (EventParticipant.delete().where(EventParticipant.user==user, EventParticipant.event==eventID)).execute() 

749 flash("Student successfully removed", "success") 

750 return "" 

751 

752@admin_bp.route('/addLaborToEvent/<eventId>', methods = ['POST']) 

753def addLaborToEvent(eventId): 

754 event = Event.get_by_id(eventId) 

755 successfullyAddedLabor = False 

756 usernameList = request.form.getlist("selectedLabor[]") 

757 alreadyAddedList = [] 

758 addedSuccessfullyList = [] 

759 errorList = [] 

760 

761 for user in usernameList: 

762 userObj = User.get_by_id(user) 

763 successfullyAddedLabor = addParticipantToEvent(userObj, event, True) 

764 if successfullyAddedLabor == "already in": 

765 alreadyAddedList.append(userObj.fullName) 

766 else: 

767 if successfullyAddedLabor: 

768 addedSuccessfullyList.append(userObj.fullName) 

769 else: 

770 errorList.append(userObj.fullName) 

771 

772 

773 studentLabor = "" 

774 if alreadyAddedList: 

775 studentLabor = ", ".join(vol for vol in alreadyAddedList) 

776 flash(f"{studentLabor} was already added to this event.", "warning") 

777 

778 if addedSuccessfullyList: 

779 studentLabor = ", ".join(vol for vol in addedSuccessfullyList) 

780 flash(f"{studentLabor} added successfully.", "success") 

781 

782 if errorList: 

783 studentLabor = ", ".join(vol for vol in errorList) 

784 flash(f"Error when adding {studentLabor} to event.", "danger") 

785 

786 if 'ajax' in request.form and request.form['ajax']: 

787 return '' 

788 

789 return redirect(url_for('admin.manageLaborPage', eventID = eventId)) 

790 

791@admin_bp.route("/event/<int:event_id>/scannerentry", methods=["GET", "POST"]) 

792def eventKioskStatus(event_id): 

793 referer = request.referrer 

794 is_labor = False 

795 

796 if referer and "manage_labor" in referer: 

797 is_labor = True 

798 elif referer and "manage_volunteers" in referer: 

799 is_labor = False 

800 

801 event = Event.get_by_id(event_id) 

802 return render_template("/events/eventKiosk.html", is_labor=is_labor, event=event) 

803 

804@admin_bp.route('/addLaborToEvent/<username>/<eventId>/isBanned', methods = ['GET']) 

805def isLaborBanned(username, eventId): 

806 return {"banned":1} if isBannedFromEvent(username, eventId) else {"banned":0}