Skip to content

Commit 908fd61

Browse files
committed
Show lecture attendance for fall lectures
It's unclear if the fall lecture series will continue into the future, but so long as it *does* exist, it can be helpful to show which people have been recorded as having completed lectures for the *next* winter school year.
1 parent 24d3d70 commit 908fd61

File tree

6 files changed

+95
-36
lines changed

6 files changed

+95
-36
lines changed

ws/models.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,14 @@ def from_user(
491491
return None
492492

493493
def attended_lectures(self, year: int) -> bool:
494-
return self.lectureattendance_set.filter(year=year).exists()
494+
# Special case: in 2024 and 2025 (and *maybe* into the future?),
495+
# we ran special lectures in November and December for next year's winter.
496+
return any(
497+
attendance.year >= year
498+
for attendance in
499+
# Use `.all()` to make prefetching work
500+
self.lectureattendance_set.all()
501+
)
495502

496503
def missed_lectures(self, year: int) -> bool:
497504
"""Whether the participant missed WS lectures in the given year."""
@@ -547,7 +554,7 @@ def _cannot_attend_because_missed_lectures(self, trip: "Trip") -> bool:
547554
the current year's lectures in any UI that surfaces that information).
548555
"""
549556
if not self.missed_lectures_for(trip):
550-
return False # Attended this year
557+
return False # Attended this year *or* it's just not a winter trip.
551558

552559
# For Winter School leaders, we have a carve-out if you've attended lectures recently
553560
if not self.can_lead(enums.Program.WINTER_SCHOOL):

ws/templates/for_templatetags/lecture_attendance.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ <h3>Lecture Attendance</h3>
99
{% else %}
1010
{{ participant.name }} has
1111
{% endif %}
12-
attended this year's lectures!
12+
attended WS {{ lecture_year }} lectures!
1313
</p>
1414
{% elif can_set_attendance %} {# User (or chair) can mark attendance. #}
1515
<form method="post" action="{% url 'lecture_attendance' %}">
@@ -30,8 +30,8 @@ <h3>Lecture Attendance</h3>
3030
{% if past_attendance %}
3131
<p>
3232
Past years' attendance:
33-
{% for record in past_attendance %}
34-
<span class="label label-success"><i class="fas fa-fw fa-check"></i><span>{{ record.year }}</span></span>
33+
{% for year in past_attendance %}
34+
<span class="label label-success"><i class="fas fa-fw fa-check"></i><span>{{ year }}</span></span>
3535
{% endfor %}
3636
</p>
3737
{% endif %}
@@ -64,8 +64,8 @@ <h3>Lecture Attendance</h3>
6464
{% if past_attendance %}
6565
<p>
6666
Past years' attendance:
67-
{% for record in past_attendance %}
68-
<span class="label label-success"><i class="fas fa-fw fa-check"></i>{{ record.year }}</span>
67+
{% for year in past_attendance %}
68+
<span class="label label-success"><i class="fas fa-fw fa-check"></i>{{ year }}</span>
6969
{% endfor %}
7070
</p>
7171
{% endif %}

ws/templatetags/ws_tags.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,19 @@ def lecture_attendance(
2323
- The activity chair marks them manually as having attended
2424
- The user marks themselves during the allowed window
2525
"""
26-
attendance = participant.lectureattendance_set
26+
years = sorted(
27+
attendance.year for attendance in participant.lectureattendance_set.all()
28+
)
29+
max_year = years[-1] if years else None
2730
this_year = ws_year()
2831
form = AttendedLecturesForm(initial={"participant": participant})
2932
form.fields["participant"].widget = HiddenInput() # Will be checked by view
3033
return {
3134
"form": form,
3235
"participant": participant,
3336
"user_viewing": user_viewing,
34-
"attended_lectures": attendance.filter(year=this_year).exists(),
35-
"past_attendance": attendance.exclude(year=this_year),
37+
"attended_lectures": max_year and max_year >= this_year,
38+
"lecture_year": max_year,
39+
"past_attendance": [year for year in years if year < this_year],
3640
"can_set_attendance": can_set_attendance,
3741
}

ws/tests/templatetags/test_ws_tags.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ def test_leader_viewing_other_participant_has_attended(self):
110110
self.assertTrue(soup.find(string="Attended", class_="label-success"))
111111
self.assertFalse(soup.find("form"))
112112

113+
@freeze_time("Nov 28 2025 20:30:00 EST")
114+
def test_next_year_lectures_recorded(self) -> None:
115+
participant = factories.ParticipantFactory.create()
116+
factories.LectureAttendanceFactory.create(participant=participant, year=2026)
117+
html_template = Template(
118+
"{% load ws_tags %}{% lecture_attendance par user_viewing can_set %}"
119+
)
120+
context = Context({"par": participant, "user_viewing": False, "can_set": False})
121+
soup = BeautifulSoup(html_template.render(context), "html.parser")
122+
123+
paragraph = soup.find("p")
124+
assert paragraph is not None
125+
self.assertEqual(
126+
strip_whitespace(paragraph.text),
127+
"Attended Test Participant has attended WS 2026 lectures!",
128+
)
129+
113130
@freeze_time("Jan 12 2019 20:30:00 EST")
114131
def test_self_has_attended(self):
115132
"""We affirm that participants have attended lectures during the first week!"""
@@ -125,7 +142,7 @@ def test_self_has_attended(self):
125142
paragraph = BeautifulSoup(html, "html.parser").find("p")
126143
self.assertEqual(
127144
strip_whitespace(paragraph.text),
128-
"Attended You have attended this year's lectures!",
145+
"Attended You have attended WS 2019 lectures!",
129146
)
130147

131148
# We tell the participant they've attended (whether or not the form is open)

ws/tests/views/test_participant.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def test_success_is_shown_in_week_one(self):
9191
attendance = soup.find("h3", string="Lecture Attendance")
9292
self.assertEqual(
9393
strip_whitespace(attendance.find_next("p").text),
94-
"Attended You have attended this year's lectures!",
94+
"Attended You have attended WS 2022 lectures!",
9595
)
9696

9797
def test_warn_if_missing_after_lectures(self):
@@ -118,7 +118,7 @@ def test_warn_if_missing_after_lectures(self):
118118
attendance = soup.find("h3", string="Lecture Attendance")
119119
self.assertEqual(
120120
strip_whitespace(attendance.find_next("p").text),
121-
"Attended You have attended this year's lectures!",
121+
"Attended You have attended WS 2022 lectures!",
122122
)
123123

124124
def test_attendance_not_shown_in_week_two(self):

ws/views/participant.py

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def get_queryset(self) -> QuerySet[models.Participant]:
316316
"emergency_info__emergency_contact",
317317
"car",
318318
"lotteryinfo",
319-
)
319+
).prefetch_related("lectureattendance_set")
320320

321321
def get_trips(self) -> GroupedTrips:
322322
participant = self.object or self.get_object()
@@ -399,6 +399,50 @@ def count(
399399

400400
return stats
401401

402+
def _show_lecture_attendance(
403+
self,
404+
participant: models.Participant,
405+
*,
406+
attendance_years: set[int],
407+
user_viewing: bool,
408+
can_set_attendance: bool,
409+
) -> bool:
410+
"""Determine if we should render whether or not the participant attended lectures.
411+
412+
This is surprisingly complicated.
413+
414+
There are a few times of year where it's important to show "yes, you attended"
415+
1. The enrollment period where participants can record attendance (2nd lecture)
416+
2. The first week of WS, after lectures but before weekend trips
417+
(this is when participants may not have recorded attendance correctly)
418+
In later weeks, we'll enforce lecture attendance as part of trip signup.
419+
3. The participant was recorded *early* as having attended a year's lecture
420+
(applicable in 2024 and 2025, when special fall lectures were held)
421+
"""
422+
if not date_utils.is_currently_iap():
423+
# Outside of WS, there's only one time it's relevant to show attendance.
424+
# That's in the fall, when we've supported recording *next* year's attendance.
425+
# (we did this in 2024 and 2025, and may or may not do it going forward)
426+
current_year = date_utils.local_now().year
427+
return any(year > current_year for year in attendance_years)
428+
429+
show_attendance = can_set_attendance or date_utils.ws_lectures_complete()
430+
431+
# We don't need to tell participants "You attended lectures!" later in WS.
432+
# This is because signup rules enforce lecture attendance *after* week 1.
433+
if (
434+
show_attendance
435+
and user_viewing
436+
and models.Trip.objects.filter(
437+
program=enums.Program.WINTER_SCHOOL.value,
438+
trip_date__gte=date_utils.jan_1(),
439+
trip_date__lt=date_utils.local_date(),
440+
)
441+
):
442+
return False
443+
444+
return show_attendance
445+
402446
def _lecture_info(
403447
self,
404448
participant: models.Participant,
@@ -407,33 +451,20 @@ def _lecture_info(
407451
"""Describe the participant's lecture attendance, if applicable."""
408452
can_set_attendance = self.can_set_attendance(participant)
409453

410-
# There are only *two* times of year where it's important to show "yes, you attended"
411-
# 1. The enrollment period where participants can record attendance (2nd lecture)
412-
# 2. The first week of WS, after lectures but before weekend trips
413-
# (this is when participants may not have recorded attendance correctly)
414-
# In later weeks, we'll enforce lecture attendance as part of trip signup.
415-
show_attendance = date_utils.is_currently_iap() and (
416-
can_set_attendance or date_utils.ws_lectures_complete()
454+
attendance_years = {
455+
attendance.year for attendance in participant.lectureattendance_set.all()
456+
}
457+
show_attendance = self._show_lecture_attendance(
458+
participant,
459+
user_viewing=user_viewing,
460+
attendance_years=attendance_years,
461+
can_set_attendance=can_set_attendance,
417462
)
418463

419-
if show_attendance:
420-
attended_lectures = participant.attended_lectures(date_utils.ws_year())
421-
422-
# We don't need to tell participants "You attended lectures!" later in WS.
423-
# This is because signup rules enforce lecture attendance *after* week 1.
424-
if user_viewing and models.Trip.objects.filter(
425-
program=enums.Program.WINTER_SCHOOL.value,
426-
trip_date__gte=date_utils.jan_1(),
427-
trip_date__lt=date_utils.local_date(),
428-
):
429-
show_attendance = False
430-
else: # Skip unnecessary db queries
431-
attended_lectures = False # Maybe they actually did, but we're not showing.
432-
433464
return {
434465
"can_set_attendance": can_set_attendance,
435466
"show_attendance": show_attendance,
436-
"attended_lectures": attended_lectures,
467+
"attended_lectures": participant.attended_lectures(date_utils.ws_year()),
437468
}
438469

439470
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:

0 commit comments

Comments
 (0)