diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 8d24ffdf6..b9032991e 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -323,7 +323,6 @@ def eventDisplay(eventId): for year, cohort in rawBonnerCohorts.items(): if cohort: bonnerCohorts[year] = cohort - invitedCohorts = list(EventCohort.select().where( EventCohort.event_id == eventId, )) diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index f9d17d1c4..45b9a2589 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -221,9 +221,7 @@ def viewUsersProfile(username): "onTranscript": onTranscript}), profileNotes = ProfileNote.select().where(ProfileNote.user == volunteer) - bonnerRequirements = getCertRequirementsWithCompletion(certification=Certification.BONNER, username=volunteer) - managersProgramDict = getManagerProgramDict(g.current_user) managersList = [id[1] for id in managersProgramDict.items()] totalSustainedEngagements = getEngagementTotal(getCommunityEngagementByTerm(volunteer)) diff --git a/app/logic/certification.py b/app/logic/certification.py index 70c3beb03..8562e09b5 100644 --- a/app/logic/certification.py +++ b/app/logic/certification.py @@ -1,18 +1,95 @@ -from peewee import JOIN, DoesNotExist, Case - +from peewee import JOIN, fn, DoesNotExist, Case +from flask import g +from app.models.event import Event +from app.models.term import Term from app.models.certification import Certification from app.models.certificationRequirement import CertificationRequirement from app.models.requirementMatch import RequirementMatch from app.models.eventParticipant import EventParticipant +from app.models.user import User +import math +def termsAttended(certification, username): + ''' + Retrieve terms attended by a user for certification and filter them based on frequency of a term + ''' + attendedTerms = [] + attendance = (RequirementMatch.select() + .join(EventParticipant, JOIN.LEFT_OUTER, on=(RequirementMatch.event == EventParticipant.event)) + .where(RequirementMatch.requirement_id == certification) + .where(EventParticipant.user == username)) + for termRecord in range(len(attendance)): + if not attendance[termRecord].event.term.isSummer: + attendedTerms.append(attendance[termRecord].event.term.description) + totalTerms = termsInTotal(username) + attendedTerms = {term for term in attendedTerms if term in totalTerms} + return attendedTerms + + +def termsInTotal(username): + ''' + The function returns all non-summer academic terms a student should have, based on their class level where it finds + the start term and populate from it with Fall-start alignment and special handling for NULL/Non-degree class level + ''' + currentTerm = g.current_term + currentDesc = currentTerm.description + user = User.select().where(User.username == username).get() + if currentTerm.isSummer and user.rawClassLevel == "Freshman": + currentDesc = f"Fall {currentTerm.year}" + elif currentTerm.isSummer: + currentDesc = f"Spring {currentTerm.year}" + classLevel = ["Freshman", "Sophomore", "Junior", "Senior"] + totalTerms = [] + for level, name in enumerate(classLevel): + if user.rawClassLevel == name: + totalTermsCount = (level + 1) * 2 + if currentDesc.startswith("Fall"): + totalTermsCount -= 1 + if currentDesc.startswith("Spring"): + startYear = currentTerm.year - level - 1 + else: + startYear = currentTerm.year - level + for k in range(totalTermsCount): + if k % 2 == 0: + season = "Fall" + year = startYear + (k // 2) + else: + season = "Spring" + year = startYear + (k // 2) + 1 + totalTerms.append(f"{season} {year}") + break + + if user.rawClassLevel is None or user.rawClassLevel in ["NULL", "Graduating", "Non-Degree"]: + totalTermsCount = 8 + currentYear = currentTerm.year + currentSeason = "Fall" if "Fall" in currentDesc else "Spring" + for a in range(totalTermsCount): + totalTerms.append(f"{currentSeason} {currentYear}") + if currentSeason == "Fall": + currentSeason = "Spring" + else: + currentSeason = "Fall" + currentYear -= 1 + list.reverse(totalTerms) + return totalTerms + +def termsMissed(certification, username): + ''' + Calculate how many certification-eligible terms a student has missed based on their class level + and attendance record. + ''' + totalTerms = termsInTotal(username) + attendedTerms = termsAttended(certification, username) + missedTerms = [term for term in totalTerms if term not in attendedTerms] + return missedTerms + def getCertRequirementsWithCompletion(*, certification, username): """ - Function to differentiate between simple requirements and requirements completion checking. - See: `getCertRequirements` + Differentiate between simple requirements and requirements completion checking. """ - return getCertRequirements(certification, username) + return getCertRequirements(certification, username, reqCheck=True) -def getCertRequirements(certification=None, username=None): +def getCertRequirements(certification=None, username=None, reqCheck=False): """ Return the requirements for all certifications, or for one if requested. @@ -28,7 +105,6 @@ def getCertRequirements(certification=None, username=None): reqList = (Certification.select(Certification, CertificationRequirement) .join(CertificationRequirement, JOIN.LEFT_OUTER, attr="requirement") .order_by(Certification.id, CertificationRequirement.order.asc(nulls="LAST"))) - if certification: if username: # I don't know how to add something to a select, so we have to recreate the whole query :( @@ -37,32 +113,63 @@ def getCertRequirements(certification=None, username=None): .select(Certification, CertificationRequirement, completedCase.alias("completed")) .join(CertificationRequirement, JOIN.LEFT_OUTER, attr="requirement") .join(RequirementMatch, JOIN.LEFT_OUTER) - .join(EventParticipant, JOIN.LEFT_OUTER, on=(RequirementMatch.event == EventParticipant.event)) - .where(EventParticipant.user.is_null(True) | (EventParticipant.user == username)) + .join(EventParticipant, JOIN.LEFT_OUTER, on=(RequirementMatch.event == EventParticipant.event) & (EventParticipant.user == username)) .order_by(Certification.id, CertificationRequirement.order.asc(nulls="LAST"))) - # we have to add the is not null check so that `cert.requirement` always exists reqList = reqList.where(Certification.id == certification, CertificationRequirement.id.is_null(False)) - reqList = reqList.distinct() - - certs = [] + certificationList = [] for cert in reqList: if username: cert.requirement.completed = bool(cert.__dict__['completed']) - certs.append(cert.requirement) - return certs - - #return [cert.requirement for cert in reqList] - - certs = {} + # this is to get the calculation when it comes to events with term, twice, annual as their frequency + cert.requirement.attendedTerms = len(termsAttended(cert.requirement.id, username)) + cert.requirement.attendedDescriptions = termsAttended(cert.requirement.id, username) + if cert.requirement.frequency == "term": + cert.requirement.missedTerms = len(termsMissed(cert.requirement.id, username)) + cert.requirement.missedDescriptions = termsMissed(cert.requirement.id, username) + cert.requirement.totalTerms = len(termsInTotal(username)) + elif cert.requirement.frequency == "annual": + totalTerms = len(termsInTotal(username)) + cert.requirement.attendedAnnual = len(termsAttended(cert.requirement.id, username)) + cert.requirement.totalAnnual = int(math.floor(totalTerms/2+0.5)) if totalTerms % 2 == 1 else totalTerms/2 + elif cert.requirement.frequency == "once" and cert.requirement.completed: + term_record = (RequirementMatch + .select(RequirementMatch, Event, Term) + .join(Event) + .join(Term) + .where(RequirementMatch.requirement == cert.requirement.id) + .order_by(Term.year.desc()) # latest term first + .first() + ) + cert.requirement.attendedTerm = term_record.event.term.description + certificationList.append(cert.requirement) + + # the .distinct() doesn't work efficiently, so we have to manually go through the list and removed duplicates that exist + validCertification = set() + certificationIndex = 0 + uniqueCertification = [] + + for cert in certificationList: + req = certificationList[certificationIndex] + if req not in validCertification: + validCertification.add(req) + uniqueCertification.append(req) + # Override incomplete requirement when a completed 'once' requirement is found when removing duplicates + elif reqCheck and req.frequency == "once" and req.completed: + for i in range(len(uniqueCertification)): + if uniqueCertification[i].id == req.id and not uniqueCertification[i].completed: + uniqueCertification[i] = req + validCertification.add(req) + certificationIndex += 1 + certificationList = uniqueCertification + return certificationList + certificationDict = {} for cert in reqList: - if cert.id not in certs.keys(): - certs[cert.id] = {"data": cert, "requirements": []} - + if cert.id not in certificationDict.keys(): + certificationDict[cert.id] = {"data": cert, "requirements": []} if getattr(cert, 'requirement', None): - certs[cert.id]["requirements"].append(cert.requirement) - - return certs + certificationDict[cert.id]["requirements"].append(cert.requirement) + return certificationDict def updateCertRequirements(certId, newRequirements): """ diff --git a/app/logic/volunteerSpreadsheet.py b/app/logic/volunteerSpreadsheet.py index 6a03ca6ac..f75d58fff 100644 --- a/app/logic/volunteerSpreadsheet.py +++ b/app/logic/volunteerSpreadsheet.py @@ -195,7 +195,7 @@ def getRetentionRate(academicYear): def termParticipation(term): base = getBaseQuery(term.academicYear) - + participationQuery = (base.select(Event.program, EventParticipant.user_id.alias('participant'), Program.programName.alias("programName")) .where(Event.term == term) .order_by(EventParticipant.user)) diff --git a/app/static/js/userProfile.js b/app/static/js/userProfile.js index 7fe6a2c8b..51d5344fc 100644 --- a/app/static/js/userProfile.js +++ b/app/static/js/userProfile.js @@ -1,6 +1,6 @@ $(document).ready(function(){ - $("#checkDietRestriction").on("change", function() { + $("#checkDietRestriction").on("change", function() { let norestrict = $(this).is(':checked'); if (norestrict) { $("#dietContainer").hide(); @@ -35,7 +35,6 @@ $(document).ready(function(){ }); }) - $("#phoneInput").inputmask('(999)-999-9999'); $(".notifyInput").click(function updateInterest(){ var programID = $(this).data("programid"); var username = $(this).data('username'); @@ -216,6 +215,8 @@ $(document).ready(function(){ } }); }); + }); + $(".deleteNoteButton").click(function() { let username = $(this).data('username') @@ -307,17 +308,27 @@ $(document).ready(function(){ }) }); - // Popover functionality - var requiredTraining = $(".trainingPopover"); - requiredTraining.popover({ - trigger: "hover", - sanitize: false, - html: true, - content: function() { - return $(this).attr('data-content'); - } + $(function () { + $('.trainingPopover').each(function () { + new bootstrap.Popover(this, { + trigger: 'hover focus', + html: true, + sanitize: false, + placement: 'right', + }); + }); }); - + $(function () { + $('.bonnerCheckmark').each(function () { + new bootstrap.Popover(this, { + trigger: 'hover focus', + html: true, + sanitize: false, + placement: 'right', + }); + }); + }); + setupPhoneNumber("#updatePhone", "#phoneInput") // Dietary Restrictions @@ -355,8 +366,10 @@ $(document).ready(function(){ typingTimer = setTimeout(saveDiet, saveInterval); }); }); - -}); // end document.ready() + const bonnerStudent = $("#bonnerStudent").data('username') + if (bonnerStudent === "False"){ + $("#bonnerStudent").prop("hidden", true) + }; // end document.ready() // Update program manager status function updateManagers(el, volunteerUsername ) { @@ -387,3 +400,4 @@ function updateManagers(el, volunteerUsername ) { } }) } + diff --git a/app/templates/admin/bonnerManagement.html b/app/templates/admin/bonnerManagement.html index ebe02bf95..54c30eddd 100644 --- a/app/templates/admin/bonnerManagement.html +++ b/app/templates/admin/bonnerManagement.html @@ -148,6 +148,7 @@