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

457 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2025-06-20 15:06 +0000

1from flask import request, render_template, url_for, g, redirect 

2from flask import flash, abort, jsonify, session, send_file 

3from peewee import DoesNotExist, fn, IntegrityError 

4from playhouse.shortcuts import model_to_dict 

5import json 

6from datetime import datetime 

7import os 

8 

9from app import app 

10from app.models.backgroundCheck import BackgroundCheck 

11from app.models.program import Program 

12from app.models.event import Event 

13from app.models.eventRsvp import EventRsvp 

14from app.models.eventParticipant import EventParticipant 

15from app.models.user import User 

16from app.models.course import Course 

17from app.models.courseInstructor import CourseInstructor 

18from app.models.courseParticipant import CourseParticipant 

19from app.models.eventTemplate import EventTemplate 

20from app.models.activityLog import ActivityLog 

21from app.models.eventRsvpLog import EventRsvpLog 

22from app.models.attachmentUpload import AttachmentUpload 

23from app.models.bonnerCohort import BonnerCohort 

24from app.models.eventCohort import EventCohort 

25from app.models.certification import Certification 

26from app.models.user import User 

27from app.models.term import Term 

28from app.models.eventViews import EventView 

29from app.models.courseStatus import CourseStatus 

30 

31from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates 

32from app.logic.createLogs import createActivityLog 

33from app.logic.certification import getCertRequirements, updateCertRequirements 

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

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

36from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp 

37from app.logic.minor import getMinorInterest 

38from app.logic.fileHandler import FileHandler 

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

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

41 

42from app.controllers.admin import admin_bp 

43from app.logic.volunteerSpreadsheet import createSpreadsheet 

44 

45 

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

47def reports(): 

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

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

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

51 

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

53def downloadFile(): 

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

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

56 return send_file(filepath, as_attachment=True) 

57 

58 

59 

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

61def switchUser(): 

62 if app.env == "production": 

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

64 abort(403) 

65 

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

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

68 

69 return redirect(request.referrer) 

70 

71 

72@admin_bp.route('/eventTemplates') 

73def templateSelect(): 

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

75 allprograms = getAllowedPrograms(g.current_user) 

76 visibleTemplates = getAllowedTemplates(g.current_user) 

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

78 programs=allprograms, 

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

80 templates=visibleTemplates) 

81 else: 

82 abort(403) 

83 

84 

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

86def createEvent(templateid, programid): 

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

88 abort(403) 

89 

90 # Validate given URL 

91 program = None 

92 try: 

93 template = EventTemplate.get_by_id(templateid) 

94 if programid: 

95 program = Program.get_by_id(programid) 

96 except DoesNotExist as e: 

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

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

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

100 

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

102 eventData = template.templateData 

103 eventData['program'] = program 

104 

105 if request.method == "GET": 

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

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

108 if program: 

109 eventData['location'] = program.defaultLocation 

110 if program.contactName: 

111 eventData['contactName'] = program.contactName 

112 if program.contactEmail: 

113 eventData['contactEmail'] = program.contactEmail 

114 

115 # Try to save the form 

116 if request.method == "POST": 

117 savedEvents = None 

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

119 eventData = preprocessEventData(eventData) 

120 if eventData.get('isSeries'): 

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

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

123 if not succeeded: 

124 for index, validationErrorMessage in failedSavedOfferings: 

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

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

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

128 else: 

129 try: 

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

131 except Exception as e: 

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

133 validationErrorMessage = "Failed to save event." 

134 

135 if savedEvents: 

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

137 if rsvpCohorts: 

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

139 if not success: 

140 flash(message, 'warning') 

141 

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

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

144 

145 

146 if program: 

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

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

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

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

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

152 

153 if len(savedEvents) > 1: 

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

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

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

157 lastEventDate = eventDates[-1] 

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

159 

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

161 

162 else: 

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

164 else: 

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

166 

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

168 else: 

169 flash(validationErrorMessage, 'warning') 

170 

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

172 preprocessEventData(eventData) 

173 isProgramManager = g.current_user.isProgramManagerFor(programid) 

174 

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

176 

177 requirements, bonnerCohorts = [], [] 

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

179 requirements = getCertRequirements(Certification.BONNER) 

180 rawBonnerCohorts = getBonnerCohorts(limit=5) 

181 bonnerCohorts = {} 

182 

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

184 if cohort: 

185 bonnerCohorts[year] = cohort 

186 

187 

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

189 template = template, 

190 eventData = eventData, 

191 futureTerms = futureTerms, 

192 requirements = requirements, 

193 bonnerCohorts = bonnerCohorts, 

194 isProgramManager = isProgramManager) 

195 

196 

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

198def rsvpLogDisplay(eventId): 

199 event = Event.get_by_id(eventId) 

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

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

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

203 event = event, 

204 allLogs = allLogs) 

205 else: 

206 abort(403) 

207 

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

209def renewEvent(eventId): 

210 try: 

211 formData = request.form 

212 try: 

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

214 except AssertionError: 

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

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

217 

218 try: 

219 if formData.get('dateEnd'): 

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

221 except AssertionError: 

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

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

224 

225 

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

227 newEventDict = priorEvent.copy() 

228 newEventDict.pop('id') 

229 newEventDict.update({ 

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

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

232 'timeStart': formData['timeStart'], 

233 'timeEnd': formData['timeEnd'], 

234 'location': formData['location'], 

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

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

237 'seriesId': priorEvent['seriesId'], 

238 }) 

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

240 if message: 

241 flash(message, "danger") 

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

243 

244 copyRsvpToNewEvent(priorEvent, newEvent[0]) 

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

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

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

248 

249 

250 except Exception as e: 

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

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

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

254 

255 

256 

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

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

259def eventDisplay(eventId): 

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

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

262 viewer = g.current_user 

263 event = Event.get_by_id(eventId) 

264 addEventView(viewer,event) 

265 # Validate given URL 

266 try: 

267 event = Event.get_by_id(eventId) 

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

269 EventCohort.event == event 

270 )) 

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

272 except DoesNotExist as e: 

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

274 abort(404) 

275 

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

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

278 abort(403) 

279 

280 eventData = model_to_dict(event, recurse=False) 

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

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

283 

284 image = None 

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

286 for attachment in associatedAttachments: 

287 for extension in picurestype: 

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

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

290 if image: 

291 break 

292 

293 

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

295 eventData = request.form.copy() 

296 try: 

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

298 

299 except Exception as e: 

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

301 savedEvents = False 

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

303 

304 

305 if savedEvents: 

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

307 updateEventCohorts(savedEvents[0], rsvpCohorts) 

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

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

310 else: 

311 flash(validationErrorMessage, 'warning') 

312 

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

314 preprocessEventData(eventData) 

315 eventData['program'] = event.program 

316 futureTerms = selectSurroundingTerms(g.current_term) 

317 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

320 requirements, bonnerCohorts = [], [] 

321 

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

323 requirements = getCertRequirements(Certification.BONNER) 

324 rawBonnerCohorts = getBonnerCohorts(limit=5) 

325 bonnerCohorts = {} 

326 

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

328 if cohort: 

329 bonnerCohorts[year] = cohort 

330 

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

332 EventCohort.event_id == eventId, 

333 )) 

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

335 else: 

336 requirements, bonnerCohorts, invitedYears = [], [], [] 

337 

338 rule = request.url_rule 

339 

340 # Event Edit 

341 if 'edit' in rule.rule: 

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

343 eventData = eventData, 

344 futureTerms = futureTerms, 

345 event = event, 

346 requirements = requirements, 

347 bonnerCohorts = bonnerCohorts, 

348 invitedYears = invitedYears, 

349 userHasRSVPed = userHasRSVPed, 

350 isProgramManager = isProgramManager, 

351 filepaths = filepaths) 

352 # Event View 

353 else: 

354 # get text representations of dates for html 

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

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

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

358 eventCountdown = getCountdownToEvent(event) 

359 

360 

361 # Identify the next event in a repeating series 

362 if event.seriesId: 

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

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

365 .order_by(Event.startDate)) 

366 eventIndex = eventSeriesList.index(event) 

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

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

369 

370 currentEventRsvpAmount = getEventRsvpCount(event.id) 

371 

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

373 

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

375 eventData=eventData, 

376 event=event, 

377 userHasRSVPed=userHasRSVPed, 

378 programTrainings=userParticipatedTrainingEvents, 

379 currentEventRsvpAmount=currentEventRsvpAmount, 

380 isProgramManager=isProgramManager, 

381 filepaths=filepaths, 

382 image=image, 

383 pageViewsCount=pageViewsCount, 

384 invitedYears=invitedYears, 

385 eventCountdown=eventCountdown 

386 ) 

387 

388 

389 

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

391def cancelRoute(eventId): 

392 if g.current_user.isAdmin: 

393 try: 

394 cancelEvent(eventId) 

395 return redirect(request.referrer) 

396 

397 except Exception as e: 

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

399 return "", 500 

400 

401 else: 

402 abort(403) 

403 

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

405def undoBackgroundCheck(): 

406 try: 

407 username = g.current_user 

408 bgCheckId = session['lastDeletedBgCheck'] 

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

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

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

412 except Exception as e: 

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

414 return "", 500 

415 

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

417def undoEvent(): 

418 try: 

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

420 for eventId in eventIds: 

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

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

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

424 if event.isRepeating: 

425 nameCounter = 1 

426 for repeatingEvent in repeatingEvents: 

427 newEventNameList = repeatingEvent.name.split() 

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

429 newEventNameList = " ".join(newEventNameList) 

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

431 nameCounter += 1 

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

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

434 except Exception as e: 

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

436 return "", 500 

437 

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

439def deleteRoute(eventId): 

440 try: 

441 deleteEvent(eventId) 

442 session['lastDeletedEvent'] = [eventId] 

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

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

445 

446 except Exception as e: 

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

448 return "", 500 

449 

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

451def deleteEventAndAllFollowingRoute(eventId): 

452 try: 

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

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

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

456 

457 except Exception as e: 

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

459 return "", 500 

460 

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

462def deleteAllEventsInSeriesRoute(eventId): 

463 try: 

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

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

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

467 

468 except Exception as e: 

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

470 return "", 500 

471 

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

473def addRepeatingEvents(): 

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

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

476 

477 

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

479def userProfile(): 

480 volunteerName= request.form.copy() 

481 if volunteerName['searchStudentsInput']: 

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

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

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

485 else: 

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

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

488 

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

490def studentSearchPage(): 

491 if g.current_user.isAdmin: 

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

493 abort(403) 

494 

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

496def activityLogs(): 

497 if g.current_user.isCeltsAdmin: 

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

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

500 allLogs = allLogs) 

501 else: 

502 abort(403) 

503 

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

505def deleteEventFile(): 

506 fileData= request.form 

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

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

509 return "" 

510 

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

512def addCourseFile(): 

513 fileData = request.files['addCourseParticipants'] 

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

515 fileData.save(filePath) 

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

517 os.remove(filePath) 

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

519 

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

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

522def manageServiceLearningCourses(term=None): 

523 

524 """ 

525 The SLC management page for admins 

526 """ 

527 if not g.current_user.isCeltsAdmin: 

528 abort(403) 

529 

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

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

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

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

534 

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

536 

537 setRedirectTarget(request.full_path) 

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

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

540 courseID = session.get("alterCourseId") 

541 

542 if courseID: 

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

544 session.pop("alterCourseId") 

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

546 courseInstructors = getInstructorCourses(), 

547 unapprovedCourses = unapprovedCourses(manageTerm), 

548 approvedCourses = approvedCourses(manageTerm), 

549 importedCourses = getImportedCourses(manageTerm), 

550 terms = selectSurroundingTerms(g.current_term), 

551 term = manageTerm, 

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

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

554 courseID = courseID 

555 ) 

556 

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

558 courseInstructors = getInstructorCourses(), 

559 unapprovedCourses = unapprovedCourses(manageTerm), 

560 approvedCourses = approvedCourses(manageTerm), 

561 importedCourses = getImportedCourses(manageTerm), 

562 terms = selectSurroundingTerms(g.current_term), 

563 term = manageTerm, 

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

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

566 ) 

567 

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

569def getSidebarInformation() -> str: 

570 """ 

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

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

573 ajax request. 

574 """ 

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

576 interestedStudentsCount: int = len(getMinorInterest()) 

577 return {"unapprovedCoursesCount": unapprovedCoursesCount, 

578 "interestedStudentsCount": interestedStudentsCount} 

579 

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

581def removeFromSession(): 

582 try: 

583 session.pop('cpPreview') 

584 except KeyError: 

585 pass 

586 

587 return "" 

588 

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

590def alterImportedCourse(courseID): 

591 """ 

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

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

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

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

596 coming from the imported courses modal.  

597 """ 

598 if request.method == 'GET': 

599 try: 

600 targetCourse = Course.get_by_id(courseID) 

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

602 

603 try: 

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

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

606 serviceHours = 20 

607 

608 courseData = model_to_dict(targetCourse, recurse=False) 

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

610 courseData['hoursEarned'] = serviceHours 

611 

612 return jsonify(courseData) 

613 

614 except DoesNotExist: 

615 flash("Course not found") 

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

617 

618 if request.method == 'POST': 

619 # Update course information in the database 

620 courseData = request.form.copy() 

621 editImportedCourses(courseData) 

622 session['alterCourseId'] = courseID 

623 

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

625 

626 

627@admin_bp.route("/manageBonner") 

628def manageBonner(): 

629 if not g.current_user.isCeltsAdmin: 

630 abort(403) 

631 

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

633 cohorts=getBonnerCohorts(), 

634 events=getBonnerEvents(g.current_term), 

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

636 

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

638def updatecohort(year, method, username): 

639 if not g.current_user.isCeltsAdmin: 

640 abort(403) 

641 

642 try: 

643 user = User.get_by_id(username) 

644 except: 

645 abort(500) 

646 

647 if method == "add": 

648 try: 

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

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

651 except IntegrityError as e: 

652 # if they already exist, ignore the error 

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

654 pass 

655 

656 elif method == "remove": 

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

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

659 else: 

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

661 abort(500) 

662 return "" 

663 

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

665def getBonnerXls(startingYear, noOfYears): 

666 if not g.current_user.isCeltsAdmin: 

667 abort(403) 

668 newfile = makeBonnerXls(startingYear, noOfYears) 

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

670 

671 

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

673def saveRequirements(certid): 

674 if not g.current_user.isCeltsAdmin: 

675 abort(403) 

676 

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

678 

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

680 

681 

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

683def displayEventFile(): 

684 fileData = request.form 

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

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

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

688 return ""