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
« 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
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
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
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)
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']))
48 return redirect(request.referrer)
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)
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)
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"))
80 # Get the data from the form or from the template
81 eventData = template.templateData
83 eventData['program'] = program
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
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))
101 except Exception as e:
102 print("Error saving event:", e)
103 savedEvents = False
104 validationErrorMessage = "Unknown Error Saving Event. Please try again"
106 if savedEvents:
107 rsvpcohorts = request.form.getlist("cohorts[]")
108 for year in rsvpcohorts:
109 rsvpForBonnerCohort(int(year), savedEvents[0].id)
111 noun = (eventData['isRecurring'] == 'on' and "Events" or "Event") # pluralize
112 flash(f"{noun} successfully created!", 'success')
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')}.")
122 return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id))
123 else:
124 flash(validationErrorMessage, 'warning')
126 # make sure our data is the same regardless of GET or POST
127 preprocessEventData(eventData)
128 isProgramManager = g.current_user.isProgramManagerFor(programid)
130 futureTerms = selectSurroundingTerms(g.current_term, prevTerms=0)
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)
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)
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)
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)
180 eventData = model_to_dict(event, recurse=False)
181 associatedAttachments = AttachmentUpload.select().where(AttachmentUpload.event == event)
182 filepaths = FileHandler(eventId=event.id).retrievePath(associatedAttachments)
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
194 if request.method == "POST": # Attempt to save form
195 eventData = request.form.copy()
196 try:
197 savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
199 except Exception as e:
200 print("Error saving event:", e)
201 savedEvents = False
202 validationErrorMessage = "Unknown Error Saving Event. Please try again"
205 if savedEvents:
206 rsvpcohorts = request.form.getlist("cohorts[]")
207 for year in rsvpcohorts:
208 rsvpForBonnerCohort(int(year), event.id)
210 flash("Event successfully updated!", "success")
211 return redirect(url_for("admin.eventDisplay", eventId = event.id))
212 else:
213 flash(validationErrorMessage, 'warning')
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 = [], []
224 if eventData['program'] and eventData['program'].isBonnerScholars:
225 requirements = getCertRequirements(Certification.BONNER)
226 bonnerCohorts = getBonnerCohorts(limit=5)
228 rule = request.url_rule
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")
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]
257 currentEventRsvpAmount = getEventRsvpCountsForTerm(g.current_term)
259 userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
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)
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)
280 except Exception as e:
281 print('Error while canceling event:', e)
282 return "", 500
284 else:
285 abort(403)
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))
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))
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))
314 except Exception as e:
315 print('Error while canceling event:', e)
316 return "", 500
318@admin_bp.route('/makeRecurringEvents', methods=['POST'])
319def addRecurringEvents():
320 recurringEvents = calculateRecurringEventFrequency(preprocessEventData(request.form.copy()))
321 return json.dumps(recurringEvents, default=str)
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'))
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)
341@admin_bp.route('/addParticipants', methods = ['GET'])
342def addParticipants():
343 '''Renders the page, will be removed once merged with full page'''
345 return render_template('addParticipants.html',
346 title="Add Participants")
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)
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 ""
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"))
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)
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'))
387 manageTerm = Term.get_or_none(Term.id == term) or g.current_term
389 setRedirectTarget(request.full_path)
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 )
401@admin_bp.route("/deleteUploadedFile", methods= ["POST"])
402def removeFromSession():
403 try:
404 session.pop('cpPreview')
405 except KeyError:
406 pass
408 return ""
410@admin_bp.route("/manageBonner")
411def manageBonner():
412 if not g.current_user.isCeltsAdmin:
413 abort(403)
415 return render_template("/admin/bonnerManagement.html",
416 cohorts=getBonnerCohorts(),
417 events=getBonnerEvents(g.current_term),
418 requirements = getCertRequirements(certification=Certification.BONNER))
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)
425 try:
426 user = User.get_by_id(username)
427 except:
428 abort(500)
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
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)
446 return ""
448@admin_bp.route("/bonnerxls")
449def bonnerxls():
450 if not g.current_user.isCeltsAdmin:
451 abort(403)
453 newfile = makeBonnerXls()
454 return send_file(open(newfile, 'rb'), download_name='BonnerStudents.xlsx', as_attachment=True)
456@admin_bp.route("/saveRequirements/<certid>", methods=["POST"])
457def saveRequirements(certid):
458 if not g.current_user.isCeltsAdmin:
459 abort(403)
461 newRequirements = updateCertRequirements(certid, request.get_json())
463 return jsonify([requirement.id for requirement in newRequirements])
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 ""