Skip to content

Commit

Permalink
Merge branch 'development' into BCI1
Browse files Browse the repository at this point in the history
  • Loading branch information
bledsoef authored Sep 6, 2024
2 parents 85aaf3c + 2124c98 commit 9914f5e
Show file tree
Hide file tree
Showing 38 changed files with 1,841 additions and 1,045 deletions.
14 changes: 14 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Issue Description

Fixes #add issue number
- Add issue description

## Changes

- Use bullet points to provide a description of added changes.
- Add images, where possible, to provide more context to your changes.

## Testing

- Use `backticks` to highlight shell commands or file directories in your test descriptions.
- Use bullet points to provide a concise description of testing procedure.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
* default python is Python 3

## Getting Started With CELTS in a devcontainer
1. If on Windows 10, make sure your Windows install is in developer mode so that core.symlinks will be set properly: https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development (requires admin privileges)
1. Windows
* If on Windows 10, make sure your Windows install is in developer mode so that core.symlinks will be set properly: https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development (requires admin privileges)
* In git-bash, set `git config --global core.symlink true` and cross your fingers
* It's possible after the final setup is done in VSCode you will need to fix the symlink in `database` and `app/scripts`
3. Set up an SSH agent with your GitHub SSH key. https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
4. Open repository directory in VSCode (either clone with VSCode or install git yourself and clone with ```git clone git@github.com:BCStudentSoftwareDevTeam/celts.git```)
5. Follow prompts to install Dev Container extension and open project in dev container
Expand Down Expand Up @@ -107,4 +110,4 @@ http://ssdt-documentation.berea.edu/

This is a permissions spreadsheet that lists all possible roles a user could have in the application and what permissions they are allowed. If you are adding a new role or feature please update this document:

https://docs.google.com/spreadsheets/d/1RQao6WqHZFZo0rYBPnuwnhvVI856ysqTaY0a5m3IR1Q/edit?usp=sharing
https://docs.google.com/spreadsheets/d/1RQao6WqHZFZo0rYBPnuwnhvVI856ysqTaY0a5m3IR1Q/edit?usp=sharing
106 changes: 92 additions & 14 deletions app/controllers/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,31 @@
from app.logic.createLogs import createActivityLog
from app.logic.certification import getCertRequirements, updateCertRequirements
from app.logic.utils import selectSurroundingTerms, getFilesFromRequest, getRedirectTarget, setRedirectTarget
from app.logic.events import cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, calculateRecurringEventFrequency, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent
from app.logic.events import cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, calculateRecurringEventFrequency, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId
from app.logic.events import cancelEvent, deleteEvent, attemptSaveEvent, preprocessEventData, calculateRecurringEventFrequency, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCount, copyRsvpToNewEvent, getCountdownToEvent, calculateNewMultipleOfferingId
from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp
from app.logic.minor import getMinorInterest
from app.logic.fileHandler import FileHandler
from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog
from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses

from app.controllers.admin import admin_bp
from app.logic.spreadsheet import createSpreadsheet


@admin_bp.route('/admin/reports')
def reports():
academicYears = Term.select(Term.academicYear).distinct().order_by(Term.academicYear.desc())
academicYears = list(map(lambda t: t.academicYear, academicYears))
return render_template("/admin/reports.html", academicYears=academicYears)

@admin_bp.route('/admin/reports/download', methods=['POST'])
def downloadFile():
academicYear = request.form.get('academicYear')
filepath = os.path.abspath(createSpreadsheet(academicYear))
return send_file(filepath, as_attachment=True)



@admin_bp.route('/switch_user', methods=['POST'])
def switchUser():
Expand Down Expand Up @@ -66,6 +83,7 @@ def templateSelect():

@admin_bp.route('/eventTemplates/<templateid>/<programid>/create', methods=['GET','POST'])
def createEvent(templateid, programid):
savedEventsList = []
if not (g.current_user.isAdmin or g.current_user.isProgramManagerFor(programid)):
abort(403)

Expand Down Expand Up @@ -98,13 +116,31 @@ def createEvent(templateid, programid):
# Try to save the form
if request.method == "POST":
eventData.update(request.form.copy())
try:
savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
if eventData.get('isMultipleOffering'):
multipleOfferingId = calculateNewMultipleOfferingId()

multipleOfferingData = json.loads(eventData.get('multipleOfferingData'))
for event in multipleOfferingData:
multipleOfferingDict = eventData.copy()
multipleOfferingDict.update({
'name': event['eventName'],
'startDate': event['eventDate'],
'timeStart': event['startTime'],
'timeEnd': event['endTime'],
'multipleOfferingId': multipleOfferingId
})
try:
savedEvents, validationErrorMessage = attemptSaveEvent(multipleOfferingDict, getFilesFromRequest(request))
savedEventsList.append(savedEvents)

except Exception as e:
print("Failed saving multi event", e)

except Exception as e:
print("Error saving event:", e)
savedEvents = False
validationErrorMessage = "Unknown Error Saving Event. Please try again"
else:
try:
savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request))
except Exception as e:
print("Failed saving regular event", e)

if savedEvents:
rsvpcohorts = request.form.getlist("cohorts[]")
Expand All @@ -113,14 +149,32 @@ def createEvent(templateid, programid):
addBonnerCohortToRsvpLog(int(year), savedEvents[0].id)


noun = (eventData['isRecurring'] == 'on' and "Events" or "Event") # pluralize
noun = ((eventData.get('isRecurring') or eventData.get('isMultipleOffering')) and "Events" or "Event") # pluralize
flash(f"{noun} successfully created!", 'success')



if program:
if len(savedEvents) > 1:
if len(savedEvents) > 1 and eventData.get('isRecurring'):
createActivityLog(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')}.")

elif len(savedEventsList) >= 1 and eventData.get('isMultipleOffering'):
modifiedSavedEvents = [item for sublist in savedEventsList for item in sublist]

event_dates = [event_data[0].startDate.strftime('%m/%d/%Y') for event_data in savedEventsList]

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

if len(modifiedSavedEvents) > 1:
#creates list of events created in a multiple series to display in the logs
event_list = ', '.join(event_list.split(', ')[:-1]) + f', and ' + event_list.split(', ')[-1]
#get last date and stick at the end after 'and' so that it reads like a sentence in admin log
last_event_date = event_dates[-1]
event_dates = ', '.join(event_dates[:-1]) + f', and {last_event_date}'

createActivityLog(f"Created events {event_list} for {program.programName}, with start dates of {event_dates}.")

else:
createActivityLog(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')}.")
createActivityLog(f"Created events <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')}.")
else:
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')}.")

Expand Down Expand Up @@ -187,7 +241,8 @@ def renewEvent(eventId):
'location': formData['location'],
'startDate': f'{formData["startDate"][-4:]}-{formData["startDate"][0:-5]}',
'endDate': f'{formData["endDate"][-4:]}-{formData["endDate"][0:-5]}',
'isRecurring': bool(priorEvent['recurringId'])
'isRecurring': bool(priorEvent['recurringId']),
'isMultipleOffering': bool(priorEvent['multipleOffeirngId']),
})
newEvent, message = attemptSaveEvent(newEventDict, renewedEvent = True)
if message:
Expand Down Expand Up @@ -339,10 +394,33 @@ def cancelRoute(eventId):
else:
abort(403)

@admin_bp.route('/event/undo', methods=['GET'])
def undoEvent():
try:
events = session['lastDeletedEvent']
for eventId in events:
Event.update({Event.deletionDate: None, Event.deletedBy: None}).where(Event.id == eventId).execute()
event = Event.get_or_none(Event.id == eventId)
recurringEvents = list(Event.select().where((Event.recurringId==event.recurringId) & (Event.deletionDate == None)).order_by(Event.id))
if event.recurringId is not None:
nameCounter = 1
for recurringEvent in recurringEvents:
newEventNameList = recurringEvent.name.split()
newEventNameList[-1] = f"{nameCounter}"
newEventNameList = " ".join(newEventNameList)
Event.update({Event.name: newEventNameList}).where(Event.id==recurringEvent.id).execute()
nameCounter += 1
flash("Deletion successfully undone.", "success")
return redirect('/eventsList/' + str(g.current_term))
except Exception as e:
print('Error while canceling event:', e)
return "", 500

@admin_bp.route('/event/<eventId>/delete', methods=['POST'])
def deleteRoute(eventId):
try:
deleteEvent(eventId)
session['lastDeletedEvent'] = [eventId]
flash("Event successfully deleted.", "success")
return redirect(url_for("main.events", selectedTerm=g.current_term))

Expand All @@ -353,7 +431,7 @@ def deleteRoute(eventId):
@admin_bp.route('/event/<eventId>/deleteEventAndAllFollowing', methods=['POST'])
def deleteEventAndAllFollowingRoute(eventId):
try:
deleteEventAndAllFollowing(eventId)
session["lastDeletedEvent"] = deleteEventAndAllFollowing(eventId)
flash("Events successfully deleted.", "success")
return redirect(url_for("main.events", selectedTerm=g.current_term))

Expand All @@ -364,7 +442,7 @@ def deleteEventAndAllFollowingRoute(eventId):
@admin_bp.route('/event/<eventId>/deleteAllRecurring', methods=['POST'])
def deleteAllRecurringEventsRoute(eventId):
try:
deleteAllRecurringEvents(eventId)
session["lastDeletedEvent"] = deleteAllRecurringEvents(eventId)
flash("Events successfully deleted.", "success")
return redirect(url_for("main.events", selectedTerm=g.current_term))

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/events/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import Flask, redirect, flash, url_for, request, render_template, g, json, abort
from flask import Flask, redirect, flash, url_for, request, render_template, g, json, abort, session
from datetime import datetime
from peewee import DoesNotExist

Expand Down
30 changes: 23 additions & 7 deletions app/controllers/main/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from peewee import JOIN
from http import cookies
from playhouse.shortcuts import model_to_dict
from flask import request, render_template, g, abort, flash, redirect, url_for
from flask import request, render_template, g, abort, flash, redirect, url_for, make_response, session

from app.controllers.main import main_bp
from app import app
Expand Down Expand Up @@ -93,6 +93,9 @@ def events(selectedTerm, activeTab, programID):

managersProgramDict = getManagerProgramDict(g.current_user)

# Fetch toggle state from session
toggle_state = session.get('toggleState', 'unchecked')

return render_template("/events/event_list.html",
selectedTerm = term,
studentLedEvents = studentLedEvents,
Expand All @@ -107,11 +110,21 @@ def events(selectedTerm, activeTab, programID):
activeTab = activeTab,
programID = int(programID),
managersProgramDict = managersProgramDict,
countUpcomingStudentLedEvents = countUpcomingStudentLedEvents
countUpcomingStudentLedEvents = countUpcomingStudentLedEvents,
toggle_state = toggle_state
)

@main_bp.route('/updateToggleState', methods=['POST'])
def update_toggle_state():
toggle_state = request.form.get('toggleState')

# Update session with toggle state
session['toggleState'] = toggle_state

return "", 200

@main_bp.route('/profile/<username>', methods=['GET'])
def viewUsersProfile(username):
def viewUsersProfile(username):
"""
This function displays the information of a volunteer to the user
"""
Expand All @@ -124,7 +137,7 @@ def viewUsersProfile(username):
else:
abort(403) # Error 403 if non admin/student-staff user trys to access via url

if (g.current_user == volunteer) or g.current_user.isAdmin:
if (g.current_user == volunteer) or g.current_user.isAdmin:
upcomingEvents = getUpcomingEventsForUser(volunteer)
participatedEvents = getParticipatedEventsForUser(volunteer)
programs = Program.select()
Expand Down Expand Up @@ -197,7 +210,8 @@ def emergencyContactInfo(username):
if not (g.current_user.username == username or g.current_user.isCeltsAdmin):
abort(403)


user = User.get(User.username == username)

if request.method == 'GET':
readOnly = g.current_user.username != username
contactInfo = EmergencyContact.get_or_none(EmergencyContact.user_id == username)
Expand All @@ -214,7 +228,7 @@ def emergencyContactInfo(username):
rowsUpdated = EmergencyContact.update(**request.form).where(EmergencyContact.user == username).execute()
if not rowsUpdated:
EmergencyContact.create(user = username, **request.form)
createActivityLog(f"{g.current_user} updated {username}'s emergency contact information.")
createActivityLog(f"{g.current_user.fullName} updated {user.fullName}'s emergency contact information.")
flash('Emergency contact information saved successfully!', 'success')

if request.args.get('action') == 'exit':
Expand All @@ -229,6 +243,8 @@ def insuranceInfo(username):
"""
if not (g.current_user.username == username or g.current_user.isCeltsAdmin):
abort(403)

user = User.get(User.username == username)

if request.method == 'GET':
readOnly = g.current_user.username != username
Expand All @@ -247,7 +263,7 @@ def insuranceInfo(username):
rowsUpdated = InsuranceInfo.update(**request.form).where(InsuranceInfo.user == username).execute()
if not rowsUpdated:
InsuranceInfo.create(user = username, **request.form)
createActivityLog(f"{g.current_user} updated {username}'s emergency contact information.")
createActivityLog(f"{g.current_user.fullName} updated {user.fullName}'s insurance information.")
flash('Insurance information saved successfully!', 'success')

if request.args.get('action') == 'exit':
Expand Down
16 changes: 12 additions & 4 deletions app/controllers/serviceLearning/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@
from app.models.attachmentUpload import AttachmentUpload
from app.logic.utils import selectSurroundingTerms, getFilesFromRequest
from app.logic.fileHandler import FileHandler
from app.logic.serviceLearningCourses import getSLProposalInfoForUser, withdrawProposal, renewProposal, updateCourse, createCourse, approvedCourses
from app.logic.serviceLearningCourses import getSLProposalInfoForUser, withdrawProposal, renewProposal, updateCourse, createCourse, approvedCourses, deleteCourseObject
from app.logic.downloadFile import *
from app.logic.utils import getRedirectTarget, setRedirectTarget
from app.controllers.serviceLearning import serviceLearning_bp


@serviceLearning_bp.route('/serviceLearning/courseManagement', methods = ['GET'])
@serviceLearning_bp.route('/serviceLearning/courseManagement/<username>', methods = ['GET'])
def serviceCourseManagement(username=None):
Expand Down Expand Up @@ -69,14 +68,15 @@ def slcEditProposal(courseID):
filePaths = FileHandler(courseId=course.id).retrievePath(associatedAttachments)

terms = selectSurroundingTerms(g.current_term, 0)

return render_template('serviceLearning/slcNewProposal.html',
course = course,
questionanswers = questionAnswers,
terms = terms,
statusOfCourse = statusOfCourse,
courseInstructor = courseInstructor,
filePaths = filePaths,
courseStatus = courseStatus,
redirectTarget = getRedirectTarget())

else:
Expand All @@ -87,9 +87,17 @@ def slcEditProposal(courseID):
def slcCreateCourse():
"""will give a new course ID so that it can redirect to an edit page"""
course = createCourse(g.current_user)

return redirect(url_for('serviceLearning.slcEditProposal', courseID = course.id))

@serviceLearning_bp.route('/serviceLearning/canceledProposal', methods=['POST'])
def slcCancelProposal():
courseID = request.form.get('courseID')
course = Course.get_by_id(courseID)
if not course.courseName and not course.courseAbbreviation:
CourseQuestion.delete().where(CourseQuestion.course == course).execute()
course.delete_instance()
return "Proposal Canceled"


@serviceLearning_bp.route('/serviceLearning/exit', methods=['GET'])
def slcExitView():
Expand Down
Loading

0 comments on commit 9914f5e

Please sign in to comment.