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

318 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-01-04 19:34 +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.program import Program 

11from app.models.event import Event 

12from app.models.user import User 

13from app.models.eventTemplate import EventTemplate 

14from app.models.adminLog import AdminLog 

15from app.models.eventRsvpLog import EventRsvpLog 

16from app.models.attachmentUpload import AttachmentUpload 

17from app.models.bonnerCohort import BonnerCohort 

18from app.models.certification import Certification 

19from app.models.user import User 

20from app.models.term import Term 

21from app.models.eventViews import EventView 

22from app.models.courseStatus import CourseStatus 

23 

24from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates 

25from app.logic.createLogs import createAdminLog 

26from app.logic.certification import getCertRequirements, updateCertRequirements 

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

28from app.logic.events import cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, calculateRecurringEventFrequency, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCountsForTerm 

29from app.logic.participants import getEventParticipants, getParticipationStatusForTrainings, checkUserRsvp 

30from app.logic.fileHandler import FileHandler 

31from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort 

32from app.controllers.admin import admin_bp 

33from app.logic.manageSLFaculty import getInstructorCourses 

34from app.logic.courseManagement import unapprovedCourses, approvedCourses 

35from app.logic.serviceLearningCoursesData import parseUploadedFile, saveCourseParticipantsToDatabase 

36 

37 

38 

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

40def switchUser(): 

41 if app.env == "production": 

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

43 abort(403) 

44 

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

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

47 

48 return redirect(request.referrer) 

49 

50 

51@admin_bp.route('/eventTemplates') 

52def templateSelect(): 

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

54 allprograms = getAllowedPrograms(g.current_user) 

55 visibleTemplates = getAllowedTemplates(g.current_user) 

56 return render_template("/events/template_selector.html", 

57 programs=allprograms, 

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

59 templates=visibleTemplates) 

60 else: 

61 abort(403) 

62 

63 

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

65def createEvent(templateid, programid): 

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

67 abort(403) 

68 

69 # Validate given URL 

70 program = None 

71 try: 

72 template = EventTemplate.get_by_id(templateid) 

73 if programid: 

74 program = Program.get_by_id(programid) 

75 except DoesNotExist as e: 

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

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

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

79 

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

81 eventData = template.templateData 

82 

83 eventData['program'] = program 

84 

85 if request.method == "GET": 

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

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

88 if program: 

89 eventData['location'] = program.defaultLocation 

90 if program.contactName: 

91 eventData['contactName'] = program.contactName 

92 if program.contactEmail: 

93 eventData['contactEmail'] = program.contactEmail 

94 

95 # Try to save the form 

96 if request.method == "POST": 

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

98 try: 

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

100 

101 except Exception as e: 

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

103 savedEvents = False 

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

105 

106 if savedEvents: 

107 rsvpcohorts = request.form.getlist("cohorts[]") 

108 for year in rsvpcohorts: 

109 rsvpForBonnerCohort(int(year), savedEvents[0].id) 

110 

111 noun = (eventData['isRecurring'] == 'on' and "Events" or "Event") # pluralize 

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

113 

114 if program: 

115 if len(savedEvents) > 1: 

116 createAdminLog(f"Created a recurring 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')}. The last event in the series will be on {datetime.strftime(savedEvents[-1].startDate, '%m/%d/%Y')}.") 

117 else: 

118 createAdminLog(f"Created <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')}.") 

119 else: 

120 createAdminLog(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')}.") 

121 

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

123 else: 

124 flash(validationErrorMessage, 'warning') 

125 

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

127 preprocessEventData(eventData) 

128 isProgramManager = g.current_user.isProgramManagerFor(programid) 

129 

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

131 

132 requirements, bonnerCohorts = [], [] 

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

134 requirements = getCertRequirements(Certification.BONNER) 

135 bonnerCohorts = getBonnerCohorts(limit=5) 

136 return render_template(f"/admin/{template.templateFile}", 

137 template = template, 

138 eventData = eventData, 

139 futureTerms = futureTerms, 

140 requirements = requirements, 

141 bonnerCohorts = bonnerCohorts, 

142 isProgramManager = isProgramManager) 

143 

144 

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

146def rsvpLogDisplay(eventId): 

147 event = Event.get_by_id(eventId) 

148 eventData = model_to_dict(event, recurse=False) 

149 eventData['program'] = event.program 

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

151 if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and isProgramManager): 

152 allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc()) 

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

154 event = event, 

155 eventData = eventData, 

156 allLogs = allLogs) 

157 else: 

158 abort(403) 

159 

160 

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

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

163def eventDisplay(eventId): 

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

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

166 viewer = g.current_user 

167 event = Event.get_by_id(eventId) 

168 addEventView(viewer,event) 

169 # Validate given URL 

170 try: 

171 event = Event.get_by_id(eventId) 

172 except DoesNotExist as e: 

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

174 abort(404) 

175 

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

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

178 abort(403) 

179 

180 eventData = model_to_dict(event, recurse=False) 

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

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

183 

184 image = None 

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

186 for attachment in associatedAttachments: 

187 for extension in picurestype: 

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

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

190 if image: 

191 break 

192 

193 

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

195 eventData = request.form.copy() 

196 try: 

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

198 

199 except Exception as e: 

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

201 savedEvents = False 

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

203 

204 

205 if savedEvents: 

206 rsvpcohorts = request.form.getlist("cohorts[]") 

207 for year in rsvpcohorts: 

208 rsvpForBonnerCohort(int(year), event.id) 

209 

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

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

212 else: 

213 flash(validationErrorMessage, 'warning') 

214 

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

216 preprocessEventData(eventData) 

217 eventData['program'] = event.program 

218 futureTerms = selectSurroundingTerms(g.current_term) 

219 userHasRSVPed = checkUserRsvp(g.current_user, event) 

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

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

222 requirements, bonnerCohorts = [], [] 

223 

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

225 requirements = getCertRequirements(Certification.BONNER) 

226 bonnerCohorts = getBonnerCohorts(limit=5) 

227 

228 rule = request.url_rule 

229 

230 # Event Edit 

231 if 'edit' in rule.rule: 

232 return render_template("admin/createEvent.html", 

233 eventData = eventData, 

234 futureTerms=futureTerms, 

235 event = event, 

236 requirements = requirements, 

237 bonnerCohorts = bonnerCohorts, 

238 userHasRSVPed = userHasRSVPed, 

239 isProgramManager = isProgramManager, 

240 filepaths = filepaths) 

241 # Event View 

242 else: 

243 # get text representations of dates 

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

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

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

247 

248 # Identify the next event in a recurring series 

249 if event.recurringId: 

250 eventSeriesList = list(Event.select().where(Event.recurringId == event.recurringId) 

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

252 .order_by(Event.startDate)) 

253 eventIndex = eventSeriesList.index(event) 

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

255 eventData["nextRecurringEvent"] = eventSeriesList[eventIndex + 1] 

256 

257 currentEventRsvpAmount = getEventRsvpCountsForTerm(g.current_term) 

258 

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

260 

261 return render_template("eventView.html", 

262 eventData = eventData, 

263 event = event, 

264 userHasRSVPed = userHasRSVPed, 

265 programTrainings = userParticipatedTrainingEvents, 

266 currentEventRsvpAmount = currentEventRsvpAmount, 

267 isProgramManager = isProgramManager, 

268 filepaths = filepaths, 

269 image = image, 

270 pageViewsCount= pageViewsCount) 

271 

272 

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

274def cancelRoute(eventId): 

275 if g.current_user.isAdmin: 

276 try: 

277 cancelEvent(eventId) 

278 return redirect(request.referrer) 

279 

280 except Exception as e: 

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

282 return "", 500 

283 

284 else: 

285 abort(403) 

286 

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

288def deleteRoute(eventId): 

289 try: 

290 deleteEvent(eventId) 

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

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

293 

294 except Exception as e: 

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

296 return "", 500 

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

298def deleteEventAndAllFollowingRoute(eventId): 

299 try: 

300 deleteEventAndAllFollowing(eventId) 

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

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

303 

304 except Exception as e: 

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

306 return "", 500 

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

308def deleteAllRecurringEventsRoute(eventId): 

309 try: 

310 deleteAllRecurringEvents(eventId) 

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

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

313 

314 except Exception as e: 

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

316 return "", 500 

317 

318@admin_bp.route('/makeRecurringEvents', methods=['POST']) 

319def addRecurringEvents(): 

320 recurringEvents = calculateRecurringEventFrequency(preprocessEventData(request.form.copy())) 

321 return json.dumps(recurringEvents, default=str) 

322 

323 

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

325def userProfile(): 

326 volunteerName= request.form.copy() 

327 if volunteerName['searchStudentsInput']: 

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

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

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

331 else: 

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

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

334 

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

336def studentSearchPage(): 

337 if g.current_user.isAdmin: 

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

339 abort(403) 

340 

341@admin_bp.route('/addParticipants', methods = ['GET']) 

342def addParticipants(): 

343 '''Renders the page, will be removed once merged with full page''' 

344 

345 return render_template('addParticipants.html', 

346 title="Add Participants") 

347 

348@admin_bp.route('/adminLogs', methods = ['GET', 'POST']) 

349def adminLogs(): 

350 if g.current_user.isCeltsAdmin: 

351 allLogs = AdminLog.select(AdminLog, User).join(User).order_by(AdminLog.createdOn.desc()) 

352 return render_template("/admin/adminLogs.html", 

353 allLogs = allLogs) 

354 else: 

355 abort(403) 

356 

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

358def deleteEventFile(): 

359 fileData= request.form 

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

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

362 return "" 

363 

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

365def addCourseFile(): 

366 fileData = request.files['addCourseParticipants'] 

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

368 fileData.save(filePath) 

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

370 os.remove(filePath) 

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

372 

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

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

375def manageServiceLearningCourses(term=None): 

376 """ 

377 The SLC management page for admins 

378 """ 

379 if not g.current_user.isCeltsAdmin: 

380 abort(403) 

381 

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

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

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

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

386 

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

388 

389 setRedirectTarget(request.full_path) 

390 

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

392 courseInstructors = getInstructorCourses(), 

393 unapprovedCourses = unapprovedCourses(term), 

394 approvedCourses = approvedCourses(term), 

395 terms = selectSurroundingTerms(g.current_term), 

396 term = manageTerm, 

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

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

399 ) 

400 

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

402def removeFromSession(): 

403 try: 

404 session.pop('cpPreview') 

405 except KeyError: 

406 pass 

407 

408 return "" 

409 

410@admin_bp.route("/manageBonner") 

411def manageBonner(): 

412 if not g.current_user.isCeltsAdmin: 

413 abort(403) 

414 

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

416 cohorts=getBonnerCohorts(), 

417 events=getBonnerEvents(g.current_term), 

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

419 

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

421def updatecohort(year, method, username): 

422 if not g.current_user.isCeltsAdmin: 

423 abort(403) 

424 

425 try: 

426 user = User.get_by_id(username) 

427 except: 

428 abort(500) 

429 

430 if method == "add": 

431 try: 

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

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

434 except IntegrityError as e: 

435 # if they already exist, ignore the error 

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

437 pass 

438 

439 elif method == "remove": 

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

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

442 else: 

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

444 abort(500) 

445 

446 return "" 

447 

448@admin_bp.route("/bonnerxls") 

449def bonnerxls(): 

450 if not g.current_user.isCeltsAdmin: 

451 abort(403) 

452 

453 newfile = makeBonnerXls() 

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

455 

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

457def saveRequirements(certid): 

458 if not g.current_user.isCeltsAdmin: 

459 abort(403) 

460 

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

462 

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

464 

465 

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

467def displayEventFile(): 

468 fileData= request.form 

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

470 eventfile.changeDisplay(fileData['id']) 

471 return ""