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..e2f86dec4 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 @@ -97,7 +98,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": @@ -114,8 +115,9 @@ def createEvent(templateid, programid): if request.method == "POST": savedEvents = None eventData.update(request.form.copy()) + print(">>>>>>>> createEvent before preprocess 1: ", list(Event.select().where(Event.isCeltsTraining))) eventData = preprocessEventData(eventData) - + print(">>>>>>>> createEvent after preprocess 1: ", list(Event.select().where(Event.isCeltsTraining))) if eventData.get('isSeries'): eventData['seriesData'] = json.loads(eventData['seriesData']) succeeded, savedEvents, failedSavedOfferings = attemptSaveMultipleOfferings(eventData, getFilesFromRequest(request)) @@ -130,7 +132,6 @@ def createEvent(templateid, programid): except Exception as e: print("Failed saving regular event", e) validationErrorMessage = "Failed to save event." - if savedEvents: rsvpCohorts = request.form.getlist("cohorts[]") if rsvpCohorts: @@ -180,8 +181,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, @@ -289,7 +288,7 @@ 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)) @@ -682,3 +681,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..647b9bdcf 100644 --- a/app/controllers/admin/userManagement.py +++ b/app/controllers/admin/userManagement.py @@ -1,7 +1,13 @@ -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, 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 @@ -9,11 +15,14 @@ from app.logic.fileHandler import FileHandler from app.logic.userManagement import addCeltsAdmin,addCeltsStudentStaff,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,56 @@ 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", "backgroundForm"]: + 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 + elif fileCategory == "backgroundForm": + t.backgroundForm = 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 + ) + diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 6f83f32dd..ba63f9f08 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,7 +38,7 @@ 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.participants import hasGoneToTraining, unattendedRequiredEvents, trainedParticipants, getParticipationStatusForTrainings, checkUserRsvp, addPersonToEvent from app.logic.users import addUserInterest, removeUserInterest, banUser, unbanUser, isEligibleForProgram, getUserBGCheckHistory, addProfileNote, deleteProfileNote, updateDietInfo @main_bp.route('/logout', methods=['GET']) @@ -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) @@ -643,3 +661,24 @@ 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) + 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..54e0f1626 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,37 +195,37 @@ 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'], + "includesLabor" : newEventData['includesLabor'], + "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() @@ -390,6 +400,7 @@ def getUpcomingEventsForUser(user, asOf=datetime.now(), program=None): return eventsList +#TODO This method appears to be very poorly tested, and very complex. NEEDS TESTING! def getParticipatedEventsForUser(user): """ Get all the events a user has participated in. @@ -399,17 +410,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.includesLabor | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"), + ((Event.includesLabor | 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.isLaborOnly == False, Event.isCeltsTraining == False) .order_by(Event.startDate, Event.name)) allVolunteer = (Event.select(Event, "", Value("Volunteer").alias("participatedType")) .join(EventParticipant) @@ -428,7 +437,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['includesLabor']]: return (False, "Raw form data passed to validate method. Preprocess first.") if data['timeEnd'] <= data['timeStart']: @@ -506,9 +515,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', 'includesLabor', 'isLaborOnly', 'isCeltsTraining'] + for checkBox in eventCheckBoxes: if checkBox not in eventData: eventData[checkBox] = False @@ -551,8 +561,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 +746,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..66ba1a05c 100644 --- a/app/logic/participants.py +++ b/app/logic/participants.py @@ -1,18 +1,22 @@ 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.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.models.backgroundCheck import BackgroundCheck from app.logic.users import isEligibleForProgram from app.logic.volunteers import getEventLengthInHours from app.logic.events import getEventRsvpCountsForTerm from app.logic.createLogs import createRsvpLog from collections import defaultdict + def trainedParticipants(programID, targetTerm): """ This function tracks the users who have attended every Prerequisite @@ -130,14 +134,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 +169,74 @@ 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] + 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' + }, + 'userID2': {'userObj: , + 'allVolunteer': True, + 'programSpecific': False, + 'bgCheck': '0/22/2026' + } + } + + Gracefully handles multiple trainings of the same type (e.g., two All Volunteers Trainings) + """ + trainedUsers = getParticipationStatusForTrainings(programID, interestedUsers, g.current_term, returnStr = False) + bannedUsers = list(User + .select(User.username) + .join(ProgramBan) + .where(ProgramBan.program == programID, + User.username << [user.username for user in interestedUsers])) - return {user.username: list(userParticipationStatus[user.username].values()) for user in userList} + bgCheckSubmitted = (User.select(User.username, BackgroundCheck.dateCompleted) + .join(BackgroundCheck) + .where(BackgroundCheck.backgroundCheckStatus == "Submitted").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" + for event in trainedUsers[interestedUser]: + if not event[1]: # they didn't attend + continue + elif event[0].isAllVolunteerTraining: # They attended the training + trainedAndInterested[interestedUser.username]["allVolunteer"] = True + elif event[0].isTraining: + trainedAndInterested[interestedUser.username]["programSpecific"] = True + trainedAndInterested[interestedUser.username]["eligible"] = True + if interestedUser in bannedUsers: + trainedAndInterested[interestedUser.username]["eligible"] = False + if interestedUser in bgCheckSubmitted: + trainedAndInterested[interestedUser.username]['bgCheck'] = "Submitted" + + return trainedAndInterested + +def getParticipantsForProgramForAY(programID, academicYear): + participants = (User.select() + .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 +267,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..32ca8c6cf 100644 --- a/app/logic/term.py +++ b/app/logic/term.py @@ -15,9 +15,16 @@ def addNextTerm(): newDescription = newSemesterMap[prevSemester] + " " + str(newYear) newAY = prevTerm.academicYear + volunteerHandbook = None + laborHandbook = None + backgroundForm = 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 + backgroundForm = prevTerm.backgroundForm semester = newDescription.split()[0] summer= "Summer" in semester @@ -25,7 +32,12 @@ def addNextTerm(): year=newYear, academicYear=newAY, isSummer= summer, - termOrder=Term.convertDescriptionToTermOrder(newDescription)) + termOrder=Term.convertDescriptionToTermOrder(newDescription), + volunteerHandbook=volunteerHandbook, + laborHandbook=laborHandbook, + backgroundForm=backgroundForm + ) + newTerm.save() return newTerm @@ -52,11 +64,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/users.py b/app/logic/users.py index 36b9ba2bf..cef9cd742 100644 --- a/app/logic/users.py +++ b/app/logic/users.py @@ -51,6 +51,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. diff --git a/app/logic/volunteerSpreadsheet.py b/app/logic/volunteerSpreadsheet.py index f75b40039..fbbcdb8ac 100644 --- a/app/logic/volunteerSpreadsheet.py +++ b/app/logic/volunteerSpreadsheet.py @@ -119,12 +119,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.includesLabor), 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) 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..06fba26d1 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) + includesLabor = 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) diff --git a/app/models/term.py b/app/models/term.py index 34f85261b..599691b71 100644 --- a/app/models/term.py +++ b/app/models/term.py @@ -6,9 +6,19 @@ class Term(baseModel): isSummer = BooleanField(default=False) isCurrentTerm = BooleanField(default=False) termOrder = CharField() + volunteerHandbook = CharField(null=True) + laborHandbook = CharField(null=True) + backgroundForm = 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/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/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/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..f39fc1e44 --- /dev/null +++ b/app/static/js/extravaganza.js @@ -0,0 +1,24 @@ +$(document).ready(function(){ + + $(".interestedInput").click(function updateInterest(){ + var programID = $(this).data("programid"); + var username = $(this).data('username'); + + console.log(programID + 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..df23af4dd --- /dev/null +++ b/app/static/js/handbookSignature.js @@ -0,0 +1,69 @@ +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(); + } + }) + } +}); \ No newline at end of file diff --git a/app/static/js/rosterManagement.js b/app/static/js/rosterManagement.js new file mode 100644 index 000000000..34dde871c --- /dev/null +++ b/app/static/js/rosterManagement.js @@ -0,0 +1,51 @@ +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); + } + }) +}); \ No newline at end of file diff --git a/app/static/js/searchStudent.js b/app/static/js/searchStudent.js index 93062f6de..a2dedd590 100644 --- a/app/static/js/searchStudent.js +++ b/app/static/js/searchStudent.js @@ -1,16 +1,37 @@ import searchUser from './searchUser.js' + function callback(selected) { - $("#searchStudent").submit(); + console.log(selected); + $("#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(); +}); + + + +// import searchUser from './searchUser.js' +// function callback(selected) { +// console.log(selected); +// $("#searchStudentsInput").submit(); +// } +// $(document).ready(function() { +// $("#searchStudentsInput").on("input", function() { +// searchUser("searchStudentsInput", callback); +// }); + +// $("#searchIcon").click(function (e) { +// e.preventDefault(); +// callback($("#searchStudentsInput").val()); +// }); +// $("#searchStudentsInput").focus() +// }) diff --git a/app/static/js/searchUser.js b/app/static/js/searchUser.js index 5ebf57c70..a73b5976c 100644 --- a/app/static/js/searchUser.js +++ b/app/static/js/searchUser.js @@ -1,41 +1,80 @@ 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 }); }; + + +// 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) { +// $.ajax({ +// url: `/searchUser/${query}`, +// type: "GET", +// dataType: "json", +// data:{"category":category}, +// success: function(searchResults) { +// console.log(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], +// } +// } +// ))}, +// 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 +// }); +// }; diff --git a/app/static/js/userManagement.js b/app/static/js/userManagement.js index 2e5e91998..020250e1b 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,38 @@ 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); + } + if(response["backgroundForm"]) { + $("#backgroundFormURL").text("AY " + response["academicYear"] + " - Background Check Form") + $("#backgroundFormURL").removeAttr("hidden"); + } else { + $("#backgroundFormURL").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") + // console.log(error, status) } }) } diff --git a/app/static/js/userProfile.js b/app/static/js/userProfile.js index 61af76787..676e8840a 100644 --- a/app/static/js/userProfile.js +++ b/app/static/js/userProfile.js @@ -1,6 +1,9 @@ -$(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 +39,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 +164,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 +224,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 +242,7 @@ $(document).ready(function(){ $("#confirmDeleteNote").data('username', $(this).data('username')) $("#confirmDeleteNote").data('noteid', $(this).data('noteid')) $("#deleteNoteWarning").modal("show") - + }); $("#confirmDeleteNote").click(function() { @@ -277,7 +281,8 @@ $(document).ready(function(){ $("#noteModal").modal("toggle") -}); + + $.ajax({ method: "POST", url: "/" + username + "/editNote", @@ -287,6 +292,7 @@ $(document).ready(function(){ } }); }); +}); /* * Background Check Functionality */ @@ -365,14 +371,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 +451,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..c22726def 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}} +
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+ {{g.current_term}} +
+ +
+
+
diff --git a/app/templates/admin/viewRoster.html b/app/templates/admin/viewRoster.html new file mode 100644 index 000000000..e43eee362 --- /dev/null +++ b/app/templates/admin/viewRoster.html @@ -0,0 +1,166 @@ +{% 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 VolunteerEmailAY {{g.current_term.academicYear}} handbookAll Volunteers TrainingProgram Specific TrainingEligibleBackground Check
{{trainedAndInterested[prospectiveVolunteer]["userObj"].fullName}}{{trainedAndInterested[prospectiveVolunteer]["userObj"].email}}{% if trainedAndInterested[prospectiveVolunteer]["userObj"].signatureTerm.academicYear == g.current_term.academicYear %} Signed {% else %} Not signed {% endif %}{% if trainedAndInterested[prospectiveVolunteer]['allVolunteer'] %} Attended {% else %} Not attended {% endif %}{% if trainedAndInterested[prospectiveVolunteer]['programSpecific'] %} Attended {% else %} Not attended {% endif %}{% if trainedAndInterested[prospectiveVolunteer]['eligible'] %} Yes {% else %} No {% 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..97386d252 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}}

+ + - + {% if participatedInLabor or volunteer.isCeltsStudentStaff%} @@ -664,7 +670,7 @@
End Date: &nbs -