diff --git a/app/config/default.yml b/app/config/default.yml index b480cc7d2..6bc439695 100644 --- a/app/config/default.yml +++ b/app/config/default.yml @@ -4,7 +4,7 @@ default_user: "ramsayb2" celts_admin_contact: "celts@berea.edu" support_email_contact: "support@bereacollege.onmicrosoft.com" -show_queries: True +show_queries: False test_entry: "Default" db: diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 8d24ffdf6..daf50ab1b 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -1,6 +1,6 @@ from flask import request, render_template, url_for, g, redirect from flask import flash, abort, jsonify, session, send_file -from peewee import DoesNotExist, fn, IntegrityError +from peewee import DoesNotExist, IntegrityError from playhouse.shortcuts import model_to_dict import json from datetime import datetime @@ -27,6 +27,7 @@ from app.models.term import Term from app.models.eventViews import EventView from app.models.courseStatus import CourseStatus +from app.models.programManager import ProgramManager from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates from app.logic.createLogs import createActivityLog @@ -75,6 +76,13 @@ def templateSelect(): if not programs: abort(403) visibleTemplates = getAllowedTemplates(g.current_user) + + # Create extravaganza QR code if it doesn't exist + imgPath = app.config["files"]["image_path"] + "/extravaganza_qr.png" + if not os.path.exists(imgPath): + import qrcode + img = qrcode.make(request.url.split("/")[-2] + "/extravaganza") + img.save(imgPath) return render_template("/events/templateSelector.html", programs=programs, celtsSponsoredProgram = Program.get(Program.isOtherCeltsSponsored), @@ -97,7 +105,7 @@ def createEvent(templateid, programid): return redirect(url_for("admin.program_picker")) # Get the data from the form or from the template - eventData = template.templateData + eventData = template.templateData eventData['program'] = program if request.method == "GET": @@ -113,9 +121,8 @@ def createEvent(templateid, programid): # Try to save the form if request.method == "POST": savedEvents = None - eventData.update(request.form.copy()) + eventData.update(request.form.copy()) eventData = preprocessEventData(eventData) - if eventData.get('isSeries'): eventData['seriesData'] = json.loads(eventData['seriesData']) succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) @@ -128,9 +135,8 @@ def createEvent(templateid, programid): try: savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request)) except Exception as e: - print("Failed saving regular event", e) + print("Failed saving regular event: ", e) validationErrorMessage = "Failed to save event." - if savedEvents: rsvpCohorts = request.form.getlist("cohorts[]") if rsvpCohorts: @@ -144,10 +150,10 @@ def createEvent(templateid, programid): if program: if len(savedEvents) > 1 and eventData.get('isRepeating'): - createActivityLog(f"Created a repeating series, {savedEvents[0].name[:-7]}, 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')}.") + createActivityLog(f'''Created a repeating series, {savedEvents[0].name[:-7]}, 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')}.''') elif len(savedEvents) >= 1 and eventData.get('isSeries'): eventDates = [eventData.startDate.strftime('%m/%d/%Y') for eventData in savedEvents] - eventList = ', '.join(f"{event.name}" for event in savedEvents) + eventList = ', '.join(f'''{event.name}''' for event in savedEvents) if len(savedEvents) > 1: #creates list of events created in a multiple series to display in the logs @@ -159,9 +165,9 @@ def createEvent(templateid, programid): createActivityLog(f"Created series {eventList} for {program.programName}, with start dates of {eventDates}.") else: - createActivityLog(f"Created event {savedEvents[0].name} for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.") + createActivityLog('''Created event {savedEvents[0].name} for {program.programName}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.''') else: - createActivityLog(f"Created a non-program event, {savedEvents[0].name}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.") + createActivityLog(f'''Created a non-program event, {savedEvents[0].name}, with a start date of {datetime.strftime(eventData['startDate'], '%m/%d/%Y')}.''') return redirect(url_for("admin.eventDisplay", eventId = savedEvents[0].id)) else: @@ -180,8 +186,6 @@ def createEvent(templateid, programid): for year, cohort in rawBonnerCohorts.items(): if cohort: bonnerCohorts[year] = cohort - - return render_template(f"/events/{template.templateFile}", template = template, eventData = eventData, @@ -239,7 +243,7 @@ def renewEvent(eventId): return redirect(url_for('admin.eventDisplay', eventId = eventId)) copyRsvpToNewEvent(priorEvent, newEvent[0]) - createActivityLog(f"Renewed {priorEvent['name']} as {newEvent[0].name}.") + createActivityLog(f'''Renewed {priorEvent['name']} as {newEvent[0].name}.''') flash("Event successfully renewed.", "success") return redirect(url_for('admin.eventDisplay', eventId = newEvent[0].id)) @@ -289,10 +293,14 @@ def eventDisplay(eventId): if request.method == "POST": # Attempt to save form - eventData = request.form.copy() + eventData = request.form.copy() try: savedEvents, validationErrorMessage = attemptSaveEvent(eventData, getFilesFromRequest(request)) + except IntegrityError as e: + print("Error saving event:", e) + savedEvents = False + validationErrorMessage = "This combination of settings is not allowed." except Exception as e: print("Error saving event:", e) savedEvents = False @@ -682,3 +690,16 @@ def displayEventFile(): isChecked = fileData.get('checked') == 'true' eventfile.changeDisplay(fileData['id'], isChecked) return "" + +@admin_bp.route("/handbookSignature", methods=["POST"]) +def handbookSignature(): + data = request.get_json() + if not (g.current_user.username == data.get('studentID')): + abort(403) + signer = User.get(User.username == g.current_user.username) + if signer: + signer.lastHandbookSignature = datetime.now().strftime("%Y-%m-%d") + signer.signatureTerm = g.current_term + signer.save() + return "", 200 + abort(403) \ No newline at end of file diff --git a/app/controllers/admin/userManagement.py b/app/controllers/admin/userManagement.py index ca944b7af..0affdf532 100644 --- a/app/controllers/admin/userManagement.py +++ b/app/controllers/admin/userManagement.py @@ -1,19 +1,28 @@ -from flask import render_template,request, flash, g, abort, redirect, url_for, jsonify +import os +from pathlib import Path + +from app.models.term import Term +from flask import render_template,request, flash, g, abort, redirect, send_file, url_for, jsonify, session from playhouse.shortcuts import model_to_dict from peewee import fn, JOIN, DoesNotExist import re +from werkzeug.utils import secure_filename + from app.controllers.admin import admin_bp from app.models.user import User from app.models.program import Program from app.logic.fileHandler import FileHandler -from app.logic.userManagement import addCeltsAdmin,addCeltsStudentStaff,removeCeltsAdmin,removeCeltsStudentStaff +from app.logic.userManagement import addCeltsAdmin,addCeltsStudentStaff, createSpreadsheetForRosters,removeCeltsAdmin,removeCeltsStudentStaff from app.logic.userManagement import changeProgramInfo +from app.logic.participants import getTrainingsForInterestedParticipants, getParticipantsForProgramForAY from app.logic.utils import selectSurroundingTerms from app.logic.term import addNextTerm, changeCurrentTerm +from app.logic.users import getProgramInterest from app.logic.volunteers import setProgramManager from app.models.attachmentUpload import AttachmentUpload from app.models.programManager import ProgramManager +from app.models.programBan import ProgramBan from app.models.user import User @admin_bp.route('/admin/manageUsers', methods = ['POST']) @@ -118,12 +127,14 @@ def userManagement(): currentAdmins = list(User.select().where(User.isCeltsAdmin)) currentStudentStaff = list(User.select().where(User.isCeltsStudentStaff)) + currentTerm = Term.get(Term.isCurrentTerm) if g.current_user.isCeltsAdmin or g.current_user.isProgramManager: return render_template('admin/userManagement.html', terms = terms, programs = list(currentPrograms), currentAdmins = currentAdmins, currentStudentStaff = currentStudentStaff, + currentTerm = currentTerm ) abort(403) @@ -131,11 +142,61 @@ def userManagement(): def changeTerm(): termData = request.form term = int(termData["id"]) - changeCurrentTerm(term) - return "" + newTerm = changeCurrentTerm(term) + return model_to_dict(newTerm) @admin_bp.route('/admin/addNewTerm', methods = ['POST']) def addNewTerm(): addNextTerm() flash("New term added", "success") return "" + +@admin_bp.route('/upload//', methods = ['POST']) +def upload(fileCategory, currentTerm): + term = Term.select().where(Term.id == currentTerm).get() + allAYterm = Term.select().where(Term.academicYear == term.academicYear) + if not fileCategory in ["laborHandbook", "volunteerHandbook"]: + abort(405) + dir_path = Path("app/static/files/", fileCategory) + dir_path.mkdir(parents=True, exist_ok=True) + file = request.files[fileCategory] + filename = g.current_term.academicYear + "-" + fileCategory + "." + secure_filename(file.filename).split(".")[-1] + full_path = os.path.join(dir_path, filename) + if os.path.exists(full_path): + os.remove(full_path) + file.save(full_path) + for t in allAYterm: + if fileCategory == "volunteerHandbook": + t.volunteerHandbook = filename + elif fileCategory == "laborHandbook": + t.laborHandbook = filename + else: + abort(405) + t.save() + g.current_term = term + flash(f"Handbook saved successfully to {term.description}!", "success") + return redirect(request.referrer) + +@admin_bp.route('/viewRoster/', methods = ['GET']) +def viewRoster(programID): + program = Program.get_by_id(programID) + interestedUsers = list(getProgramInterest(program)) + trainedAndInterested = getTrainingsForInterestedParticipants(programID, interestedUsers) + lastYearsParticipants = getParticipantsForProgramForAY(programID, g.current_term.previousAcademicYear) + currentYearsParticipants = getParticipantsForProgramForAY(programID, g.current_term.academicYear) + return render_template('admin/viewRoster.html', + program = program, + interestedUsers = interestedUsers, + trainedAndInterested = trainedAndInterested, + lastYearsParticipants = lastYearsParticipants, + currentYearsParticipants = currentYearsParticipants + ) + +@admin_bp.route('/exportRosters//', methods = ['GET']) +def exportRosters(programID, academicYear): + try: + outFile = createSpreadsheetForRosters(academicYear, programID) + filepath = os.path.abspath(outFile) + return send_file(filepath, as_attachment=True, download_name=filepath.split("/")[-1], mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + except DoesNotExist: + abort(403) \ No newline at end of file diff --git a/app/controllers/admin/volunteers.py b/app/controllers/admin/volunteers.py index c16012803..7df4e649f 100644 --- a/app/controllers/admin/volunteers.py +++ b/app/controllers/admin/volunteers.py @@ -11,12 +11,12 @@ from app.models.insuranceInfo import InsuranceInfo from app.logic.searchUsers import searchUsers from app.logic.volunteers import updateEventParticipants, getEventLengthInHours, addUserBackgroundCheck, setProgramManager, deleteUserBackgroundCheck -from app.logic.participants import trainedParticipants, addPersonToEvent, getParticipationStatusForTrainings, sortParticipantsByStatus +from app.logic.participants import addPersonToEvent, getParticipationStatusForTrainings, sortParticipantsByStatus from app.logic.events import getPreviousSeriesEventData, getEventRsvpCount from app.models.eventRsvp import EventRsvp from app.models.backgroundCheck import BackgroundCheck from app.logic.createLogs import createActivityLog, createRsvpLog -from app.logic.users import getBannedUsers, isBannedFromEvent +from app.logic.users import getBannedUsers, isBannedFromEvent, trainedParticipants @admin_bp.route('/searchVolunteers/', methods = ['GET']) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 6f83f32dd..2e90cb60e 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -4,6 +4,8 @@ from http import cookies from playhouse.shortcuts import model_to_dict from flask import request, render_template, jsonify, g, abort, flash, redirect, url_for, make_response, session, request +from dateutil.relativedelta import relativedelta + from app.controllers.main import main_bp from app import app @@ -36,8 +38,8 @@ from app.logic.certification import getCertRequirementsWithCompletion from app.logic.landingPage import getManagerProgramDict, getActiveEventTab from app.logic.minor import toggleMinorInterest, declareMinorInterest, getCommunityEngagementByTerm, getEngagementTotal -from app.logic.participants import unattendedRequiredEvents, trainedParticipants, getParticipationStatusForTrainings, checkUserRsvp, addPersonToEvent -from app.logic.users import addUserInterest, removeUserInterest, banUser, unbanUser, isEligibleForProgram, getUserBGCheckHistory, addProfileNote, deleteProfileNote, updateDietInfo +from app.logic.participants import hasGoneToTraining, unattendedRequiredEvents, getParticipationStatusForTrainings, checkUserRsvp, addPersonToEvent +from app.logic.users import addUserInterest, isBannedFromEvent, removeUserInterest, banUser, unbanUser, isEligibleForProgram, getUserBGCheckHistory, addProfileNote, deleteProfileNote, updateDietInfo, trainedParticipants @main_bp.route('/logout', methods=['GET']) def redirectToLogout(): @@ -196,7 +198,6 @@ def viewUsersProfile(username): allBackgroundHistory = getUserBGCheckHistory(volunteer) backgroundTypes = list(BackgroundCheckType.select()) - eligibilityTable = [] @@ -233,6 +234,10 @@ def viewUsersProfile(username): managersProgramDict = getManagerProgramDict(g.current_user) managersList = [id[1] for id in managersProgramDict.items()] totalSustainedEngagements = getEngagementTotal(getCommunityEngagementByTerm(volunteer)) + handbookOverdue = getHandbookStatus(volunteer) + currentTerm = Term.get(Term.isCurrentTerm) + + training = hasGoneToTraining(g.current_user, g.current_term) return render_template ("/main/userProfile.html", username=username, @@ -252,9 +257,19 @@ def viewUsersProfile(username): managersList = managersList, participatedInLabor = getCeltsLaborHistory(volunteer), totalSustainedEngagements = totalSustainedEngagements, + handbookOverdue = handbookOverdue, + training = training, + currentTerm = currentTerm ) abort(403) +def getHandbookStatus(volunteer): + handbookOverdue = False + + if not volunteer.signatureTerm or volunteer.signatureTerm.academicYear != g.current_term.academicYear: + handbookOverdue = True + return handbookOverdue + @main_bp.route('/profile//emergencyContact', methods=['GET', 'POST']) def emergencyContactInfo(username): """ @@ -452,21 +467,24 @@ def unban(program_id, username): flash("Failed to unban the volunteer", "danger") return "Failed to unban the volunteer", 500 - @main_bp.route('//addInterest/', methods=['POST']) -def addInterest(program_id, username): +@main_bp.route('//addInterest//', methods=['POST']) +def addInterest(program_id, username, showFlash = True): """ This function adds a program to the list of programs a user interested in program_id: the primary id of the program the student is adding interest of username: unique value of a user to correctly identify them - """ + """ + showFlash = False if showFlash == "False" else True try: success = addUserInterest(program_id, username) if success: - flash("Successfully added " + Program.get_by_id(program_id).programName + " as an interest", "success") - return "" + if bool(showFlash): + flash("Successfully added " + Program.get_by_id(program_id).programName + " as an interest", "success") + return jsonify(model_to_dict(User.get_or_none(User.username == username))) else: - flash("Was unable to remove " + Program.get_by_id(program_id).programName + " as an interest.", "danger") + if bool(showFlash): + flash("Was unable to add " + Program.get_by_id(program_id).programName + " as an interest.", "danger") except Exception as e: print(e) @@ -499,8 +517,8 @@ def volunteerRegister(): event = Event.get_by_id(request.form['id']) program = event.program user = g.current_user - - isEligible = isEligibleForProgram(program, user) + now = datetime.datetime.now() + isEligible = False if isBannedFromEvent(user, event) else True personAdded = False if isEligible: @@ -643,3 +661,28 @@ def updateMinorDeclaration(username): tab = request.args.get("tab", "interested") return redirect(url_for('admin.manageMinor', tab=tab)) +@main_bp.route('/extravaganza', methods=['GET']) +def extravaganza(): + programs = Program.select().where(Program.isOtherCeltsSponsored == False, + Program.programName != "Hunger Initiatives", + Program.programName != "Bonner Scholars") + interests = Interest.select(Interest, Program).join(Program).where(Interest.user == g.current_user) + programsInterested = [interest.program for interest in interests] + + upcomingAllVolunteers = Event.select().join(Term).where(Event.isAllVolunteerTraining, Term.academicYear == g.current_term.academicYear) + for training in upcomingAllVolunteers: + training.startDate = training.startDate.strftime("%b %d") + training.timeStart = training.timeStart.strftime("%I:%M %p") + + upcomingTrainings = Event.select().join(Term).where(Event.isTraining, Term.academicYear == g.current_term.academicYear) + + for training in upcomingTrainings: + training.startDate = training.startDate.strftime("%b %d") + training.timeStart = training.timeStart.strftime("%I:%M %p") + + return render_template("main/extravanganzaWelcome.html", + programs = programs, + programsInterested = programsInterested, + upcomingTrainings = upcomingTrainings, + upcomingAllVolunteers = upcomingAllVolunteers + ) \ No newline at end of file diff --git a/app/logic/events.py b/app/logic/events.py index 8a26f58fd..2577aa85a 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -119,8 +119,19 @@ def attemptSaveMultipleOfferings(eventData, attachmentFiles = None): seriesId = calculateNewSeriesId() # Create separate event data for each event in the series, inheriting from the original eventData + + # Reformat dates from Jul 10, 2026 to 2026-07-10 for easier sorting + eventData2 = [] + for ed in eventData['seriesData']: + try: + ed['eventDate'] = datetime.strptime(ed['eventDate'], "%b %d, %Y").strftime("%Y-%m-%d") + except: + pass # eventDate comes into the system differently for recurring weekly events and recurring events (non-weekly). This should format it correctly for both cases + eventData2.append(ed) + eventData['seriesData'] = eventData2 + seriesData = sorted(eventData.get('seriesData'), key=lambda x: datetime.strptime(x['eventDate'].split(' ')[0] + ' ' + x['startTime'], '%Y-%m-%d %H:%M')) - # sorts the events in the series by date and time so that the events are created in order and the naming convention of Week 1, Week 2, etc. is consistent with the order of the events. + # sorts the events in the series by date and time so that the events are created in order and the naming convention of Week 1, Week 2, etc. is consistent with the order of the events. isRepeating = bool(eventData.get('isRepeating')) with mainDB.atomic() as transaction: for index, event in enumerate(seriesData): @@ -164,7 +175,6 @@ def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False): # automatically changed from "" to 0 if eventData["rsvpLimit"] == "": eventData["rsvpLimit"] = None - newEventData = preprocessEventData(eventData) isValid, validationErrorMessage = validateNewEventData(newEventData) @@ -185,40 +195,42 @@ def saveEventToDb(newEventData, renewedEvent = False): raise Exception("Unvalidated data passed to saveEventToDb") isNewEvent = ('id' not in newEventData) - eventRecords = [] with mainDB.atomic(): - eventData = { - "term": newEventData['term'], - "name": newEventData['name'], - "description": newEventData['description'], - "timeStart": newEventData['timeStart'], - "timeEnd": newEventData['timeEnd'], - "location": newEventData['location'], - "isFoodProvided" : newEventData['isFoodProvided'], - "isLaborOnly" : newEventData['isLaborOnly'], - "isTraining": newEventData['isTraining'], - "isEngagement": newEventData['isEngagement'], - "isRsvpRequired": newEventData['isRsvpRequired'], - "isService": newEventData['isService'], - "startDate": newEventData['startDate'], - "rsvpLimit": newEventData['rsvpLimit'], - "contactEmail": newEventData['contactEmail'], - "contactName": newEventData['contactName'], + "term": newEventData['term'], + "name": newEventData['name'], + "description": newEventData['description'], + "timeStart": newEventData['timeStart'], + "timeEnd": newEventData['timeEnd'], + "location": newEventData['location'], + "isFoodProvided" : newEventData['isFoodProvided'], + "isLaborOnly" : newEventData['isLaborOnly'], + "allowsLabor" : newEventData['allowsLabor'], + "isTraining": newEventData['isTraining'], + "isEngagement": newEventData['isEngagement'], + "isRsvpRequired": newEventData['isRsvpRequired'], + "isService": newEventData['isService'], + "startDate": newEventData['startDate'], + "rsvpLimit": newEventData['rsvpLimit'], + "contactEmail": newEventData['contactEmail'], + "contactName": newEventData['contactName'], } - # The three fields below are only relevant during event creation so we only set/change them when + # These fields below are only relevant during event creation so we only set/change them when # it is a new event. if isNewEvent: eventData['program'] = newEventData['program'] eventData['seriesId'] = newEventData.get('seriesId') eventData['isRepeating'] = bool(newEventData.get('isRepeating')) eventData["isAllVolunteerTraining"] = newEventData['isAllVolunteerTraining'] - eventRecord = Event.create(**eventData) + eventData["isCeltsTraining"] = bool(newEventData.get('isCeltsTraining', False)) + eventRecord = Event.create(**eventData) else: eventRecord = Event.get_by_id(newEventData['id']) - Event.update(**eventData).where(Event.id == eventRecord).execute() + for key, value in eventData.items(): + setattr(eventRecord, key, value) + eventRecord.save() if 'certRequirement' in newEventData and newEventData['certRequirement'] != "": updateCertRequirementForEvent(eventRecord, newEventData['certRequirement']) @@ -230,9 +242,9 @@ def getVolunteerOpportunities(term): volunteerOpportunities = list(Event.select(Event, Program) .join(Program) .where((Event.term == term) & - (Event.deletionDate.is_null(True)) & + (Event.deletionDate.is_null()) & (Event.isService == True) & - ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) + ((Event.isLaborOnly == False) | Event.isLaborOnly.is_null()) ) .order_by(Event.startDate, Event.timeStart) .execute()) @@ -399,17 +411,15 @@ def getParticipatedEventsForUser(user): :return: A list of Event objects """ - eventName = fn.LOWER(Event.name) - checkIfLaborMeeting = eventName.contains("labor meeting") - + # Does this handle labor only and/or includes labor events? participatedEvents = (Event.select(Event, Program.programName, Case(None, ( - ((Event.isLaborOnly | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"), - ((Event.isLaborOnly | Event.name.contains("Labor")), "Labor"), + ((Event.allowsLabor | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"), + ((Event.allowsLabor | Event.isLaborOnly | Event.name.contains("Labor")), "Labor"), (Event.isService, "Volunteer")), "Attendee").alias("participatedType")) .join(Program, JOIN.LEFT_OUTER).switch() .join(EventParticipant) .where(EventParticipant.user == user, - Event.isAllVolunteerTraining == False, Event.deletionDate == None, ~checkIfLaborMeeting) + Event.isAllVolunteerTraining == False, Event.deletionDate == None, Event.isCeltsTraining == False) .order_by(Event.startDate, Event.name)) allVolunteer = (Event.select(Event, "", Value("Volunteer").alias("participatedType")) .join(EventParticipant) @@ -428,7 +438,7 @@ def validateNewEventData(data): Returns 3 values: (boolean success, the validation error message, the data object) """ - if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['isLaborOnly']]: + if 'on' in [data['isFoodProvided'], data['isRsvpRequired'], data['isTraining'], data['isEngagement'], data['isService'], data['isRepeating'], data['allowsLabor']]: return (False, "Raw form data passed to validate method. Preprocess first.") if data['timeEnd'] <= data['timeStart']: @@ -506,9 +516,10 @@ def preprocessEventData(eventData): - seriesData should be a JSON string - Look up matching certification requirement if necessary """ - ## Process checkboxes - eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'isLaborOnly'] + ## Process checkboxes and templateData + eventCheckBoxes = ['isFoodProvided', 'isRsvpRequired', 'isService', 'isTraining', 'isEngagement', 'isRepeating', 'isAllVolunteerTraining', 'allowsLabor', 'isLaborOnly', 'isCeltsTraining'] + for checkBox in eventCheckBoxes: if checkBox not in eventData: eventData[checkBox] = False @@ -551,8 +562,8 @@ def preprocessEventData(eventData): eventData['timeStart'] = format24HourTime(eventData['timeStart']) if 'timeEnd' in eventData: - eventData['timeEnd'] = format24HourTime(eventData['timeEnd']) - + eventData['timeEnd'] = format24HourTime(eventData['timeEnd']) + return eventData def getTomorrowsEvents(): @@ -736,5 +747,3 @@ def updateEventCohorts(event, cohortYears): except Exception as e: print(f"Error updating cohorts for event: {e}") return False, f"Error updating cohorts for event: {e}", [] - - diff --git a/app/logic/loginManager.py b/app/logic/loginManager.py index f21a8815e..1348c5803 100644 --- a/app/logic/loginManager.py +++ b/app/logic/loginManager.py @@ -53,4 +53,4 @@ def getLoginUser(): return user def getCurrentTerm(): - return Term.get_or_none(isCurrentTerm = True) + return Term.get_or_none(isCurrentTerm = True) \ No newline at end of file diff --git a/app/logic/participants.py b/app/logic/participants.py index 630ea0aea..01f4b88f7 100644 --- a/app/logic/participants.py +++ b/app/logic/participants.py @@ -1,44 +1,29 @@ from flask import g from peewee import fn, JOIN -from datetime import date +from playhouse.shortcuts import model_to_dict +from datetime import date, datetime +from app.logic.users import isEligibleForProgram from app.models.user import User from app.models.event import Event from app.models.term import Term from app.models.eventRsvp import EventRsvp from app.models.program import Program +from app.models.programBan import ProgramBan from app.models.eventParticipant import EventParticipant -from app.logic.users import isEligibleForProgram +from app.models.backgroundCheck import BackgroundCheck from app.logic.volunteers import getEventLengthInHours from app.logic.events import getEventRsvpCountsForTerm from app.logic.createLogs import createRsvpLog from collections import defaultdict +from app import app -def trainedParticipants(programID, targetTerm): - """ - This function tracks the users who have attended every Prerequisite - event and adds them to a list that will not flag them when tracking hours. - Returns a list of user objects who've completed all training events. - """ - - # Reset program eligibility each term for all other trainings - isRelevantAllVolunteer = (Event.isAllVolunteerTraining) & (Event.term.academicYear == targetTerm.academicYear) - isRelevantProgramTraining = (Event.program == programID) & (Event.term == targetTerm) & (Event.isTraining) - allTrainings = (Event.select() - .join(Term) - .where(isRelevantAllVolunteer | isRelevantProgramTraining, - Event.isCanceled == False)) - - fullyTrainedUsers = (User.select() - .join(EventParticipant) - .where(EventParticipant.event.in_(allTrainings)) - .group_by(EventParticipant.user) - .having(fn.Count(EventParticipant.user) == len(allTrainings)).order_by(User.username)) - return list(fullyTrainedUsers) def addBnumberAsParticipant(bnumber, eventId): - """Accepts scan input and signs in the user. If user exists or is already - signed in will return user and login status""" + """ + Accepts scan input and signs in the user. If user exists or is already + signed in will return user and login status + """ try: kioskUser = User.get(User.bnumber == bnumber) except Exception as e: @@ -46,7 +31,7 @@ def addBnumberAsParticipant(bnumber, eventId): return None, "does not exist" event = Event.get_by_id(eventId) - if not isEligibleForProgram(event.program, kioskUser): + if (ProgramBan.select().where(ProgramBan.user == kioskUser, ProgramBan.program == event.program, ProgramBan.endDate > datetime.now(), ProgramBan.unbanNote == None).exists()): userStatus = "banned" elif checkUserVolunteer(kioskUser, event): @@ -106,7 +91,6 @@ def addPersonToEvent(user, event): return True def unattendedRequiredEvents(program, user): - # Check for events that are prerequisite for program requiredEvents = (Event.select(Event) .where(Event.isTraining == True, Event.program == program)) @@ -114,7 +98,9 @@ def unattendedRequiredEvents(program, user): if requiredEvents: attendedRequiredEventsList = [] for event in requiredEvents: - attendedRequirement = (EventParticipant.select().where(EventParticipant.user == user, EventParticipant.event == event)) + attendedRequirement = (EventParticipant.select() + .join(User) + .where(EventParticipant.user == User.username, EventParticipant.event == event)) if not attendedRequirement: attendedRequiredEventsList.append(event.name) if attendedRequiredEventsList is not None: @@ -130,14 +116,14 @@ def getEventParticipants(event): return [p for p in eventParticipants] -def getParticipationStatusForTrainings(program, userList, term): +def getParticipationStatusForTrainings(program, userList, term, returnStr = True): """ This function returns a dictionary of all trainings for a program and whether the current user participated in them. :returns: trainings for program and if the user participated """ - isRelevantTraining = ((Event.isAllVolunteerTraining | ((Event.isTraining) & (Event.program == program))) & + isRelevantTraining = ((Event.isAllVolunteerTraining | Event.isCeltsTraining | ((Event.isTraining) & (Event.program == program))) & (Event.term.academicYear == term.academicYear)) programTrainings = (Event.select(Event, Term, EventParticipant, EventRsvp) .join(EventParticipant, JOIN.LEFT_OUTER).switch() @@ -165,8 +151,107 @@ def getParticipationStatusForTrainings(program, userList, term): for user in userList: if training.name not in userParticipationStatus[user.username] or user.username in attendeeList: userParticipationStatus[user.username][training.name] = [training, user.username in attendeeList] - - return {user.username: list(userParticipationStatus[user.username].values()) for user in userList} + if returnStr: + return {user.username: list(userParticipationStatus[user.username].values()) for user in userList} + else: + return {user: list(userParticipationStatus[user.username].values()) for user in userList} + +def getTrainingsForInterestedParticipants(programID, interestedUsers): + """ + Takes in a programID and a list of interested users, and returns all of the trainings and background checks they have completed. + Returns a nested dictionary which looks like the following: + {'userID1': {'userObj': , + 'allVolunteer': True, + 'programSpecific': False, + 'bgCheck': '0/22/2026', + 'eligible': True, + 'star': False + }, + 'userID2': {'userObj': , + 'allVolunteer': True, + 'programSpecific': True, + 'bgCheck': '0/22/2026', + 'eligible': True, + 'star': True + } + } + + Gracefully handles multiple trainings of the same type (e.g., two All Volunteers Trainings) + """ + trainedUsers = getParticipationStatusForTrainings(programID, interestedUsers, g.current_term, returnStr = False) + now = datetime.now() + bannedUsers = list(User + .select(User.username) + .join(ProgramBan) + .where(ProgramBan.program == programID, + ProgramBan.endDate > now, + ProgramBan.unbanNote == None, + User.username << [user.username for user in interestedUsers])) + bgCheckSubmitted = (User.select(User.username, BackgroundCheck.dateCompleted) + .join(BackgroundCheck) + .where(BackgroundCheck.user == User.username, + BackgroundCheck.deletionDate.is_null()) + .distinct()) + trainedAndInterested = {} + for interestedUser in interestedUsers: + if interestedUser in trainedUsers: + trainedAndInterested[interestedUser.username] = {} + trainedAndInterested[interestedUser.username]["userObj"] = interestedUser + trainedAndInterested[interestedUser.username]['allVolunteer'] = False + trainedAndInterested[interestedUser.username]['programSpecific'] = False + trainedAndInterested[interestedUser.username]['bgCheck'] = "Not submitted" + trainedAndInterested[interestedUser.username]["eligible"] = True + trainedAndInterested[interestedUser.username]["star"] = False + + # Go through the trainings + for event in trainedUsers[interestedUser]: + if not event[1]: # they didn't attend this training + continue + elif event[0].isAllVolunteerTraining: # They attended AVT + trainedAndInterested[interestedUser.username]["allVolunteer"] = True + elif event[0].isTraining: # They attended the Program-specific training + trainedAndInterested[interestedUser.username]["programSpecific"] = True + # They are banned + if interestedUser in bannedUsers: + trainedAndInterested[interestedUser.username]["eligible"] = False + # They submitted their background check + if interestedUser in bgCheckSubmitted: + trainedAndInterested[interestedUser.username]['bgCheck'] = "Submitted" + + # NOTE: Handbook signature already tracked inside the user object + + # Give them a star if they have met all the requirements + if ( trainedAndInterested[interestedUser.username]["allVolunteer"] and + trainedAndInterested[interestedUser.username]["programSpecific"] and + trainedAndInterested[interestedUser.username]["eligible"] and + trainedAndInterested[interestedUser.username]['bgCheck'] == "Submitted" and + trainedAndInterested[interestedUser.username]['userObj'].lastHandbookSignature is not None and + trainedAndInterested[interestedUser.username]['userObj'].signatureTerm.academicYear == g.current_term.academicYear): + trainedAndInterested[interestedUser.username]["star"] = True + + return trainedAndInterested + +def getParticipantsForProgramForAY(programID, academicYear): + participants = (User.select(User.username, + User.bnumber, + User.email, + User.phoneNumber, + User.firstName, + User.lastName, + User.cpoNumber, + User.major, + User.rawClassLevel, + User.dietRestriction, + User.lastHandbookSignature) + .join(EventParticipant) + .join(Event) + .join(Program) + .switch(Event) + .join(Term) + .where(Program.id == programID, Term.academicYear == academicYear, User.hasGraduated == False, EventParticipant.hoursEarned > 0) + .distinct() + ) + return participants def sortParticipantsByStatus(event): @@ -197,4 +282,32 @@ def sortParticipantsByStatus(event): eventVolunteerData = [volunteer for volunteer in eventNonAttendedData if volunteer not in eventWaitlistData] eventNonAttendedData = [] - return eventNonAttendedData, eventWaitlistData, eventVolunteerData, eventParticipants \ No newline at end of file + return eventNonAttendedData, eventWaitlistData, eventVolunteerData, eventParticipants + +def hasGoneToTraining(participant, term): + """ + Taken in a User object, and returns which training (specifically, All volunteers training or All CELTS labor training) they attended for this term. + This is necessary for delivering the correct handbook to the student for signing. + + return: A single event object of, in this order of precedence: + 1) the All Celts training, if they attended, + 2) the All Volunteers training, if they attended, + 3) None + """ + attended = (EventParticipant.select() + .join(User) + .switch(EventParticipant) + .join(Event) + .join(Term) + .where(User.username == participant.username, + Term.id == term.id, + Event.isAllVolunteerTraining | Event.isCeltsTraining) + .order_by(Event.isCeltsTraining) + ) + + if not attended: + return None + if len(attended) > 1: + attended = attended[-1] + return attended.get().event + diff --git a/app/logic/term.py b/app/logic/term.py index 1e9238c68..57d9465a3 100644 --- a/app/logic/term.py +++ b/app/logic/term.py @@ -15,9 +15,14 @@ def addNextTerm(): newDescription = newSemesterMap[prevSemester] + " " + str(newYear) newAY = prevTerm.academicYear + volunteerHandbook = None + laborHandbook = None if prevSemester == "Summer": # we only change academic year when the latest term in the table is Summer year1, year2 = prevTerm.academicYear.split("-") newAY = year2 + "-" + str(int(year2)+1) + else: # if the previous term is fall or spring, make sure we copy the handbook + volunteerHandbook = prevTerm.volunteerHandbook + laborHandbook = prevTerm.laborHandbook semester = newDescription.split()[0] summer= "Summer" in semester @@ -25,7 +30,11 @@ def addNextTerm(): year=newYear, academicYear=newAY, isSummer= summer, - termOrder=Term.convertDescriptionToTermOrder(newDescription)) + termOrder=Term.convertDescriptionToTermOrder(newDescription), + volunteerHandbook=volunteerHandbook, + laborHandbook=laborHandbook + ) + newTerm.save() return newTerm @@ -52,11 +61,12 @@ def addPastTerm(description): return createdOldTerm def changeCurrentTerm(term): - oldCurrentTerm = Term.get_by_id(g.current_term) - oldCurrentTerm.isCurrentTerm = False - oldCurrentTerm.save() + activeTerms = Term.select().where(Term.isCurrentTerm) + nterms = (Term.update(isCurrentTerm = False).where(Term.isCurrentTerm).execute()) newCurrentTerm = Term.get_by_id(term) newCurrentTerm.isCurrentTerm = True newCurrentTerm.save() session["current_term"] = model_to_dict(newCurrentTerm) - createActivityLog(f"Changed Current Term from {oldCurrentTerm.description} to {newCurrentTerm.description}") + createActivityLog(f"Changed Current Term to {newCurrentTerm.description}") + + return newCurrentTerm diff --git a/app/logic/userManagement.py b/app/logic/userManagement.py index 8819dfc22..4eb75855a 100644 --- a/app/logic/userManagement.py +++ b/app/logic/userManagement.py @@ -1,6 +1,12 @@ -from flask import g, session -from playhouse.shortcuts import model_to_dict +import datetime +from flask import abort, g, session +from playhouse.shortcuts import DoesNotExist, model_to_dict +import xlsxwriter +from app import app +from app.logic.participants import getParticipantsForProgramForAY, getTrainingsForInterestedParticipants +from app.logic.users import getProgramInterest +from app.logic.volunteerSpreadsheet import makeDataXls from app.models.user import User from app.models.term import Term from app.models.programManager import ProgramManager @@ -104,4 +110,83 @@ def getAllowedTemplates(currentUser): if currentUser.isCeltsAdmin: return EventTemplate.select().where(EventTemplate.isVisible==True).order_by(EventTemplate.name) else: - return [] \ No newline at end of file + return [] + +def generateSheetData(program, academicYear, rosterType): + columns = [] + if rosterType == "Interested Volunteers": + columns = ["Username", + "B-number", + "Email", + "Phone", + "First Name", + "Last Name", + "CPO", + "Major", + "Class Level", + "Dietary Restrictions", + "Handbook Signature", + "All Volunteers Training", + "Program Specific Training", + "Background Check", + "Eligible" + ] + query = getTrainingsForInterestedParticipants(program, getProgramInterest(program)) + query = cleanInterestedParticipantsData(query) + return (columns, query) + elif rosterType == "Engaged Volunteers" or rosterType == "Last Year Volunteers": + columns = ["Username", + "B-number", + "Email", + "Phone", + "First Name", + "Last Name", + "CPO", + "Major", + "Class Level", + "Dietary Restrictions", + "Handbook Signature" + ] + if rosterType == "Last Year Volunteers": + academicYear = Term.select().where(Term.academicYear == academicYear).get().previousAcademicYear + query = getParticipantsForProgramForAY(program, academicYear) + query = [model_to_dict(user, only=(User.username, User.bnumber, User.email, User.phoneNumber, User.firstName, User.lastName, User.cpoNumber, User.major, User.rawClassLevel, User.dietRestriction, User.lastHandbookSignature)) for user in query] + query = cleanInterestedParticipantsData(query) + return (columns, query) + +def cleanInterestedParticipantsData(query): + if type(query) == dict: + for username, userData in query.items(): + # Dictionary of user object and participation data + query[username]["userObj"].major = "Unknown" if not query[username]["userObj"].major else query[username]["userObj"].major + query[username]["userObj"].rawClassLevel = "Unknown" if not query[username]["userObj"].rawClassLevel else query[username]["userObj"].rawClassLevel + query[username]["userObj"].dietRestriction = "Unknown" if not query[username]["userObj"].dietRestriction else query[username]["userObj"].dietRestriction + query[username]["userObj"].lastHandbookSignature = "Not Signed" if not query[username]["userObj"].lastHandbookSignature else query[username]["userObj"].lastHandbookSignature + query[username]["allVolunteer"] = "No" if query[username]["allVolunteer"] == False else "Yes" + query[username]["programSpecific"] = "No" if query[username]["programSpecific"] == False else "Yes" + query[username]["eligible"] = "No" if query[username]["eligible"] == False else "Yes" + del(query[username]["star"]) # Not needed in spreadsheet + else: + # User objects only + for index, userObj in enumerate(query): + userObj["major"] = "Unknown" if not userObj["major"] else userObj["major"] + userObj["rawClassLevel"] = "Unknown" if not userObj["rawClassLevel"] else userObj["rawClassLevel"] + userObj["dietRestriction"] = "Unknown" if not userObj["dietRestriction"] else userObj["dietRestriction"] + userObj["lastHandbookSignature"] = "Not Signed" if not userObj["lastHandbookSignature"] else userObj["lastHandbookSignature"] + query[index] = userObj + return query + + +def createSpreadsheetForRosters(academicYear, program): + try: + program = Program.get_by_id(program) + except DoesNotExist: + raise DoesNotExist + filepath = f'''{app.config['files']['base_path']}/{program.programName.replace(" ", "_")}_rosters_{academicYear}.xlsx''' + workbook = xlsxwriter.Workbook(filepath, {'in_memory': True}) + makeDataXls("Interested Volunteers", generateSheetData(program, academicYear, "Interested Volunteers"), workbook, sheetDesc=f"This worksheet shows all current students who have indicated interest in {program.programName}") + makeDataXls(f"Engaged Volunteers ({academicYear})", generateSheetData(program, academicYear, "Engaged Volunteers"), workbook, sheetDesc=f"This worksheet shows all students who have participated in a service hours earning event in {program.programName}") + makeDataXls(f"Last Year Volunteers", generateSheetData(program, academicYear, "Last Year Volunteers"), workbook, sheetDesc=f"This worksheet shows all current students who participated in a service hours earning event in {program.programName} during the previous academic year") + + workbook.close() + return filepath \ No newline at end of file diff --git a/app/logic/users.py b/app/logic/users.py index 36b9ba2bf..b33f175b5 100644 --- a/app/logic/users.py +++ b/app/logic/users.py @@ -1,3 +1,6 @@ +from app.models.eventParticipant import EventParticipant +from app.models.program import Program +from app.models.term import Term from app.models.user import User from app.models.event import Event from app.models.programBan import ProgramBan @@ -10,22 +13,45 @@ from app.models.backgroundCheckType import BackgroundCheckType from app.logic.volunteers import addUserBackgroundCheck import datetime -from peewee import JOIN +from peewee import JOIN, DoesNotExist, fn from dateutil import parser from flask import g +from playhouse.shortcuts import model_to_dict def isEligibleForProgram(program, user): """ - Verifies if a given user is eligible for a program by checking if they are - banned from a program. + Verifies if a given user is eligible for a program by checking if they are: + 1. Banned from a program. + 2. Missed All Volunteer Training (volunteers) or All CELTS training (labor) + 3. Missed Program-specific training + 4. Not signed the handbook (or expired) + 5. Background Check Submitted (does not matter if it passed or not) :param program: accepts a Program object or a valid programid :param user: accepts a User object or userid :return: True if the user is not banned and meets the requirements, and False otherwise """ now = datetime.datetime.now() + try: + user = User.get_by_id(user) + except DoesNotExist: + raise DoesNotExist + # Banned? if (ProgramBan.select().where(ProgramBan.user == user, ProgramBan.program == program, ProgramBan.endDate > now, ProgramBan.unbanNote == None).exists()): return False + # Missed trainings? + if user not in trainedParticipants(program, g.current_term): + return False + # Missing signature? + if not user.signatureTerm: + return False + # Old signature? + elif not user.signatureTerm.academicYear == g.current_term.academicYear: + return False + # Background check submitted? + if not (User.select(User.username, BackgroundCheck.dateCompleted).join(BackgroundCheck).distinct()): + return False + return True def addUserInterest(program_id, username): @@ -51,6 +77,22 @@ def removeUserInterest(program_id, username): interestToDelete.delete_instance() return True +def getUserInterest(username): + """ + This function is used to retrieve a user's interests. + Parameters: + username: username of the user showing interest + """ + return Interest.select().where(Interest.user == username) + +def getProgramInterest(program): + """ + This function is used to retrieve a programs's interested users. + Parameters: + program: Program object + """ + return User.select().join(Interest).where(Interest.program == program) + def getBannedUsers(program): """ This function returns users banned from a program. @@ -63,7 +105,38 @@ def isBannedFromEvent(username, eventId): """ program = Event.get_by_id(eventId).program user = User.get(User.username == username) - return not isEligibleForProgram(program, user) + isBanned = (ProgramBan.select() + .join(User) + .switch(ProgramBan) + .join(Program) + .where(ProgramBan.user == user, + ProgramBan.program == program, + ProgramBan.endDate > datetime.datetime.now(), + ProgramBan.unbanNote.is_null()).exists() + ) + return isBanned + +def trainedParticipants(programID, targetTerm): + """ + This function tracks the users who have attended every Prerequisite + event and adds them to a list that will not flag them when tracking hours. + Returns a list of user objects who've completed all training events. + """ + + # Reset program eligibility each term for all other trainings + isRelevantAllVolunteer = (Event.isAllVolunteerTraining | Event.isCeltsTraining) & (Event.term.academicYear == targetTerm.academicYear) + isRelevantProgramTraining = (Event.program == programID) & (Event.term == targetTerm) & (Event.isTraining) + allTrainings = (Event.select() + .join(Term) + .where(isRelevantAllVolunteer | isRelevantProgramTraining, + Event.isCanceled == False)) + + fullyTrainedUsers = (User.select() + .join(EventParticipant) + .where(EventParticipant.event.in_(allTrainings)) + .group_by(EventParticipant.user) + .having(fn.Count(EventParticipant.user) == len(allTrainings)).order_by(User.username)) + return list(fullyTrainedUsers) def banUser(program_id, username, note, banEndDate, creator): """ diff --git a/app/logic/volunteerSpreadsheet.py b/app/logic/volunteerSpreadsheet.py index f75b40039..411bab07a 100644 --- a/app/logic/volunteerSpreadsheet.py +++ b/app/logic/volunteerSpreadsheet.py @@ -1,6 +1,7 @@ from os import major import xlsxwriter -from peewee import fn, Case, JOIN, SQL, Select +from peewee import ModelSelect, fn, Case, JOIN, SQL, Select +from playhouse.shortcuts import model_to_dict from collections import defaultdict from datetime import date, datetime,time from app import app @@ -119,12 +120,12 @@ def getAllTermData(term): base = getBaseQuery(term.academicYear) columns = ["Program Name", "Event Name", "Event Description", "Event Date", "Event Start Time", "Event End Time", "Event Location", - "Food Provided", "Labor Only", "Training Event", "RSVP Required", "Service Event", "Engagement Event", "All Volunteer Training", + "Food Provided", "Includes Labor", "Training Event", "RSVP Required", "Service Event", "Engagement Event", "All Volunteer Training", "RSVP Limit", "Series #", "Is Repeating Event", "Contact Name", "Contact Email", "Student First Name", "Student Last Name", "Student Email", "Student B-Number", "Student Phone", "Student CPO", "Student Major", "Student Has Graduated", "Student Class Level", "Student Dietary Restrictions", "Hours Earned"] query = (base.select(Program.programName,Event.name, Event.description, Event.startDate, Event.timeStart, Event.timeEnd, Event.location, - makeCase(Event.isFoodProvided), makeCase(Event.isLaborOnly), makeCase(Event.isTraining), makeCase(Event.isRsvpRequired), makeCase(Event.isService), makeCase(Event.isEngagement), makeCase(Event.isAllVolunteerTraining), + makeCase(Event.isFoodProvided), makeCase(Event.allowsLabor), makeCase(Event.isTraining), makeCase(Event.isRsvpRequired), makeCase(Event.isService), makeCase(Event.isEngagement), makeCase(Event.isAllVolunteerTraining), Event.rsvpLimit, Event.seriesId, makeCase(Event.isRepeating), Event.contactName, Event.contactEmail, User.firstName, User.lastName, fn.CONCAT(User.username,'@berea.edu'), User.bnumber, User.phoneNumber,User.cpoNumber,User.major, makeCase(User.hasGraduated), User.rawClassLevel, User.dietRestriction, EventParticipant.hoursEarned) @@ -304,7 +305,7 @@ def laborAttendanceByTerm(term): def makeDataXls(sheetName, sheetData, workbook, sheetDesc=None): # assumes the length of the column titles matches the length of the data - (columnTitles, dataTuples) = sheetData + (columnTitles, dataRows) = sheetData worksheet = workbook.add_worksheet(sheetName) bold = workbook.add_format({'bold': True}) @@ -315,24 +316,35 @@ def makeDataXls(sheetName, sheetData, workbook, sheetDesc=None): for column, title in enumerate(columnTitles): worksheet.write(3, column, title, bold) - for row, rowData in enumerate(dataTuples): - for column, value in enumerate(rowData): - # dates and times should use their text representation - if isinstance(value, (datetime, date, time)): - value = str(value) - - worksheet.write(row + 4, column, value) - - # set the width to the size of the text, with a maximum of 50 characters - for column, title in enumerate(columnTitles): - # put all of the data in each column into a list - columnData = [title] + [rowData[column] for rowData in dataTuples] - - # find the largest item in the list (and cut it off at 50) - setColumnWidth = min(max(len(str(x)) for x in columnData),50) - - worksheet.set_column(column, column, setColumnWidth + 3) - + if type(dataRows) == list: + for row, rowData in enumerate(dataRows): + col_idx = 0 + for column, value in rowData.items(): + # dates and times should use their text representation + if isinstance(value, (datetime, date, time)): + value = str(value) + + worksheet.write(row + 4, col_idx, str(value)) + col_idx += 1 + + elif type(dataRows) == dict: + idx = 0 + # Each participant + for row, rowData in dataRows.items(): + idx2 = 0 + # All participant data + for key, value in dict(rowData).items(): + if key == "userObj": + # Data inside user object + for key, userObjVal in model_to_dict(rowData[key], only=(User.username, User.bnumber, User.email, User.phoneNumber, User.firstName, User.lastName, User.cpoNumber, User.major, User.rawClassLevel, User.dietRestriction, User.lastHandbookSignature)).items(): + worksheet.write(idx + 4, idx2, userObjVal) + idx2 += 1 + else: + # participation data outside user object + worksheet.write(idx + 4, idx2, str(value)) + idx2 += 1 + + idx += 1 def createSpreadsheet(academicYear): filepath = f"{app.config['files']['base_path']}/volunteer_data_{academicYear}.xlsx" diff --git a/app/logic/volunteers.py b/app/logic/volunteers.py index eb40b4df7..2164d36d7 100644 --- a/app/logic/volunteers.py +++ b/app/logic/volunteers.py @@ -7,6 +7,7 @@ from app.models.programManager import ProgramManager from datetime import datetime, date from app.logic.createLogs import createActivityLog +from flask import g def getEventLengthInHours(startTime, endTime, eventDate): """ @@ -66,7 +67,7 @@ def addUserBackgroundCheck(user, bgType, bgStatus, dateCompleted): else: if not dateCompleted: dateCompleted = None - update = BackgroundCheck.create(user=user, type=bgType, backgroundCheckStatus=bgStatus, dateCompleted=dateCompleted) + update = BackgroundCheck.create(user=user, type=bgType, backgroundCheckStatus=bgStatus, dateCompleted=dateCompleted, termSubmitted=g.current_term) if bgStatus == 'Submitted': createActivityLog(f"Marked {user.firstName} {user.lastName}'s background check for {bgType} as submitted.") elif bgStatus == 'Passed': diff --git a/app/models/backgroundCheck.py b/app/models/backgroundCheck.py index 1c782e044..08f776d06 100644 --- a/app/models/backgroundCheck.py +++ b/app/models/backgroundCheck.py @@ -1,5 +1,6 @@ from app.models import * from app.models.user import User +from app.models.term import Term from app.models.backgroundCheckType import BackgroundCheckType class BackgroundCheck(baseModel): diff --git a/app/models/event.py b/app/models/event.py index 1ffd6ee68..382f203c9 100644 --- a/app/models/event.py +++ b/app/models/event.py @@ -11,12 +11,14 @@ class Event(baseModel): timeEnd = TimeField() location = CharField() isFoodProvided = BooleanField(default=False) - isLaborOnly = BooleanField(default=False) - isTraining = BooleanField(default=False) + allowsLabor = BooleanField(default=False) # Event has some labor students working in addition to volunteers + isLaborOnly = BooleanField(default=False) # Event is a labor meeting, specifically for labor students only + isTraining = BooleanField(default=False) # Event is a training for a Program (required by volunteers to earn service hours in that program) + isAllVolunteerTraining = BooleanField(default=False) # Event is an All Volunteers Training (required to earn any service hours) + isCeltsTraining = BooleanField(default=False) # Event is a CELTS labor training (required by all CELTS labor students) isRsvpRequired = BooleanField(default=False) isService = BooleanField(default=False) isEngagement = BooleanField(default=False) - isAllVolunteerTraining = BooleanField(default=False) rsvpLimit = IntegerField(null=True) startDate = DateField() seriesId = IntegerField(null=True) @@ -27,12 +29,61 @@ class Event(baseModel): isCanceled = BooleanField(default=False) deletionDate = DateTimeField(null=True) deletedBy = TextField(null=True) + eventFlagsMatrix = {'isAllVolunteerTraining': {'isAllVolunteerTraining', + 'isTraining'}, + 'isCeltsTraining': {'isCeltsTraining', + 'isLaborOnly', + 'isTraining'}, + 'isLaborOnly': {'isLaborOnly', + 'isCeltsTraining', + 'isTraining'}, + 'allowsLabor': {'allowsLabor', + 'isTraining', + 'isService', + 'isEngagement'}, + 'isTraining': {'isTraining', + 'isAllVolunteerTraining', + 'isCeltsTraining', + 'isLaborOnly', + 'allowsLabor'}, + 'isService': {'isService', + 'allowsLabor'}, + 'isEngagement': {'isEngagement', + 'allowsLabor'} + } _spCache = "Empty" + def save(self, *args, **kwargs): + """ + Overrides the default Peewee save method. + NOTE: This method is not called when using Model.update() + """ + if self.checkFlags(): + return super().save(*args, **kwargs) + else: + raise IntegrityError("This combination of options is not allowed") + def __str__(self): return f"{self.id}: {self.description}" + def checkFlags(self): + """ + Checks the eventFlagsMatrix to ensure the user is only picking a combination of flags that are allowed. + """ + setFlags = [] + for attribute in self._meta.fields.keys(): + if self.eventFlagsMatrix.get(attribute): + if getattr(self, attribute): + setFlags.append(attribute) + for setFlag in setFlags: + allowedFlags = self.eventFlagsMatrix[setFlag] + if not all(flag in allowedFlags for flag in setFlags): + return False + return True + + + @property def isDeleted(self): return self.deletionDate is not None diff --git a/app/models/term.py b/app/models/term.py index 34f85261b..5496be054 100644 --- a/app/models/term.py +++ b/app/models/term.py @@ -6,9 +6,18 @@ class Term(baseModel): isSummer = BooleanField(default=False) isCurrentTerm = BooleanField(default=False) termOrder = CharField() + volunteerHandbook = CharField(null=True) + laborHandbook = CharField(null=True) _cache = None + @property + def previousAcademicYear(self): + """ + Returns the previous academic year. + """ + return f"{int(self.academicYear.split('-')[0])-1}-{int(self.academicYear.split('-')[1])-1}" + @property def academicYearStartingTerm(self): """ diff --git a/app/models/user.py b/app/models/user.py index c9652f86c..77539b6e9 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,4 +1,5 @@ from app.models import * +from app.models.term import Term class User(baseModel): username = CharField(primary_key=True) @@ -19,6 +20,8 @@ class User(baseModel): minorInterest = BooleanField(null=True) hasGraduated = BooleanField(default=False) declaredMinor = BooleanField(default=False) + lastHandbookSignature = DateField(null=True) + signatureTerm = ForeignKeyField(Term, null = True) # override BaseModel's __init__ so that we can set up an instance attribute for cache def __init__(self,*args, **kwargs): diff --git a/app/scripts/fix_event_flags.py b/app/scripts/fix_event_flags.py new file mode 100644 index 000000000..b7a0faf18 --- /dev/null +++ b/app/scripts/fix_event_flags.py @@ -0,0 +1,39 @@ +from app.models.event import Event +from peewee import ForeignKeyField + +def fix_event_flags(): + events = Event.select() + bad_ids = [] + for e in events: + if not e.checkFlags(): + bad_ids.append(e.id) + print("####################################") + for field in e._meta.fields.keys(): + print(field, ": ", getattr(e, field)) + print("\n\n\n") + print(bad_ids) + + # NOTE: Any event printed by the above code needs added to the lists below. They would be events that are not in our backed up database, but are in production. + + + + # The following events were confirmed with CELTS: + + # The rule is: No event that is Labor Only can earn service hours. + ids = [276, 277, 303, 548, 549, 557] # Cannot be service and training at the same time. Remove service flag + for id in ids: + event = Event.get_by_id(id) + event.isService = False + event.save() + + # Rule: These events cannot be an engagement and Labor Only. Also confirmed with CELTS. + ids = [1297, 1298] + + for id in ids: + event = Event.get_by_id(id) + event.isEngagement = False + event.save() + + + +fix_event_flags() \ No newline at end of file diff --git a/app/static/.DS_Store b/app/static/.DS_Store new file mode 100644 index 000000000..e4504ab8b Binary files /dev/null and b/app/static/.DS_Store differ diff --git a/app/static/css/programManagement.css b/app/static/css/programManagement.css index 7a8717b6e..b7d7b9456 100644 --- a/app/static/css/programManagement.css +++ b/app/static/css/programManagement.css @@ -36,3 +36,14 @@ right: 10px; /* Added px to right */ } +form.uploaders { + margin-bottom: 1em; +} + +.uploaderButtons { + margin-top: 24px; +} + +#collapseTwo div.card { + margin-bottom: 40px; +} \ No newline at end of file diff --git a/app/static/css/templateSelector.css b/app/static/css/templateSelector.css new file mode 100644 index 000000000..80ff69e14 --- /dev/null +++ b/app/static/css/templateSelector.css @@ -0,0 +1,10 @@ +#extravaganzaURL { +border-left: 1px solid #dee2e6; +padding-left: 8px; +} + +#extravaganzaQR { +height: 32px; +width: 32px; +display: block; +} \ No newline at end of file diff --git a/app/static/css/userProfile.css b/app/static/css/userProfile.css index 561e2fc7f..461deec8f 100644 --- a/app/static/css/userProfile.css +++ b/app/static/css/userProfile.css @@ -39,3 +39,38 @@ div.profile-links a:not(:first-child) { position: relative; left: 12px; width: 95%; } +.inline-wrapper { + white-space: nowrap; +} + +/* Container holds both elements */ +.tooltip-container { + position: relative; + display: inline-flex; + align-items: center; + cursor: pointer; + margin-left: 4px; /* Adds a small gap after the text */ +} + +/* Hidden by default and positioned to the right */ +.tooltip-text { + visibility: hidden; + position: absolute; + top: 50%; /* Aligns top edge to the middle of the icon */ + left: 115%; /* Pushes the text box completely to the right of the icon */ + transform: translateY(-50%); /* Perfectly centers the text box vertically */ + background-color: #333; + color: #fff; + padding: 8px; + border-radius: 4px; + white-space: nowrap; + font-size: 14px; + opacity: 0; + transition: opacity 0.3s; +} + +/* Show the text on hover */ +.tooltip-container:hover .tooltip-text { + visibility: visible; + opacity: 1; +} diff --git a/app/static/css/viewRoster.css b/app/static/css/viewRoster.css new file mode 100644 index 000000000..aaa3e5fd8 --- /dev/null +++ b/app/static/css/viewRoster.css @@ -0,0 +1,3 @@ +.ui-front { + z-index: 1061; +} \ No newline at end of file diff --git a/app/static/images/extravaganza_qr.png b/app/static/images/extravaganza_qr.png new file mode 100644 index 000000000..298efc1c7 Binary files /dev/null and b/app/static/images/extravaganza_qr.png differ diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index 8e0609633..4e8c5ef10 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -433,7 +433,7 @@ function updateOfferingsTable() { var endTime = format24to12HourTime(offering.endTime); offeringsTable.append(`` + "" + offering.eventName + "" + - "" + formattedEventDate + "" + + "" + formattedEventDate + "" + "" + startTime + "" + "" + endTime + "" + "" + (offering.eventLocation || offering.location || "") + "" + @@ -503,7 +503,11 @@ function checkValidation() { let allFieldFilled = true; let seriesEvent = $("#checkIsSeries").is(":checked"); let seriesWeeklyId = $("#checkIsRepeating").is(":checked"); - let isAllVolunteer = $("#pageTitle").text() == 'Create All Volunteer Training'; + var pageId = $("#pageTitle").attr('name'); + + let isAllVolunteer = pageId == "all-volunteer"; + let isLaborOnly = (pageId == "labor-meeting" || pageId == "all-celts-training"); + enableLiveCustomValidityClearing([".all", ".series", ".seriesWeekly", ".main", ".allV"]); // Always validate common fields (.all class) @@ -513,6 +517,9 @@ function checkValidation() { // Validate all volunteer specific fields allFieldFilled = validateFieldGroup(".allV", allFieldFilled); + } else if (isLaborOnly) { + allFieldFilled = validateFieldGroup(".allV", allFieldFilled); + } else if (seriesEvent) { // Validate series-specific fields allFieldFilled = validateFieldGroup(".series", allFieldFilled); @@ -587,33 +594,33 @@ $(document).ready(function () { }); //to show the msgFlash message when the event is canceled -$("#cancelEvent").on('click', function (event) { - event.preventDefault(); // Prevent normal form submission - - // Get the form action URL - let formAction = $(this).closest('form').attr('action'); - - // Submit via AJAX - $.ajax({ - url: formAction, - method: 'POST', - success: function(response) { - msgFlash("You have successfully canceled the event", "success", 5000); - $('#cancelWarning').modal('hide'); - // Optionally refresh the page or update the UI - location.reload(); // or update specific elements - }, - error: function() { - msgFlash("Failed to cancel the event", "error"); - } - }); -}); + $("#cancelEvent").on('click', function (event) { + event.preventDefault(); // Prevent normal form submission + + // Get the form action URL + let formAction = $(this).closest('form').attr('action'); + + // Submit via AJAX + $.ajax({ + url: formAction, + method: 'POST', + success: function(response) { + msgFlash("You have successfully canceled the event", "success", 5000); + $('#cancelWarning').modal('hide'); + // Optionally refresh the page or update the UI + location.reload(); // or update specific elements + }, + error: function() { + msgFlash("Failed to cancel the event", "error"); + } + }); + }); // When Save buttton is clicked, check if required are filled and then submit $("#saveButton").on('click', function (event) { event.preventDefault(); //prevents from submitting checkValidation(); -}); + }); updateOfferingsTable(); @@ -704,7 +711,7 @@ $("#cancelEvent").on('click', function (event) { "#repeatingEventsEndDate, " + "#repeatingEventsStartTime, " + "#repeatingEventsEndTime").on("change", handleRepeatingEventsChange); -// this handels start date, end date, last event date, start time, and end time + // this handels start date, end date, last event date, start time, and end time function handleRepeatingEventsChange() { if (!verifyRepeatingFields()) { let table = $("#generatedEventsList").children(); @@ -757,7 +764,7 @@ $("#cancelEvent").on('click', function (event) { let mainTime = $("#startTime-main").val(); let endTime = $("#endTime-main").val(); createOfferingModalRow({ eventLocation: mainLocation, eventDate: mainDate, startTime: mainTime, endTime: endTime }); -}); + }); var minDate = new Date('10/25/1999') $("#startDatePicker-main").datepicker("option", "minDate", minDate) @@ -883,4 +890,4 @@ $("#cancelEvent").on('click', function (event) { }); setCharacterLimit($("#inputCharacters"), "#remainingCharacters"); - }); \ No newline at end of file +}); \ No newline at end of file diff --git a/app/static/js/extravaganza.js b/app/static/js/extravaganza.js new file mode 100644 index 000000000..b5a921ea2 --- /dev/null +++ b/app/static/js/extravaganza.js @@ -0,0 +1,22 @@ +$(document).ready(function(){ + + $(".interestedInput").click(function updateInterest(){ + var programID = $(this).data("programid"); + var username = $(this).data('username'); + + var interest = $(this).is(':checked'); + var routeUrl = interest ? "addInterest" : "removeInterest"; + var interestUrl = "/" + username + "/" + routeUrl + "/" + programID ; + $.ajax({ + method: "POST", + url: interestUrl, + success: function(response) { + window.location.reload(); + }, + error: function(request, status, error) { + console.log(status,error); + location.reload(); + } + }); + }); +}); \ No newline at end of file diff --git a/app/static/js/handbookSignature.js b/app/static/js/handbookSignature.js new file mode 100644 index 000000000..6095bdf63 --- /dev/null +++ b/app/static/js/handbookSignature.js @@ -0,0 +1,70 @@ +var canvas = $('#handbook-signature-pad')[0]; +var signaturePad = new SignaturePad(canvas); + + +// Resize canvas to fix Bootstrap 3 responsiveness +function resizeCanvas() { + var ratio = Math.max(window.devicePixelRatio || 1, 1); + var displayWidth = canvas.offsetWidth; + var displayHeight = canvas.offsetHeight; + canvas.width = displayWidth * ratio; + canvas.height = displayHeight * ratio; + // Lock the on-page size so it doesn't visually grow with the buffer + canvas.style.width = displayWidth + "px"; + canvas.style.height = displayHeight + "px"; + canvas.getContext("2d").scale(ratio, ratio); + // signaturePad.clear(); +} + +window.addEventListener("resize", resizeCanvas); + +$('#editVolunteerModal').on('shown.bs.modal', function () { + resizeCanvas(); +}); + +$('#editVolunteerModal').on('hidden.bs.modal', function () { + if (!signaturePad.isEmpty() & hasSavedSignature) { + $("#signatureHeader").remove(); + canvas.remove(); + // signaturePad.clear(); + $("#signatureButtons").hide(); + } +}); + +$('#clear-btn').on('click', function () { + signaturePad.clear(); +}); + +var hasSavedSignature = false; + +$('#save-btn').on('click', function () { + if (signaturePad.isEmpty()) { + alert("You forgot to sign!"); + } else { + $.ajax({ + url: `/handbookSignature`, + type: "POST", + headers: {'Content-Type': 'application/json'}, + data: JSON.stringify({studentID: $('#handbook-signature-pad').attr('data-student')}), + success: function(s){ + signaturePad.off(); + $("#signatureConfirmation").show(); + $("#signatureButtons").hide(); + const today = new Date(); + $("#signatureText").text("CELTS Handbook signed for AY 2026-2027"); + $("#signatureText").css("color", "black"); + $("#handbookSignatureContainer .bi-info-circle-fill").hide(); + hasSavedSignature = true; + }, + error: function(error, status){ + console.log(error, status) + $("#signatureConfirmation h3").text("Uh oh. Something wrong. Please seek out help from the CELTS staff") + $("#signatureConfirmation h3").replaceWith(function() { + return $('

', { html: $(this).html() }); + }); + $("#signatureConfirmation").show(); + $("#signatureButtons").hide(); + } + }) + } +}); diff --git a/app/static/js/rosterManagement.js b/app/static/js/rosterManagement.js new file mode 100644 index 000000000..0010fd48f --- /dev/null +++ b/app/static/js/rosterManagement.js @@ -0,0 +1,90 @@ +import searchUser from './searchUser.js' + +$(document).ready(function(){ + var dt = $('#rosterTable').DataTable(); + + searchUser("searchStudentsInput", callback, "searchStudentsInput"); // initialize ONCE + + $("#searchIcon").click(function (e) { + e.preventDefault(); + callback($("#searchStudentsInput").val()); + }); + + $("#searchStudentsInput").focus(); + + $("#dismissModal").click(function() { + location.reload(); + }) +}); + +function callback(selected) { + var form = $("#searchStudentForm"); + form.attr("action", "/" + selected["username"] + "/addInterest/" + form.data("program") + "/False"); + $("#searchStudentForm").submit(); +} + +$('#searchStudentForm').on('submit', function(e) { + e.preventDefault(); // stops the browser's normal form submission/redirect + + var formData = $(this).serialize(); // or new FormData(this) if you have file inputs + $.ajax({ + url: $(this).attr('action'), + type: $(this).attr('method') || 'POST', + success: function(response) { + var targetDiv = $("#addedNameDivTarget"); + var targetP = $("#addedNameToClone").clone(); + targetDiv.append(targetP); + var targetSpan = targetP.find(".addedNameTarget"); + targetSpan.text(response['firstName'] + " " + response["lastName"]); + targetP.attr("hidden", false); + }, + error: function(xhr, status, error) { + var targetDiv = $("#addedNameDivTarget"); + var targetP = $("#addedNameToClone"); + var targetSpan = targetP.find(".addedNameTarget"); + targetP.html(targetSpan); + targetSpan.text("Uh oh... something went wrong. Contact a CELTS staff member."); + targetP.attr("hidden", false); + console.log(status, error); + } + }) +}); + +$("#exportRoster").click(function() { + var year = $(this).data("year"); + var program = $(this).data('program'); + $.ajax({ + url: "/exportRosters/" + program + "/" + year, + type: 'GET', + xhrFields: { responseType: "blob" }, + success: function(blob, status, xhr) { + msgFlash("Download Successful", "success"); + var filename = 'download.xlsx'; // fallback if header is missing/unparseable + var disposition = xhr.getResponseHeader('Content-Disposition'); + if (disposition) { + // handles both: filename="report.xlsx" and filename=report.xlsx + var match = disposition.match(/filename\*?=(?:UTF-\d['"]*)?["']?([^"';\n]+)["']?/i); + if (match && match[1]) { + filename = decodeURIComponent(match[1]); + } + } + + var xlsxBlob = new Blob([blob], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + + var url = window.URL.createObjectURL(xlsxBlob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + }, + error: function(xhr, status, error) { + console.log(status, error); + } + }) +}) + diff --git a/app/static/js/searchStudent.js b/app/static/js/searchStudent.js index 93062f6de..bdfcc9e1f 100644 --- a/app/static/js/searchStudent.js +++ b/app/static/js/searchStudent.js @@ -1,16 +1,16 @@ import searchUser from './searchUser.js' + function callback(selected) { - $("#searchStudent").submit(); + $("#searchStudentsInput").submit(); } + $(document).ready(function() { - $("#searchStudentsInput").on("input", function() { - searchUser("searchStudentsInput", callback); - }); - + searchUser("searchStudentsInput", callback); // initialize ONCE + $("#searchIcon").click(function (e) { e.preventDefault(); callback($("#searchStudentsInput").val()); }); - $("#searchStudentsInput").focus() -}) + $("#searchStudentsInput").focus(); +}); \ No newline at end of file diff --git a/app/static/js/searchUser.js b/app/static/js/searchUser.js index 5ebf57c70..db6ee7e7e 100644 --- a/app/static/js/searchUser.js +++ b/app/static/js/searchUser.js @@ -1,41 +1,36 @@ export default function searchUser(inputId, callback, clear=false, parentElementId=null, category = null) { - var query = $(`#${inputId}`).val() - let columnDict = {}; $(`#${inputId}`).autocomplete({ appendTo: (parentElementId === null) ? null : `#${parentElementId}`, minLength: 2, - source: function(request, response) { + source: function(request, response) { $.ajax({ - url: `/searchUser/${query}`, + url: `/searchUser/${request.term}`, // use the live term type: "GET", dataType: "json", - data:{"category":category}, + data: {"category": category}, success: function(searchResults) { - response(Object.entries(searchResults).map( (item) => { - return { - // label: firstName lastName (username) - // value: username - label: (item[1]["firstName"]+" "+item[1]["lastName"]+" ("+item[0]+")"), - value: item[1]["username"], - dictvalue: item[1], - } - } - ))}, + response(Object.entries(searchResults).map((item) => { + return { + label: (item[1]["firstName"] + " " + item[1]["lastName"] + " (" + item[0] + ")"), + value: item[1]["username"], + dictvalue: item[1], + } + })) + }, error: function(request, status, error) { console.log(status, error); } }) }, - select: function(event, ui) { - $(`#${inputId}`).val(ui.item.value); - callback(ui.item.dictvalue); - if(clear){ - $(`#${inputId}`).val(""); - } - - return false; - }, - autoFocus: true + select: function(event, ui) { + $(`#${inputId}`).val(ui.item.value); + callback(ui.item.dictvalue); + if(clear){ + $(`#${inputId}`).val(""); + } + return false; + }, + autoFocus: true }); -}; +}; \ No newline at end of file diff --git a/app/static/js/userManagement.js b/app/static/js/userManagement.js index 2e5e91998..7a9c5e8fe 100644 --- a/app/static/js/userManagement.js +++ b/app/static/js/userManagement.js @@ -27,6 +27,7 @@ function callbackProgramManager(selected, action = 'add') { } $(document).ready(function(){ + // Admin Management $("#searchCeltsAdminInput").on("input", function(){ searchUser("searchCeltsAdminInput", callbackAdmin, false, null, "celtsLinkAdmin") @@ -152,6 +153,13 @@ $(document).ready(function(){ }) }); +$('.viewRoster').on('click', function() { + // Openning the modal after the data was received + $('#programPlaceholder').data('programid', $(this).data('programid')) + let modal = new bootstrap.Modal($('#viewRosterModal')); + modal.show(); +}); + function submitRequest(method, username){ let data = { method: method, @@ -244,12 +252,31 @@ function submitTerm(){ url: "/admin/changeTerm", type: "POST", data: termInfo, - success: function(s){ - msgFlash("Current term successfully changed to " + selectedTerm.html(), "success") + success: function(response){ + console.log(response) + msgFlash("Current term successfully changed to " + response["description"], "success") + $(".uploaders").each(function(idx) { + // update the form action attribute to point at the right term + $(this).attr("action", $(this).attr("action").split("/").slice(0, -1).join("/") + "/" + termInfo["id"]); + }) + + // Update all other fields in the form uploader section + $("#collapseTwo h5").text("AY " + response["academicYear"] + " files"); + if(response["volunteerHandbook"]) { + $("#volunteerHandbookURL").text("AY " + response["academicYear"] + " - CELTS Student Handbook") + $("#volunteerHandbookURL").removeAttr("hidden"); + } else { + $("#volunteerHandbookURL").attr("hidden", true); + } + if(response["laborHandbook"]) { + $("#laborHandbookURL").text("AY " + response["academicYear"] + " - CELTS Labor Handbook") + $("#laborHandbookURL").removeAttr("hidden"); + } else { + $("#laborHandbookURL").attr("hidden", true); + } }, error: function(error, status){ - msgFlash("Current term was not changed. Please try again.", "warning") - console.log(error, status) + msgFlash("Current term was not changed. Please reload the page and try again.", "warning") } }) } diff --git a/app/static/js/userProfile.js b/app/static/js/userProfile.js index 61af76787..883520502 100644 --- a/app/static/js/userProfile.js +++ b/app/static/js/userProfile.js @@ -1,6 +1,10 @@ -$(document).ready(function(){ +$(document).ready(function(){ + + $('#editVolunteerModal').modal({ + backdrop: 'static' + }); - $("#checkDietRestriction").on("change", function() { + $("#checkDietRestriction").on("change", function() { let norestrict = $(this).is(':checked'); if (norestrict) { $("#dietContainer").hide(); @@ -36,6 +40,7 @@ $(document).ready(function(){ }) $("#phoneInput").inputmask('(999)-999-9999'); + $(".notifyInput").click(function updateInterest(){ var programID = $(this).data("programid"); var username = $(this).data('username'); @@ -160,45 +165,45 @@ $(document).ready(function(){ /* * Note Functionality */ - function bonnerNoteOff() { - $("#bonnerInput").prop("checked", false); - $("#noteDropdown").show() - $("#bonnerStatement").hide() - $("#visibilityLabel").show() - } + function bonnerNoteOff() { + $("#bonnerInput").prop("checked", false); + $("#noteDropdown").show() + $("#bonnerStatement").hide() + $("#visibilityLabel").show() + } - function bonnerNoteOn() { - $("#bonnerInput").prop("checked", true); - $("#noteDropdown").hide() - $("#bonnerStatement").show() - $("#visibilityLabel").hide() - } + function bonnerNoteOn() { + $("#bonnerInput").prop("checked", true); + $("#noteDropdown").hide() + $("#bonnerStatement").show() + $("#visibilityLabel").hide() + } - $("#addNoteButton").click(function() { - bonnerNoteOff() - $("#addNoteTextArea").val('') - $("#notesSaveButton").data('mode', 'add') - $("#notesSaveButton").data('noteid', null) - $("#noteModal").modal("toggle") - }); + $("#addNoteButton").click(function() { + bonnerNoteOff() + $("#addNoteTextArea").val('') + $("#notesSaveButton").data('mode', 'add') + $("#notesSaveButton").data('noteid', null) + $("#noteModal").modal("toggle") + }); - $("#addVisibility").click(function() { - var bonnerChecked = $("input[name='bonner']:checked").val() + $("#addVisibility").click(function() { + var bonnerChecked = $("input[name='bonner']:checked").val() - if (bonnerChecked == 'on') { - bonnerNoteOn() - } else { - bonnerNoteOff() - } - }); + if (bonnerChecked == 'on') { + bonnerNoteOn() + } else { + bonnerNoteOff() + } + }); - $("#addBonnerNoteButton").click(function() { - bonnerNoteOn() - $("#addNoteTextArea").val('') - $("#notesSaveButton").data('mode', 'add') - $("#notesSaveButton").data('noteid', null) - $("#noteModal").modal("toggle"); - }); + $("#addBonnerNoteButton").click(function() { + bonnerNoteOn() + $("#addNoteTextArea").val('') + $("#notesSaveButton").data('mode', 'add') + $("#notesSaveButton").data('noteid', null) + $("#noteModal").modal("toggle"); + }); $('#addNoteForm').submit(function(event) { @@ -220,9 +225,9 @@ $(document).ready(function(){ method: "POST", url: "/profile/addNote", data: {"username": username, - "visibility": $("#noteDropdown").val(), - "noteTextbox": $("#addNoteTextArea").val(), - "bonner": isBonner ? "yes" : "no"}, + "visibility": $("#noteDropdown").val(), + "noteTextbox": $("#addNoteTextArea").val(), + "bonner": isBonner ? "yes" : "no"}, success: function(response) { target = isBonner ? "bonner" : "notes" msgFlash("Successfully added a note", "success", 1300, true); @@ -238,7 +243,7 @@ $(document).ready(function(){ $("#confirmDeleteNote").data('username', $(this).data('username')) $("#confirmDeleteNote").data('noteid', $(this).data('noteid')) $("#deleteNoteWarning").modal("show") - + }); $("#confirmDeleteNote").click(function() { @@ -277,7 +282,8 @@ $(document).ready(function(){ $("#noteModal").modal("toggle") -}); + + $.ajax({ method: "POST", url: "/" + username + "/editNote", @@ -287,6 +293,7 @@ $(document).ready(function(){ } }); }); +}); /* * Background Check Functionality */ @@ -365,14 +372,16 @@ $(document).ready(function(){ }); // Popover functionality - var requiredTraining = $(".trainingPopover"); - requiredTraining.popover({ + var requiredTraining = document.querySelectorAll(".trainingPopover"); + requiredTraining.forEach(function(el) { + new bootstrap.Popover(el, { trigger: "hover", sanitize: false, html: true, content: function() { - return $(this).attr('data-content'); + return this.getAttribute('data-content'); } + }); }); setupPhoneNumber("#updatePhone", "#phoneInput") @@ -443,4 +452,4 @@ function updateManagers(el, volunteerUsername ) { console.log(error, status) } }) -} +} \ No newline at end of file diff --git a/app/templates/admin/searchStudentPage.html b/app/templates/admin/searchStudentPage.html index cf3af8987..6c7ddb589 100644 --- a/app/templates/admin/searchStudentPage.html +++ b/app/templates/admin/searchStudentPage.html @@ -3,13 +3,13 @@ {% block scripts %} {{super()}} - + {% endblock %} {% block app_content %} -

+

Student Search Page


diff --git a/app/templates/admin/userManagement.html b/app/templates/admin/userManagement.html index cea20a377..351645c92 100644 --- a/app/templates/admin/userManagement.html +++ b/app/templates/admin/userManagement.html @@ -28,15 +28,18 @@ {{ program.programName }}
+ + View Roster + {% if show_edit_managers %} -
+
+ +
+
AY {{g.current_term.academicYear}} files
+
+ +
+
+ + +
+
+ +
+ {{g.current_term}} +
+ + +
+ +
+
+
+
+ + +
+
+ +
+ {{g.current_term}} +
+ +
+
+
diff --git a/app/templates/admin/viewRoster.html b/app/templates/admin/viewRoster.html new file mode 100644 index 000000000..03f31ca26 --- /dev/null +++ b/app/templates/admin/viewRoster.html @@ -0,0 +1,174 @@ +{% set title = "Roster Management" %} +{% extends "base.html" %} + +{% block scripts %} + {{super()}} + + +{% endblock %} + +{% block styles %} + {{super()}} + + +{% endblock %} + +{% block app_content %} +
+
+
+
+

{{program.programName}} Rosters

+
+
+ +
+
+
+
+
+

+ +

+
+
+
+
+

The following table includes all students who have indicated they are interested in {{program.programName}} on their profile.

+ + + + + + + + + + + + + + {% for prospectiveVolunteer in trainedAndInterested %} + + + + + + + + + + {% endfor %} + +
Potential VolunteerEmailEligibleAll Volunteers TrainingProgram Specific TrainingAY {{g.current_term.academicYear}} Handbook SignedBackground Check
{% if trainedAndInterested[prospectiveVolunteer]['star']%} {% endif %}{{trainedAndInterested[prospectiveVolunteer]["userObj"].fullName}}{{trainedAndInterested[prospectiveVolunteer]["userObj"].email}}{% if trainedAndInterested[prospectiveVolunteer]['eligible'] %} Yes {% else %} No {% endif %}{% if trainedAndInterested[prospectiveVolunteer]['allVolunteer'] %} Attended {% else %} Not attended {% endif %}{% if trainedAndInterested[prospectiveVolunteer]['programSpecific'] %} Attended {% else %} Not attended {% endif %}{% if trainedAndInterested[prospectiveVolunteer]["userObj"].signatureTerm.academicYear == g.current_term.academicYear %} Signed {% else %} Not signed {% endif %}{% if trainedAndInterested[prospectiveVolunteer]['bgCheck'] == "Not submitted" %} {% else %} {% endif %}{{trainedAndInterested[prospectiveVolunteer]['bgCheck']}}
+ +
+
+
+
+
+ +
+

+ +

+
+
+
+
+

The following table includes all current students who have participated in at least one {{program.programName}} event in AY {{g.current_term.academicYear}}.

+ {% if currentYearsParticipants|length == 0%} +

There are no participants currently in {{program.programName}} for AY {{g.current_term.academicYear}}

+ {% else %} + + + + + + + + + {% for currentYearparticipant in currentYearsParticipants %} + + + + + {% endfor %} + +
Volunteer NameEmail
{{currentYearparticipant.fullName}}{{currentYearparticipant.email}}
+ {% endif %} +
+
+
+
+
+ +
+

+ +

+
+
+
+
+

The following table includes all current students who participated in at least one {{program.programName}} event in AY {{g.current_term.previousAcademicYear}}.

+ {% if lastYearsParticipants|length == 0%} +

There were no participants in {{program.programName}} during AY {{g.current_term.previousAcademicYear}}

+ {% else %} + + + + + + + + + {% for lastYearparticipant in lastYearsParticipants %} + + + + + {% endfor %} + +
Volunteer NameEmail
{{lastYearparticipant.fullName}}{{lastYearparticipant.email}}
+ {% endif %} +
+
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index ebc754eab..ab0206886 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -3,10 +3,10 @@ {% block styles %} - + {% endblock %} @@ -96,8 +96,6 @@ - - {% endblock %} diff --git a/app/templates/events/createEvent.html b/app/templates/events/createEvent.html index 7b8a2b346..549e45013 100644 --- a/app/templates/events/createEvent.html +++ b/app/templates/events/createEvent.html @@ -2,6 +2,7 @@ {% set isNewEvent = 'create' in request.path %} {% set showRecurringToggle = True %} +{% set showEventTypeOptions = True %} {% if isNewEvent %} {% if template.tag == 'single-program' %} {% set programName = eventData.program.programName %} @@ -15,14 +16,26 @@ {% elif eventData["program"].programName == 'CELTS-Sponsored Event' and template.tag == 'all-volunteer' %} {% set page_title = 'Create All Volunteer Training' %} {% set showRecurringToggle = False %} + {% set showEventTypeOptions = False %} + {% elif template.tag == 'all-celts-training' %} + {% set page_title = 'Create All CELTS Training (Labor)' %} + {% set showRecurringToggle = False %} + {% set showEventTypeOptions = False %} + + {% elif template.tag == 'labor-meeting' %} + {% set page_title = 'Create Weekly Labor Meeting' %} + {% set showRecurringToggle = True %} + {% set showEventTypeOptions = False %} + {% elif template.tag == 'no-program' %} {% set page_title = 'Create Other CELTS-Sponsored Event' %} {% else %} {% set page_title = 'Create ' + template.name + ' Event' %} {% endif %} -{% extends "base.html" %} + + {% extends "base.html" %} {% else %} {% set page_title = eventData.name %} {% extends "events/eventNav.html"%} @@ -56,7 +69,7 @@ {% endblock %} {{super()}} {% else %} -
+

{{page_title}}

{% endif %} @@ -243,63 +256,65 @@

{{page_title}}

- {% if page_title != 'Create All Volunteer Training' %} + {% if showEventTypeOptions %}
-
-
- -
- - -
-
- - -
-
- - -
- {% if eventData['program'].isBonnerScholars %} -
- - -
- {% endif %} -
- -
- -
- -
- - -
-
- {% set hide = "" if eventData.isRsvpRequired else "display: none" %} -
- - +
+
+ +
+ + +
+
+ + +
+
+ + +
+ {% if eventData['program'].isBonnerScholars %} +
+ + +
+ {% endif %} +
+ +
+ +
+ +
+ + +
+
+ {% set hide = "" if eventData.isRsvpRequired else "display: none" %} +
+ + +
+
+ + +
+ {% if not eventData.isLaborOnly %} +
+ + +
+ {% endif %} +
-
- - -
-
- -
-
-
-
-{% endif %} -
- - -
+ {% endif %} +
+ + +
{% if filepaths %}
@@ -332,6 +347,8 @@

{{page_title}}

+ +
diff --git a/app/templates/macros/programTableMacro.html b/app/templates/macros/programTableMacro.html new file mode 100644 index 000000000..49b458679 --- /dev/null +++ b/app/templates/macros/programTableMacro.html @@ -0,0 +1,66 @@ +{% macro programTable(eligibilityTable, volunteer) %} +
+ + + + + + + {% if g.current_user.isCeltsAdmin or g.current_user.isCeltsStudentStaff%} + + {% if g.current_user.isCeltsAdmin%} + + {% endif %} + {% endif %} + + + + + {% for row in eligibilityTable %} + {% set trainingList = row['trainingList'][volunteer.username] %} + {% set checked = "" %} + {% if row.program in programsInterested %} + {% set checked = "checked" %} + {% endif %} + {% from 'macros/trainingsHoverMacro.html' import trainingsHover %} + + + + + {% if g.current_user.isCeltsAdmin or g.current_user.isCeltsStudentStaff %} + {% if row.isNotBanned %} + + + {% if g.current_user.isCeltsAdmin %} + + {% endif %} + + + {% endif %} + + {% endfor %} + +
ProgramIndicated InterestedTrainingEligibilityOn Transcript
{{row.program.programName}} + {% set hasCompletedAllTrainings = row.completedTraining %} +    + View + Eligible + {% set label = "Ban" %} + {% else %} + Banned + {% set label = "Unban" %} + {% endif %} + + {% if g.current_user.isCeltsAdmin %} + + {% endif %} + + +
+
+ For more information about CELTS opportunities, click here. +
+ +{% endmacro %} \ No newline at end of file diff --git a/app/templates/main/extravanganzaWelcome.html b/app/templates/main/extravanganzaWelcome.html new file mode 100644 index 000000000..c73b78493 --- /dev/null +++ b/app/templates/main/extravanganzaWelcome.html @@ -0,0 +1,72 @@ +{%set title ="Volunteer Extravaganza"%} +{% extends "base.html"%} + +{% block scripts %} + {{super()}} + +{% endblock %} + +{% block styles %} + {{super()}} +{% endblock %} + +{% block app_content %} +

Welcome to the {{g.current_term.academicYear}} CELTS Volunteer Extravaganza
{{g.current_user.fullName}}!

+

Step 1: Indicate your interest in a CELTS programs below. Your interest helps us get you involved!

+

Step 2: Attend one of the All Volunteers Trainings listed below. Click to RSVP!

+

Step 3: Attend the Program-specific training to learn more about volunteering with that program!

+ +

CELTS Programs

+
+ + + + + +
All programs require attending one All Volunteers Training! + {% for training in upcomingAllVolunteers %} + {% if not loop.first %}
{% endif %} + {{training.name}} ({{training.startDate}} @ {{training.timeStart}}) + {% endfor %} +
+
+
+ + + + + + + + + + {% for program in programs %} + {% if program in programsInterested %} + {% set checked = "checked" %} + {% endif %} + + + + + + {% endfor %} + +
ProgramIndicate InterestUpcoming Trainings
(Required to participate)
{{program.programName}} + + + {% for training in upcomingTrainings %} + {% if training.program.id == program.id %} + {{training.name}} ({{training.startDate}} @ {{training.timeStart}}){% if loop.index != 0 %}
{% endif %} + {% endif %} + {% endfor %} +
+ For more information about CELTS opportunities, click here. +
+ + +{% endblock %} diff --git a/app/templates/main/userProfile.html b/app/templates/main/userProfile.html index b4cafbccf..94ae718de 100644 --- a/app/templates/main/userProfile.html +++ b/app/templates/main/userProfile.html @@ -5,10 +5,15 @@ {{super()}} + + + + {% endblock %} {% block styles %} {{super()}} + {% endblock %} @@ -18,11 +23,11 @@

{{volunteer.firstName}} {{volunteer.lastName}}

-
+
{{volunteer.bnumber}}
{{volunteer.email}}
-
+
{% if volunteer.major -%}
{{volunteer.major}}
{% endif %} @@ -37,6 +42,7 @@

{{volunteer.firstName}} {{volunteer.lastName}}

{%endif%}
+
- +
@@ -204,7 +210,7 @@

Program - Notify + Interest Training {% if g.current_user.isCeltsAdmin or g.current_user.isCeltsStudentStaff%} Eligibility @@ -265,7 +271,7 @@

- + {% if participatedInLabor or volunteer.isCeltsStudentStaff%} @@ -699,15 +705,66 @@
Dietary Restrictions
- + {% endif %} diff --git a/database/base_data.py b/database/base_data.py index be63e7670..b343d9278 100644 --- a/database/base_data.py +++ b/database/base_data.py @@ -53,6 +53,22 @@ "templateFile": "createEvent.html", "isVisible": True }, + { + "id": 3, + "name": "All CELTS Training", + "tag": "all-celts-training", + "templateJSON": '{"name": "All CELTS Training (Labor)","description": "Training for all CELTS Labor Students", "isTraining": true, "isService": false, "isRequired": true, "isLaborOnly": true, "isCeltsTraining": true, "rsvpLimit": ""}', + "templateFile": "createEvent.html", + "isVisible": True + }, + { + "id": 4, + "name": "Weekly Labor Meeting", + "tag": "labor-meeting", + "templateJSON": '{"name": "CELTS Labor Meeting","description": "Regularly scheduled CELTS labor meeting", "isTraining": true, "isService": false, "isRequired": true, "isLaborOnly": true, "rsvpLimit": ""}', + "templateFile": "createEvent.html", + "isVisible": True + }, ] EventTemplate.insert_many(templates).on_conflict_replace().execute() diff --git a/database/test_data.py b/database/test_data.py index 847bc2a4d..034fe5bc3 100644 --- a/database/test_data.py +++ b/database/test_data.py @@ -642,7 +642,7 @@ "term": 2, "name": "Empty Bowls Spring Event 1", "description": "Empty Bowls Spring 2021", - "isTraining": True, + "isTraining": False, "timeStart": datetime.strptime("6:00 pm", "%I:%M %p"), "timeEnd": datetime.strptime("9:00 pm", "%I:%M %p"), "location": "Seabury Center", @@ -1613,9 +1613,15 @@ }, { "user": "ayisie", - "positionTitle": "AGP Team Memeber", + "positionTitle": "AGP Team Member", "term": 2, "isAcademicYear": True + }, + { + "user": "neillz", + "positionTitle": "AGP Team Leader", + "term": 3, + "isAcademicYear": True } ] CeltsLabor.insert_many(celtsLabor).on_conflict_replace().execute() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index aeb9c89fd..470c3cc9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,6 +66,7 @@ python-dateutil==2.9.0.post0 python-dotenv==1.1.1 python-editor==1.0.4 PyYAML==6.0.2 +qrcode[pil]==8.2 regex==2025.7.34 requests==2.32.4 selenium==4.34.2 diff --git a/tests/code/test_event_list.py b/tests/code/test_event_list.py index fbe955150..6c0baa157 100644 --- a/tests/code/test_event_list.py +++ b/tests/code/test_event_list.py @@ -66,6 +66,7 @@ def test_getVolunteerOpportunities(training_events): training_events.term = 2 training_events.isService = True + training_events.isTraining = False training_events.deletionDate = None training_events.save() training_events.program.save() diff --git a/tests/code/test_events.py b/tests/code/test_events.py index 7f24e1053..c2727d6dd 100644 --- a/tests/code/test_events.py +++ b/tests/code/test_events.py @@ -233,19 +233,18 @@ def test_preprocessEventData_requirement(): def test_correctValidateNewEventData(): eventData = {'isFoodProvided': False, 'isRsvpRequired': False, 'isService': False, - 'isTraining': True,'isEngagement': False,'isRepeating': False, 'isLaborOnly': True, 'startDate': parser.parse('1999-12-12'), + 'isTraining': True,'isEngagement': False,'isRepeating': False, 'isLaborOnly': True, 'allowsLabor': False, 'programId': 1,'location': "a big room", - 'timeEnd': '06:00', 'timeStart': '04:00','description': "Empty Bowls Spring 2021", + 'startDate': parser.parse('1999-12-12'), 'timeEnd': '06:00', 'timeStart': '04:00','description': "Empty Bowls Spring 2021", 'name': 'Empty Bowls Spring Event 1','term': 1,'contactName': "Kaidou of the Beast",'contactEmail': 'beastpirates@gmail.com'} - eventData['isRepeating'] = False isValid, eventErrorMessage = validateNewEventData(eventData) - assert isValid == True + assert isValid assert eventErrorMessage == "All inputs are valid." @pytest.mark.integration def test_wrongValidateNewEventData(): - eventData = {'isFoodProvided': False, 'isRsvpRequired':False, 'isService':False, 'isLaborOnly': True, + eventData = {'isFoodProvided': False, 'isRsvpRequired':False, 'isService':False, 'isLaborOnly': True, 'allowsLabor': False, 'isTraining':True,'isEngagement': False, 'isRepeating':False, 'isSeries': False, 'programId':1, 'location':"a big room", 'timeEnd':'12:00', 'timeStart':'15:00', 'description':"Empty Bowls Spring 2021", 'name':'Empty Bowls Spring Event 1','term':1,'contactName': "Big Mom", 'contactEmail': 'weeeDDDINgCAKKe@gmail.com'} @@ -436,7 +435,7 @@ def test_attemptSaveMultipleOfferings(): @pytest.mark.integration def test_saveEventToDb_create(): - eventInfo = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isService':False, 'isLaborOnly': True, + eventInfo = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isService':False, 'isLaborOnly': False, 'allowsLabor': False, 'isTraining':True, 'isEngagement': False,'isRepeating': False,'isAllVolunteerTraining': True, 'seriesId':None, 'startDate': parser.parse('2021-12-12'), 'location':"a big room", 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', 'description':"Empty Bowls Spring 2021", @@ -471,21 +470,21 @@ def test_saveEventToDb_create(): def test_saveEventToDb_repeating(): with mainDB.atomic() as transaction: with app.app_context(): - eventInfo_1 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': True, + eventInfo_1 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': False, 'allowsLabor': False, 'isService':False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False, 'isRepeating': True, 'seriesId':1, 'startDate': parser.parse('12-12-2021'), 'location':"this is only a test", 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', 'description':"Empty Bowls Spring 2021", 'name':'Empty Bowls Spring','term':1,'contactName':"Brianblius Ramsablius", 'contactEmail': 'ramsayBlius@gmail.com'} - eventInfo_2 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': True, + eventInfo_2 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': False, 'allowsLabor': False, 'isService':False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False, 'isRepeating': True, 'seriesId':1, 'startDate': parser.parse('12-12-2021'), 'location':"this is only a test", 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', 'description':"Empty Bowls Spring 2021", 'name':'Empty Bowls Spring','term':1,'contactName':"Brianblius Ramsablius", 'contactEmail': 'ramsayBlius@gmail.com'} - eventInfo_3 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': True, + eventInfo_3 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': False, 'allowsLabor': False, 'isService':False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False, 'isRepeating': True, 'seriesId':1, 'startDate': parser.parse('12-12-2021'), 'location':"this is only a test", @@ -519,21 +518,21 @@ def test_saveEventToDb_repeating(): def test_saveEventToDb_nonRepeatingSeries(): with mainDB.atomic() as transaction: with app.app_context(): - eventInfo_1 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': True, + eventInfo_1 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': False, 'allowsLabor': False, 'isService':False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False, 'isRepeating': False, 'seriesId':1, 'startDate': parser.parse('12-12-2021'), 'location':"this is only a test", 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', 'description':"Empty Bowls Spring 2021", 'name':'Empty Bowls Spring','term':1,'contactName':"Brianblius Ramsablius", 'contactEmail': 'ramsayBlius@gmail.com'} - eventInfo_2 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': True, + eventInfo_2 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': False, 'allowsLabor': False, 'isService':False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False, 'isRepeating': False, 'seriesId':1, 'startDate': parser.parse('12-12-2021'), 'location':"this is only a test", 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', 'description':"Empty Bowls Spring 2021", 'name':'Empty Bowls Spring','term':1,'contactName':"Brianblius Ramsablius", 'contactEmail': 'ramsayBlius@gmail.com'} - eventInfo_3 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': True, + eventInfo_3 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isLaborOnly': False, 'allowsLabor': False, 'isService':False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False,'isRepeating': False, 'seriesId':1, 'startDate': parser.parse('12-12-2021'),'location':"this is only a test", @@ -590,6 +589,7 @@ def test_saveEventToDb_update(): 'isEngagement': False, 'isRsvpRequired': True, 'isLaborOnly': True, + 'allowsLabor': False, 'rsvpLimit': None, 'isAllVolunteerTraining': True, 'isService': False, @@ -683,20 +683,20 @@ def test_deleteEvent(): transaction.rollback() # create repeating events - event_1 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isService':False, 'isLaborOnly': True, + event_1 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isService':False, 'isLaborOnly': False, 'allowsLabor': False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False, 'isRepeating': True, 'seriesId':1, 'startDate': parser.parse('12-12-2021'), 'location':"this is only a test", 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', 'description':"Empty Bowls Spring 2021", 'name':'Empty Bowls Spring Week 1','term':1,'contactName':"Brianblius Ramsablius", 'contactEmail': 'ramsayBlius@gmail.com'} - event_2 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isService':False, 'isLaborOnly': True, + event_2 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isService':False, 'isLaborOnly': False, 'allowsLabor': False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False, 'isRepeating': True, 'seriesId':1, 'startDate': parser.parse('12-12-2021'), 'location':"this is only a test", 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', 'description':"Empty Bowls Spring 2021", 'name':'Empty Bowls Spring Week 2','term':1, 'contactName':"Brianblius Ramsablius", 'contactEmail': 'ramsayBlius@gmail.com'} - event_3 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isService':False, 'isLaborOnly': True, + event_3 = {'isFoodProvided': False, 'isRsvpRequired':False, 'rsvpLimit': None, 'isService':False, 'isLaborOnly': False, 'allowsLabor': False, 'isAllVolunteerTraining': True, 'isTraining':True, 'isEngagement': False, 'isRepeating': True, 'seriesId':1, 'startDate': parser.parse('12-12-2021'), 'location':"this is only a test", 'timeEnd':'09:00 PM', 'timeStart':'06:00 PM', @@ -1053,6 +1053,7 @@ def test_getParticipatedEventsForUser_participatedTypes(): startDate="2021-12-12", isAllVolunteerTraining=False, isLaborOnly=True, + allowsLabor=False, isService=False, program=program ) @@ -1067,6 +1068,7 @@ def test_getParticipatedEventsForUser_participatedTypes(): startDate="2021-12-13", isAllVolunteerTraining=False, isLaborOnly=False, + allowsLabor=False, isService=True, program=program ) @@ -1080,7 +1082,8 @@ def test_getParticipatedEventsForUser_participatedTypes(): location="The moon", startDate="2021-12-14", isAllVolunteerTraining=False, - isLaborOnly=True, + isLaborOnly=False, + allowsLabor=True, isService=True, program=program ) @@ -1095,6 +1098,7 @@ def test_getParticipatedEventsForUser_participatedTypes(): startDate="2021-12-15", isAllVolunteerTraining=True, isLaborOnly=False, + allowsLabor=False, isService=False, program=program ) diff --git a/tests/code/test_graduationManagement.py b/tests/code/test_graduationManagement.py index 705b214eb..93e50a26c 100644 --- a/tests/code/test_graduationManagement.py +++ b/tests/code/test_graduationManagement.py @@ -4,6 +4,8 @@ from app.models import mainDB from app.models.eventRsvp import EventRsvp +from app.models.eventRsvpLog import EventRsvpLog +from app.models.eventViews import EventView from app.models.user import User from app.models.bonnerCohort import BonnerCohort from app.models.celtsLabor import CeltsLabor @@ -76,6 +78,8 @@ def test_getGraduationManagementUsers(): ProfileNote.delete().execute() Note.delete().execute() ActivityLog.delete().execute() + EventRsvpLog.delete().execute() + EventView.delete().execute() User.delete().execute() testUser1 = User.create(username = 'usrtst1', diff --git a/tests/code/test_participants.py b/tests/code/test_participants.py index 31c615bf9..e3c8f5dee 100644 --- a/tests/code/test_participants.py +++ b/tests/code/test_participants.py @@ -2,10 +2,10 @@ import pytest from datetime import datetime, timedelta, time from peewee import IntegrityError, DoesNotExist -from app import app from flask import g +from app import app from werkzeug.datastructures import ImmutableMultiDict - +from playhouse.shortcuts import model_to_dict from app.models import mainDB from app.models.user import User from app.models.event import Event @@ -13,7 +13,8 @@ from app.models.program import Program from app.models.eventParticipant import EventParticipant from app.logic.volunteers import getEventLengthInHours, updateEventParticipants -from app.logic.participants import unattendedRequiredEvents, addBnumberAsParticipant, getEventParticipants, trainedParticipants, getParticipationStatusForTrainings, checkUserRsvp, checkUserVolunteer, addPersonToEvent, sortParticipantsByStatus +from app.logic.participants import unattendedRequiredEvents, addBnumberAsParticipant, getEventParticipants, getParticipationStatusForTrainings, checkUserRsvp, checkUserVolunteer, addPersonToEvent, sortParticipantsByStatus +from app.logic.users import trainedParticipants from app.models.eventRsvp import EventRsvp @@ -282,77 +283,96 @@ def test_trainedParticipants(): # tests for unattendedRequiredEvents @pytest.mark.integration def test_unattendedRequiredEvents(): + with mainDB.atomic() as transaction: - # test unattended events - program = 1 - user = 'ramsayb2' + # test unattended events + program = 1 + user = 'ramsayb2' + + # There are no events yet that are registered as trainings for this program + unattendedEvents = unattendedRequiredEvents(program, user) + assert len(unattendedEvents) == 0 - unattendedEvents = unattendedRequiredEvents(program, user) - assert len(unattendedEvents) == 1 + # Create a required training for this program + Event.create(name = "Hunger Initiatives test event", + term = Term.get(1), + description= "This Event is created to do whatever.", + timeStart= "06:00 PM", + timeEnd= "09:00 PM", + location = "The Sun", + isRsvpRequired = 0, + isTraining = 1, + isService = 0, + startDate= "2021-12-12", + recurringId = None, + program = Program.get_by_id(program)) - # test after user has attended an event - with mainDB.atomic() as transaction: + unattendedEvents = unattendedRequiredEvents(program, user) + assert len(unattendedEvents) == 1 + + # Have the user attend the event event = Event.get(Event.name == unattendedEvents[0]) EventParticipant.create(user = user, event = event) - unattendedEvents = unattendedRequiredEvents(program, user) assert len(unattendedEvents) == 0 transaction.rollback() - # test where all required events are attended - user = 'khatts' - unattendedEvents = unattendedRequiredEvents(program, user) - assert unattendedEvents == [] + # test where all required events are attended + user = 'khatts' + unattendedEvents = unattendedRequiredEvents(program, user) + assert unattendedEvents == [] - # test for program with no requirements - program = 4 - unattendedEvents = unattendedRequiredEvents(program, user) - assert unattendedEvents == [] + # test for a program with no requirements + program = 4 + unattendedEvents = unattendedRequiredEvents(program, user) + assert unattendedEvents == [] - # test for incorrect program - program = 500 - unattendedEvents = unattendedRequiredEvents(program, user) - assert unattendedEvents == [] + # test for invalid program + program = 500 + unattendedEvents = unattendedRequiredEvents(program, user) + assert unattendedEvents == [] - #test for incorrect user - program = 1 - user = "asdfasdf56" - unattendedEvents = unattendedRequiredEvents(program, user) - assert unattendedEvents == ['Empty Bowls Spring Event 1'] + #test for invalid user + program = 1 + user = "asdfasdf56" + unattendedEvents = unattendedRequiredEvents(program, user) + assert unattendedEvents == [] @pytest.mark.integration def test_addBnumberAsParticipant(): # Tests the Kiosk - # user is banned with mainDB.atomic() as transaction: - signedInUser, userStatus = addBnumberAsParticipant("B00739736", 2) - assert userStatus == "banned" + with app.app_context(): + g.current_term = Term.get_by_id(1) + # Test a banned user + signedInUser, userStatus = addBnumberAsParticipant("B00739736", 2) + assert userStatus == "banned" - # user is already signed in - signedInUser, userStatus = addBnumberAsParticipant("B00751360", 2) - assert userStatus == "already signed in" + # user is already signed in + signedInUser, userStatus = addBnumberAsParticipant("B00751360", 2) + assert userStatus == "already signed in" - # user is eligible but the user is not in EventParticipant and EventRsvp - signedInUser = User.get(User.bnumber=="B00759117") - with pytest.raises(DoesNotExist): - EventParticipant.get(EventParticipant.user==signedInUser, EventParticipant.event==2) - EventRsvp.get(EventRsvp.user==signedInUser, EventRsvp.event==2) + # user is eligible but the user is not in EventParticipant and EventRsvp + signedInUser = User.get(User.bnumber=="B00759117") + with pytest.raises(DoesNotExist): + EventParticipant.get(EventParticipant.user==signedInUser, EventParticipant.event==2) + EventRsvp.get(EventRsvp.user==signedInUser, EventRsvp.event==2) - signedInUser, userStatus = addBnumberAsParticipant("B00759117", 2) - assert userStatus == "success" + signedInUser, userStatus = addBnumberAsParticipant("B00759117", 2) + assert userStatus == "success" - participant = EventParticipant.select().where(EventParticipant.event==2, EventParticipant.user==signedInUser) - assert "agliullovak" in participant + participant = EventParticipant.select().where(EventParticipant.event==2, EventParticipant.user==signedInUser) + assert "agliullovak" in participant - userRsvp = EventRsvp.select().where(EventRsvp.event==2, EventRsvp.user==signedInUser) - assert "agliullovak" in userRsvp + userRsvp = EventRsvp.select().where(EventRsvp.event==2, EventRsvp.user==signedInUser) + assert "agliullovak" in userRsvp - EventParticipant.delete(EventParticipant.user==signedInUser, EventParticipant.event==2).execute() - EventRsvp.delete(EventRsvp.user==signedInUser, EventRsvp.event==2).execute() - transaction.rollback() + EventParticipant.delete(EventParticipant.user==signedInUser, EventParticipant.event==2).execute() + EventRsvp.delete(EventRsvp.user==signedInUser, EventRsvp.event==2).execute() + transaction.rollback() @pytest.mark.integration def test_getEventParticipants(): diff --git a/tests/code/test_spreadsheet.py b/tests/code/test_spreadsheet.py index e950b3d90..1391030c3 100644 --- a/tests/code/test_spreadsheet.py +++ b/tests/code/test_spreadsheet.py @@ -34,7 +34,7 @@ def fixture_info(): isCanceled=False, deletionDate=None, isService=True, - isLaborOnly=True + isLaborOnly=False ) event2 = Event.create( name='Event2', @@ -44,7 +44,7 @@ def fixture_info(): isCanceled=False, deletionDate=None, isService=True, - isLaborOnly=True + isLaborOnly=False ) event3 = Event.create( name='Event3', diff --git a/tests/code/test_users.py b/tests/code/test_users.py index a344ffbb0..ad5bdad74 100644 --- a/tests/code/test_users.py +++ b/tests/code/test_users.py @@ -6,22 +6,26 @@ from flask import g, request, session from app.models import mainDB +from app.models.eventParticipant import EventParticipant from app.models.program import Program from app.models.programBan import ProgramBan from app.models.note import Note from app.models.profileNote import ProfileNote +from app.models.term import Term from app.models.user import User from app.models.programManager import ProgramManager from app.models.backgroundCheck import BackgroundCheck from app.models.event import Event from app.logic.users import addUserInterest, removeUserInterest, banUser, unbanUser, isEligibleForProgram, getUserBGCheckHistory, addProfileNote, deleteProfileNote, getBannedUsers, isBannedFromEvent, updateDietInfo from app.logic.volunteers import addUserBackgroundCheck, deleteUserBackgroundCheck +from playhouse.shortcuts import model_to_dict @pytest.mark.integration def test_deleteUserBackgroundCheck(): with mainDB.atomic() as transaction: with app.app_context(): g.current_user = "ramsayb2" + g.current_term = Term.get_by_id(1) # Create a test user to run background checks on testUser = User.create(username = 'zawn', @@ -69,19 +73,56 @@ def test_user_model(): @pytest.mark.integration def test_isEligibleForProgram(): + with mainDB.atomic() as transaction: + with app.app_context(): + g.current_term = Term.get_by_id(1) + + # Test user Sandesh's eligibility for program 2 + user = User.get(User.username == "lamichhanes2") + program = Program.get(Program.id == 2) + + # Sandesh is ineligible at first (tests id's and obj's) + eligible = isEligibleForProgram(2, "lamichhanes2") + assert not eligible + eligible = isEligibleForProgram(program, user) + assert not eligible + + # Sandesh completes All Volunteers training (he is still ineligible) + avt = Event.get(Event.isAllVolunteerTraining) + EventParticipant.create(user=user, event=avt) + eligible = isEligibleForProgram(2, user) + assert not eligible + + + # Sandesh attends Program-specific training (still not eligible!) + programSpecificTraining = Event.create( + name="Program Specific Training", + term=g.current_term, + description="Program Specific Training", + timeStart="18:00:00", + timeEnd="21:00:00", + location="The moon", + startDate="2021-12-15", + isAllVolunteerTraining=True, + isLaborOnly=False, + isTraining=True, + program=program + ) + EventParticipant.create(user=user, event=programSpecificTraining) + eligible = isEligibleForProgram(2, user) + assert not eligible + + # Sandesh signs the handbook (he is NOW eligible) + user.lastHandbookSignature = "2026-07-21" + user.signatureTerm = g.current_term + user.save() + print(user.signatureTerm.academicYear) + print(g.current_term.academicYear) + eligible = isEligibleForProgram(2, user) + assert eligible - # user has attended all required events - user = User.get(User.username == "lamichhanes2") - program = Program.get(Program.id == 2) - - eligible = isEligibleForProgram(2, "lamichhanes2") - assert eligible - eligible = isEligibleForProgram(program, user) - assert eligible + transaction.rollback() - # there are no required events - eligible = isEligibleForProgram(4, "ayisie") - assert eligible @pytest.mark.integration def test_addUserInterest(): @@ -360,6 +401,7 @@ def test_getUserBGCheckHistory(): with mainDB.atomic() as transaction: with app.app_context(): g.current_user = "ramsayb2" + g.current_term = Term.get_by_id(1) # Create a test user to run background checks on testusr = User.create(username = 'usrtst', @@ -420,25 +462,27 @@ def test_getBannedUsers(): @pytest.mark.integration def test_isBannedFromEvent(): with mainDB.atomic() as transaction: - userToBan = User.create(username = 'usrtst', # Test banned user - firstName = 'Test', - lastName = 'User', - bnumber = '03522492', - email = 'usert@berea.deu', - isStudent = True) - banUser(1, User.get_by_id("usrtst"), "nope", "2050-11-29", "ramsayb2") - assert isBannedFromEvent("usrtst", 1) - - unbanUser(1, 'usrtst', "yep", "ramsayb2") # Test eligible but previously banned user - assert not isBannedFromEvent("usrtst", 1) - - notBannedUser = User.create(username = 'usrtst2', # Test eligible user - firstName = 'Test', - lastName = 'User 2', - bnumber = '03522493', - email = 'usert2@berea.deu', - isStudent = True) - assert not isBannedFromEvent("usrtst2", 1) + with app.app_context(): + g.current_term = Term.get_by_id(1) + userToBan = User.create(username = 'usrtst', # Test banned user + firstName = 'Test', + lastName = 'User', + bnumber = '03522492', + email = 'usert@berea.deu', + isStudent = True) + banUser(1, User.get_by_id("usrtst"), "nope", "2050-11-29", "ramsayb2") + assert isBannedFromEvent("usrtst", 1) + + unbanUser(1, 'usrtst', "yep", "ramsayb2") # Test eligible but previously banned user + assert not isBannedFromEvent("usrtst", 1) + + notBannedUser = User.create(username = 'usrtst2', # Test eligible user + firstName = 'Test', + lastName = 'User 2', + bnumber = '03522493', + email = 'usert2@berea.deu', + isStudent = True) + assert not isBannedFromEvent("usrtst2", 1) transaction.rollback() @pytest.mark.integration diff --git a/tests/code/test_volunteers.py b/tests/code/test_volunteers.py index 69a0d86a9..321f7e6e9 100644 --- a/tests/code/test_volunteers.py +++ b/tests/code/test_volunteers.py @@ -10,6 +10,8 @@ from peewee import DoesNotExist from dateutil import parser +from app.models.term import Term + @pytest.mark.integration def test_getEventLengthInHours(): @@ -111,6 +113,7 @@ def test_backgroundCheck(): with mainDB.atomic() as transaction: with app.app_context(): g.current_user = "ramsayb2" + g.current_term = Term.get_by_id(1) # tests the model created in tests_data and the one that is created (multiple entries) updatebackground = addUserBackgroundCheck("khatts","CAN","Submitted",parser.parse("2020-07-20")) updatedModel = list(BackgroundCheck.select().where(BackgroundCheck.user == "khatts", BackgroundCheck.type == "CAN"))