Skip to content

Commit 3edf3df

Browse files
authored
Merge pull request #1804 from BCStudentSoftwareDevTeam/development
Dev -> Prod 2026-09-09
2 parents 5f7f5ea + f1b05d9 commit 3edf3df

28 files changed

Lines changed: 579 additions & 312 deletions

‎app/__init__.py‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,8 @@ def load_user():
7777
from app.logic.loginManager import getCurrentTerm
7878
@app.before_request
7979
def load_currentTerm():
80-
# An exception handles both current_term not being set and a mismatch between models
81-
try:
82-
g.current_term = dict_to_model(Term, session['current_term'])
83-
except Exception as e:
84-
term = getCurrentTerm()
85-
session['current_term'] = model_to_dict(term)
86-
g.current_term = term
80+
# Query the current term from the database on each request to avoid stale session data
81+
g.current_term = getCurrentTerm()
8782

8883
import datetime
8984
@app.before_request

‎app/config/default.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ support_email_contact: "support@bereacollege.onmicrosoft.com"
77
show_queries: True
88
test_entry: "Default"
99

10+
lsf_url: "USE local-override.yml"
11+
1012
db:
1113
name: "celts"
1214
host: "db"

‎app/controllers/admin/routes.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def eventDisplay(eventId):
373373

374374
currentEventRsvpAmount = getEventRsvpCount(event.id)
375375

376-
userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term)
376+
userParticipatedTrainingEvents = getParticipationStatusForTrainings(eventData['program'], [g.current_user], g.current_term, includeFutureEvents = False)
377377

378378
return render_template("events/eventView.html",
379379
eventData=eventData,

‎app/controllers/main/routes.py‎

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from app.models.programManager import ProgramManager
2525
from app.models.backgroundCheck import BackgroundCheck
2626
from app.models.emergencyContact import EmergencyContact
27-
from app.models.eventParticipant import EventParticipant
2827
from app.models.courseInstructor import CourseInstructor
2928
from app.models.backgroundCheckType import BackgroundCheckType
3029

@@ -39,7 +38,7 @@
3938
from app.logic.landingPage import getManagerProgramDict, getActiveEventTab
4039
from app.logic.minor import toggleMinorInterest, declareMinorInterest, getCommunityEngagementByTerm, getEngagementTotal
4140
from app.logic.participants import hasGoneToTraining, unattendedRequiredEvents, getParticipationStatusForTrainings, checkUserRsvp, addPersonToEvent
42-
from app.logic.users import addUserInterest, isBannedFromEvent, removeUserInterest, banUser, unbanUser, isEligibleForProgram, getUserBGCheckHistory, addProfileNote, deleteProfileNote, updateDietInfo, trainedParticipants
41+
from app.logic.users import *
4342

4443
@main_bp.route('/logout', methods=['GET'])
4544
def redirectToLogout():
@@ -234,8 +233,8 @@ def viewUsersProfile(username):
234233
managersProgramDict = getManagerProgramDict(g.current_user)
235234
managersList = [id[1] for id in managersProgramDict.items()]
236235
totalSustainedEngagements = getEngagementTotal(getCommunityEngagementByTerm(volunteer))
237-
handbookOverdue = getHandbookStatus(volunteer)
238236

237+
handbookOverdue = getHandbookStatus(volunteer)
239238
training = hasGoneToTraining(g.current_user, g.current_term)
240239

241240
return render_template ("/main/userProfile.html",
@@ -390,22 +389,34 @@ def eventTravelForm(eventID):
390389
userList = userList,
391390
)
392391

393-
@main_bp.route('/profile/addNote', methods=['POST'])
392+
@main_bp.route("/profile/addNote", methods=["POST"])
394393
def addNote():
395-
"""
396-
This function adds a note to the user's profile.
397-
"""
398-
postData = request.form
399394
try:
400-
note = addProfileNote(postData["visibility"], postData["bonner"] == "yes", postData["noteTextbox"], postData["username"])
395+
noteData = getProfileNoteData(
396+
request.form,
397+
includeUsername=True,
398+
)
399+
addProfileNote(**noteData)
401400
flash("Successfully added profile note", "success")
402-
return redirect(url_for("main.viewUsersProfile", username=postData["username"]))
403-
except Exception as e:
404-
print("Error adding note", e)
401+
except Exception as error:
402+
print("Error adding profile note:", error)
405403
flash("Failed to add profile note", "danger")
406-
return "Failed to add profile note", 500
407-
408-
404+
return str(error), 500
405+
return "success"
406+
@main_bp.route("/<username>/editNote", methods=["POST"])
407+
def editProfileNote(username):
408+
try:
409+
noteData = getProfileNoteData( request.form, includeId=True, )
410+
profileNote = ProfileNote.get_by_id(noteData["profileNoteID"] )
411+
if (profileNote.user.username != username or (profileNote.note.createdBy != g.current_user and not g.current_user.isCeltsAdmin) ):
412+
abort(403)
413+
updateProfileNote(**noteData)
414+
flash("Successfully updated profile note", "success")
415+
except Exception as error:
416+
print("Error updating profile note:", error)
417+
flash("Failed to update profile note", "danger")
418+
return str(error), 500
419+
return "success"
409420
@main_bp.route('/<username>/deleteNote', methods=['POST'])
410421
def deleteNote(username):
411422
"""

‎app/controllers/minor/routes.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ def viewCceMinor(username):
2929
"""
3030
Load minor management page with community engagements and summer experience
3131
"""
32+
if not (g.current_user.isAdmin or g.current_user.username == username or g.current_user.isCeltsStudentStaff):
33+
return abort(403)
34+
3235
sustainedEngagementByTerm = getCommunityEngagementByTerm(username)
3336

3437
activeTab = request.args.get("tab", "sustainedCommunityEngagements")

‎app/logic/celtsLabor.py‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,16 @@ def refreshCeltsLaborRecords(laborDict):
9797
for positionTitle, termNames in value.items():
9898
for term in termNames:
9999
termTableMatch = Term.select()
100-
if term[0].isalpha():
100+
isAcademicYear = False
101+
102+
if term[0].isalpha(): # Fall, Spring, Summer
101103
termTableMatch = termTableMatch.where(Term.description == term)
102-
else:
104+
else: # e.g., 2025-2026
103105
termTableMatch = termTableMatch.where(Term.academicYear == term, Term.description % "Fall%")
106+
isAcademicYear = True
107+
104108
try:
105109
laborTerm = termTableMatch.get()
106-
isAcademicYear = not laborTerm.isSummer
107110
celtsLabor.append({"user": key,
108111
"positionTitle": positionTitle,
109112
"term": laborTerm,
@@ -117,14 +120,13 @@ def refreshCeltsLaborRecords(laborDict):
117120
def getCeltsLaborHistory(volunteer):
118121

119122
laborHistoryList = list(CeltsLabor.select(CeltsLabor.positionTitle,
123+
CeltsLabor.id,
124+
CeltsLabor.isAcademicYear,
120125
Term.description,
121126
Term.academicYear,
122127
Term.isSummer)
123128
.join(Term, on=(CeltsLabor.term == Term.id))
124-
.where(CeltsLabor.user == volunteer))
125-
126-
laborHistoryDict= {}
127-
for position in laborHistoryList:
128-
laborHistoryDict[position.positionTitle] = position.term.description if position.term.isSummer else position.term.academicYear
129+
.where(CeltsLabor.user == volunteer)
130+
.order_by(Term.termOrder.asc()))
129131

130-
return laborHistoryDict
132+
return [(p.positionTitle, f"AY {p.term.academicYear}" if p.isAcademicYear else p.term.description) for p in laborHistoryList]

‎app/logic/events.py‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -411,22 +411,32 @@ def getParticipatedEventsForUser(user):
411411
:return: A list of Event objects
412412
"""
413413

414-
# Does this handle labor only and/or includes labor events?
415-
participatedEvents = (Event.select(Event, Program.programName, Case(None, (
416-
((Event.allowsLabor | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"),
417-
((Event.allowsLabor | Event.isLaborOnly | Event.name.contains("Labor")), "Labor"),
418-
(Event.isService, "Volunteer")), "Attendee").alias("participatedType"))
414+
eventName = fn.LOWER(Event.name)
415+
checkIfLaborMeeting = eventName.contains("labor meeting")
416+
417+
participatedEvents = (Event.select(Event,
418+
Program.programName,
419+
Case(None,
420+
(
421+
((Event.allowsLabor | Event.name.contains("Labor")) & Event.isService, "Labor & Volunteer"),
422+
((Event.allowsLabor | Event.isLaborOnly | Event.name.contains("Labor")), "Labor"),
423+
(Event.isService, "Volunteer")
424+
),
425+
"Attendee").alias("participatedType"),
426+
EventParticipant.hoursEarned
427+
)
419428
.join(Program, JOIN.LEFT_OUTER).switch()
420429
.join(EventParticipant)
421430
.where(EventParticipant.user == user,
422431
Event.isAllVolunteerTraining == False, Event.deletionDate == None, Event.isCeltsTraining == False)
423432
.order_by(Event.startDate, Event.name))
424-
allVolunteer = (Event.select(Event, "", Value("Volunteer").alias("participatedType"))
433+
434+
allVolunteer = (Event.select(Event, "", Value("Volunteer").alias("participatedType"), Value(0).alias("hoursEarned"))
425435
.join(EventParticipant)
426436
.where(Event.isAllVolunteerTraining == True,
427437
EventParticipant.user == user))
428438
union = participatedEvents.union_all(allVolunteer)
429-
unionParticipationWithVolunteer = list(union.select_from(union.c.id, union.c.programName, union.c.startDate, union.c.name, union.c.participatedType).order_by(union.c.startDate, union.c.name).execute())
439+
unionParticipationWithVolunteer = list(union.select_from(union.c.id, union.c.isService, union.c.programName, union.c.startDate, union.c.name, union.c.participatedType, union.c.hoursEarned).order_by(union.c.startDate, union.c.name).execute())
430440
return unionParticipationWithVolunteer
431441

432442
def validateNewEventData(data):

‎app/logic/participants.py‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def getEventParticipants(event):
116116

117117
return [p for p in eventParticipants]
118118

119-
def getParticipationStatusForTrainings(program, userList, term, returnStr = True):
119+
def getParticipationStatusForTrainings(program, userList, term, includeFutureEvents = True):
120120
"""
121121
This function returns a dictionary of all trainings for a program and
122122
whether the current user participated in them.
@@ -130,7 +130,10 @@ def getParticipationStatusForTrainings(program, userList, term, returnStr = True
130130
.join(EventRsvp, JOIN.LEFT_OUTER).switch()
131131
.join(Term)
132132
.where(isRelevantTraining, (Event.isCanceled != True)).order_by(Event.startDate))
133-
133+
if not includeFutureEvents:
134+
programTrainings = programTrainings.where(
135+
(Event.startDate < datetime.now().date()) |
136+
((Event.startDate == datetime.now().date()) & (Event.timeStart <= datetime.now().time())))
134137
# Create a dictionary where the keys are trainings and values are a set of those who attended
135138
trainingData = defaultdict(set)
136139
for training in programTrainings:
@@ -151,7 +154,7 @@ def getParticipationStatusForTrainings(program, userList, term, returnStr = True
151154
for user in userList:
152155
if training.name not in userParticipationStatus[user.username] or user.username in attendeeList:
153156
userParticipationStatus[user.username][training.name] = [training, user.username in attendeeList]
154-
if returnStr:
157+
if includeFutureEvents:
155158
return {user.username: list(userParticipationStatus[user.username].values()) for user in userList}
156159
else:
157160
return {user: list(userParticipationStatus[user.username].values()) for user in userList}
@@ -178,7 +181,7 @@ def getTrainingsForInterestedParticipants(programID, interestedUsers):
178181
179182
Gracefully handles multiple trainings of the same type (e.g., two All Volunteers Trainings)
180183
"""
181-
trainedUsers = getParticipationStatusForTrainings(programID, interestedUsers, g.current_term, returnStr = False)
184+
trainedUsers = getParticipationStatusForTrainings(programID, interestedUsers, g.current_term, includeFutureEvents = False)
182185
now = datetime.now()
183186
bannedUsers = list(User
184187
.select(User.username)
@@ -207,7 +210,7 @@ def getTrainingsForInterestedParticipants(programID, interestedUsers):
207210
for event in trainedUsers[interestedUser]:
208211
if not event[1]: # they didn't attend this training
209212
continue
210-
elif event[0].isAllVolunteerTraining: # They attended AVT
213+
elif event[0].isAllVolunteerTraining or event[0].isCeltsTraining: # They attended AVT or ACT
211214
trainedAndInterested[interestedUser.username]["allVolunteer"] = True
212215
elif event[0].isTraining: # They attended the Program-specific training
213216
trainedAndInterested[interestedUser.username]["programSpecific"] = True
@@ -241,7 +244,10 @@ def getParticipantsForProgramForAY(program, academicYear):
241244
.where(Program.id == program,
242245
Term.academicYear == academicYear,
243246
User.hasGraduated == False,
244-
EventParticipant.hoursEarned > 0)
247+
EventParticipant.hoursEarned > 0,
248+
# Only include currently enrolled undergraduate students.
249+
# This prevents alumni and past participants from appearing in the older reports.
250+
User.rawClassLevel.in_(["Freshman", "Sophomore", "Junior", "Senior"]))
245251
.distinct()
246252
)
247253
return participants

‎app/logic/searchUsers.py‎

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from peewee import fn
12
from playhouse.shortcuts import model_to_dict
23
from app.models.user import User
34
def searchUsers(query, category=None):
@@ -6,15 +7,19 @@ def searchUsers(query, category=None):
67
78
MySQL LIKE is case insensitive
89
'''
9-
# add wildcards to each piece of the query
1010
splitSearch = query.strip().split()
11-
firstName = splitSearch[0] + "%"
12-
lastName = " ".join(splitSearch[1:]) +"%"
13-
14-
if len(splitSearch) == 1: # search for query in first OR last name
15-
searchWhere = (User.firstName ** firstName | User.lastName ** firstName | User.username ** splitSearch)
16-
else: # search for first AND last name
17-
searchWhere = (User.firstName ** firstName & User.lastName ** lastName)
11+
if not splitSearch:
12+
return User.select().where(False)
13+
searchWhere = None
14+
for namePart in splitSearch:
15+
nameSearch = namePart + "%"
16+
# This individual search term can match the user's first name, last name, or username.
17+
namePartWhere = (User.firstName.contains(namePart) | User.lastName.contains(namePart) | User.username.contains(namePart))
18+
# For the first search term, initialize the WHERE condition.
19+
if searchWhere is None:
20+
searchWhere = namePartWhere
21+
else:
22+
searchWhere &= namePartWhere # Require every search term to match at least one of the first name, last name, or username fields.
1823

1924
if category == "instructor":
2025
userWhere = (User.isFaculty | User.isStaff)
@@ -26,12 +31,21 @@ def searchUsers(query, category=None):
2631
userWhere = (User.isCeltsOperationsTeam)
2732
elif category == "celtsLinkAdmin":
2833
userWhere = (User.isFaculty | User.isStaff | User.isCeltsStudentStaff | User.isCeltsOperationsTeam)
34+
elif category == "currentStudents":
35+
userWhere = (User.rawClassLevel.in_(["Freshman", "Sophomore", "Junior", "Senior"]))
2936
elif category == "all":
3037
userWhere = (True)
3138
else:
3239
userWhere = (User.isStudent)
3340

41+
fullSearchText = " ".join(splitSearch)
3442
# Combine into query
35-
searchResults = User.select().where(searchWhere, userWhere)
43+
searchResults = User.select().where(searchWhere, userWhere).order_by(
44+
fn.CONCAT(User.firstName, " ", User.lastName).contains(fullSearchText).desc(),
45+
User.firstName.startswith(fullSearchText).desc(),
46+
User.lastName.startswith(fullSearchText).desc(),
47+
User.lastName,
48+
User.firstName
49+
)
3650

3751
return { user.username : model_to_dict(user) for user in searchResults }

‎app/logic/users.py‎

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def getProgramInterest(program):
9191
Parameters:
9292
program: Program object
9393
"""
94-
return User.select().join(Interest).where(Interest.program == program)
94+
return User.select().join(Interest).where(Interest.program == program, User.rawClassLevel.in_(["Freshman", "Sophomore", "Junior", "Senior"]))
9595

9696
def getBannedUsers(program):
9797
"""
@@ -196,20 +196,48 @@ def getUserBGCheckHistory(username):
196196
for row in allBackgroundChecks:
197197
bgHistory[row.type_id].append(row)
198198
return bgHistory
199-
200-
def addProfileNote(visibility, bonner, noteTextbox, username):
199+
def getProfileNoteData(formData, includeUsername=False, includeId=False):
200+
noteData = {
201+
"visibility": int(formData.get("visibility", 1)),
202+
"bonner": formData.get("bonner") == "yes",
203+
"cceMinor": formData.get("cceMinor") == "yes",
204+
"noteTextbox": formData.get("noteTextbox", "").strip(),
205+
}
206+
if not noteData["noteTextbox"]:
207+
raise ValueError("Note cannot be empty")
208+
if includeUsername:
209+
noteData["username"] = formData.get("username")
210+
if not noteData["username"]:
211+
raise ValueError("Missing username")
212+
if includeId:
213+
noteData["profileNoteID"] = formData.get("id")
214+
if not noteData["profileNoteID"]:
215+
raise ValueError("Missing profile note ID")
216+
return noteData
217+
218+
def addProfileNote(visibility, bonner, cceMinor, noteTextbox, username):
219+
user = User.get(User.username == username)
220+
visibility = int(visibility)
201221
if bonner:
202-
visibility = 1 # bonner notes are always admins and the student
222+
visibility = 1
203223

204-
noteForDb = Note.create(createdBy = g.current_user,
205-
createdOn = datetime.datetime.now(),
206-
noteContent = noteTextbox,
207-
noteType = "profile")
208-
createProfileNote = ProfileNote.create(user = User.get(User.username == username),
209-
note = noteForDb,
210-
isBonnerNote = bonner,
211-
viewTier = visibility)
212-
return createProfileNote
224+
noteForDb = Note.create( createdBy=g.current_user, createdOn=datetime.datetime.now(), noteContent=noteTextbox, noteType="profile" )
225+
profileNote = ProfileNote.create( user=user, note=noteForDb, isBonnerNote=bonner, isCCEMinorNote=cceMinor, viewTier=visibility, )
226+
return profileNote
227+
228+
def updateProfileNote( profileNoteID, visibility, bonner, cceMinor, noteTextbox):
229+
profileNote = ProfileNote.get_by_id(profileNoteID)
230+
visibility = int(visibility)
231+
if bonner:
232+
visibility = 1
233+
note = profileNote.note
234+
note.noteContent = noteTextbox
235+
note.save()
236+
profileNote.viewTier = visibility
237+
profileNote.isBonnerNote = bonner
238+
profileNote.isCCEMinorNote = cceMinor
239+
profileNote.save()
240+
return profileNote
213241

214242
def deleteProfileNote(noteId):
215243
return ProfileNote.delete().where(ProfileNote.id == noteId).execute()

0 commit comments

Comments
 (0)