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

464 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2026-04-29 19:36 +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, getTargetList 

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 # Existing RSVP-specific log entries 

199 event_logs = list(EventRsvpLog.select(EventRsvpLog, User) 

200 .join(User, on=(EventRsvpLog.createdBy == User.username)) 

201 .where(EventRsvpLog.event_id == eventId)) 

202 

203 # Include invited users from EventRsvp so the log display reflects invitations too 

204 invited_rsvps = EventRsvp.select(EventRsvp, User).join(User).where(EventRsvp.event == eventId) 

205 

206 from collections import namedtuple 

207 LogEntry = namedtuple('LogEntry', ['createdOn', 'createdBy', 'rsvpLogContent']) 

208 

209 allLogs = [] 

210 allLogs.extend(event_logs) 

211 

212 # Only add invitation logs for non-RSVP events, as for RSVP events, EventRsvp represents RSVPs, not invitations 

213 if not event.isRsvpRequired: 

214 for rsvp in invited_rsvps: 

215 # Provide an explicit invitation action for EventRsvp records 

216 allLogs.append(LogEntry( 

217 createdOn=rsvp.rsvpTime, 

218 createdBy=rsvp.user, 

219 rsvpLogContent=f"Added {rsvp.user.fullName} to {getTargetList(event)}" 

220 )) 

221 

222 allLogs.sort(key=lambda entry: entry.createdOn, reverse=True) 

223 

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

225 event = event, 

226 allLogs = allLogs) 

227 else: 

228 abort(403) 

229 

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

231def renewEvent(eventId): 

232 try: 

233 formData = request.form 

234 try: 

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

236 except AssertionError: 

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

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

239 

240 try: 

241 if formData.get('dateEnd'): 

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

243 except AssertionError: 

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

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

246 

247 

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

249 newEventDict = priorEvent.copy() 

250 newEventDict.pop('id') 

251 newEventDict.update({ 

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

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

254 'timeStart': formData['timeStart'], 

255 'timeEnd': formData['timeEnd'], 

256 'location': formData['location'], 

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

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

259 'seriesId': priorEvent['seriesId'], 

260 }) 

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

262 if message: 

263 flash(message, "danger") 

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

265 

266 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

270 

271 

272 except Exception as e: 

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

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

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

276 

277 

278 

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

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

281def eventDisplay(eventId): 

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

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

284 viewer = g.current_user 

285 event = Event.get_by_id(eventId) 

286 addEventView(viewer,event) 

287 # Validate given URL 

288 try: 

289 event = Event.get_by_id(eventId) 

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

291 EventCohort.event == event 

292 )) 

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

294 except DoesNotExist as e: 

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

296 abort(404) 

297 

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

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

300 abort(403) 

301 

302 eventData = model_to_dict(event, recurse=False) 

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

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

305 

306 image = None 

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

308 for attachment in associatedAttachments: 

309 for extension in picurestype: 

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

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

312 if image: 

313 break 

314 

315 

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

317 eventData = request.form.copy() 

318 try: 

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

320 

321 except Exception as e: 

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

323 savedEvents = False 

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

325 

326 

327 if savedEvents: 

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

329 updateEventCohorts(savedEvents[0], rsvpCohorts) 

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

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

332 else: 

333 flash(validationErrorMessage, 'warning') 

334 

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

336 preprocessEventData(eventData) 

337 eventData['program'] = event.program 

338 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

341 requirements, bonnerCohorts = [], [] 

342 

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

344 requirements = getCertRequirements(Certification.BONNER) 

345 rawBonnerCohorts = getBonnerCohorts(limit=5) 

346 bonnerCohorts = {} 

347 

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

349 if cohort: 

350 bonnerCohorts[year] = cohort 

351 

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

353 EventCohort.event_id == eventId, 

354 )) 

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

356 else: 

357 requirements, bonnerCohorts, invitedYears = [], [], [] 

358 

359 rule = request.url_rule 

360 

361 # Event Edit 

362 if 'edit' in rule.rule: 

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

364 eventData = eventData, 

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

366 event = event, 

367 requirements = requirements, 

368 bonnerCohorts = bonnerCohorts, 

369 invitedYears = invitedYears, 

370 userHasRSVPed = userHasRSVPed, 

371 isProgramManager = isProgramManager, 

372 filepaths = filepaths) 

373 # Event View 

374 else: 

375 # get text representations of dates for html 

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

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

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

379 eventCountdown = getCountdownToEvent(event) 

380 

381 

382 # Identify the next event in a repeating series 

383 if event.seriesId: 

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

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

386 .order_by(Event.startDate)) 

387 eventIndex = eventSeriesList.index(event) 

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

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

390 

391 currentEventRsvpAmount = getEventRsvpCount(event.id) 

392 

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

394 

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

396 eventData=eventData, 

397 event=event, 

398 userHasRSVPed=userHasRSVPed, 

399 programTrainings=userParticipatedTrainingEvents, 

400 currentEventRsvpAmount=currentEventRsvpAmount, 

401 isProgramManager=isProgramManager, 

402 filepaths=filepaths, 

403 image=image, 

404 pageViewsCount=pageViewsCount, 

405 invitedYears=invitedYears, 

406 eventCountdown=eventCountdown 

407 ) 

408 

409 

410 

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

412def cancelRoute(eventId): 

413 if g.current_user.isAdmin: 

414 try: 

415 cancelEvent(eventId) 

416 return redirect(request.referrer) 

417 

418 except Exception as e: 

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

420 return "", 500 

421 

422 else: 

423 abort(403) 

424 

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

426def undoBackgroundCheck(): 

427 try: 

428 username = g.current_user 

429 bgCheckId = session['lastDeletedBgCheck'] 

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

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

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

433 except Exception as e: 

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

435 return "", 500 

436 

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

438def undoEvent(): 

439 try: 

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

441 for eventId in eventIds: 

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

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

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

445 if event.isRepeating: 

446 nameCounter = 1 

447 for repeatingEvent in repeatingEvents: 

448 newEventNameList = repeatingEvent.name.split() 

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

450 newEventNameList = " ".join(newEventNameList) 

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

452 nameCounter += 1 

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

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

455 except Exception as e: 

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

457 return "", 500 

458 

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

460def deleteRoute(eventId): 

461 try: 

462 deleteEvent(eventId) 

463 session['lastDeletedEvent'] = [eventId] 

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

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

466 

467 except Exception as e: 

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

469 return "", 500 

470 

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

472def deleteEventAndAllFollowingRoute(eventId): 

473 try: 

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

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

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

477 

478 except Exception as e: 

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

480 return "", 500 

481 

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

483def deleteAllEventsInSeriesRoute(eventId): 

484 try: 

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

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

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

488 

489 except Exception as e: 

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

491 return "", 500 

492 

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

494def addRepeatingEvents(): 

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

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

497 

498 

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

500def userProfile(): 

501 volunteerName= request.form.copy() 

502 if volunteerName['searchStudentsInput']: 

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

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

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

506 else: 

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

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

509 

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

511def studentSearchPage(): 

512 if g.current_user.isAdmin: 

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

514 abort(403) 

515 

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

517def activityLogs(): 

518 if g.current_user.isCeltsAdmin: 

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

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

521 allLogs = allLogs) 

522 else: 

523 abort(403) 

524 

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

526def deleteEventFile(): 

527 fileData= request.form 

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

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

530 return "" 

531 

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

533def addCourseFile(): 

534 fileData = request.files['addCourseParticipants'] 

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

536 fileData.save(filePath) 

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

538 os.remove(filePath) 

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

540 

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

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

543def manageServiceLearningCourses(term=None): 

544 

545 """ 

546 The SLC management page for admins 

547 """ 

548 if not g.current_user.isCeltsAdmin: 

549 abort(403) 

550 

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

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

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

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

555 

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

557 

558 setRedirectTarget(request.full_path) 

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

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

561 courseID = session.get("alterCourseId") 

562 

563 if courseID: 

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

565 session.pop("alterCourseId") 

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

567 courseInstructors = getInstructorCourses(), 

568 unapprovedCourses = unapprovedCourses(manageTerm), 

569 approvedCourses = approvedCourses(manageTerm), 

570 importedCourses = getImportedCourses(manageTerm), 

571 terms = selectSurroundingTerms(g.current_term), 

572 term = manageTerm, 

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

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

575 courseID = courseID 

576 ) 

577 

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

579 courseInstructors = getInstructorCourses(), 

580 unapprovedCourses = unapprovedCourses(manageTerm), 

581 approvedCourses = approvedCourses(manageTerm), 

582 importedCourses = getImportedCourses(manageTerm), 

583 terms = selectSurroundingTerms(g.current_term), 

584 term = manageTerm, 

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

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

587 ) 

588 

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

590def getSidebarInformation() -> str: 

591 """ 

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

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

594 ajax request. 

595 """ 

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

597 interestedStudentsCount: int = len(getMinorInterest()) 

598 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

599 "interestedStudentsCount": interestedStudentsCount} 

600 

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

602def removeFromSession(): 

603 try: 

604 session.pop('cpPreview') 

605 except KeyError: 

606 pass 

607 

608 return "" 

609 

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

611def alterImportedCourse(courseID): 

612 """ 

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

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

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

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

617 coming from the imported courses modal.  

618 """ 

619 if request.method == 'GET': 

620 try: 

621 targetCourse = Course.get_by_id(courseID) 

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

623 

624 try: 

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

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

627 serviceHours = 20 

628 

629 courseData = model_to_dict(targetCourse, recurse=False) 

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

631 courseData['hoursEarned'] = serviceHours 

632 

633 return jsonify(courseData) 

634 

635 except DoesNotExist: 

636 flash("Course not found") 

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

638 

639 if request.method == 'POST': 

640 # Update course information in the database 

641 courseData = request.form.copy() 

642 editImportedCourses(courseData) 

643 session['alterCourseId'] = courseID 

644 

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

646 

647 

648@admin_bp.route("/manageBonner") 

649def manageBonner(): 

650 if not g.current_user.isCeltsAdmin: 

651 abort(403) 

652 

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

654 cohorts=getBonnerCohorts(), 

655 events=getBonnerEvents(g.current_term), 

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

657 

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

659def updatecohort(year, method, username): 

660 if not g.current_user.isCeltsAdmin: 

661 abort(403) 

662 

663 try: 

664 user = User.get_by_id(username) 

665 except: 

666 abort(500) 

667 

668 if method == "add": 

669 try: 

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

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

672 except IntegrityError as e: 

673 # if they already exist, ignore the error 

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

675 pass 

676 

677 elif method == "remove": 

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

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

680 else: 

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

682 abort(500) 

683 return "" 

684 

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

686def getBonnerXls(startingYear, noOfYears): 

687 if not g.current_user.isCeltsAdmin: 

688 abort(403) 

689 newfile = makeBonnerXls(startingYear, noOfYears) 

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

691 

692 

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

694def saveRequirements(certid): 

695 if not g.current_user.isCeltsAdmin: 

696 abort(403) 

697 

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

699 

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

701 

702 

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

704def displayEventFile(): 

705 fileData = request.form 

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

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

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

709 return ""