Fix RSVP buttons broken by hx-boost; require a real Out reason; add optional Maybe reason
The In/Out hero buttons (and Out's reason confirm) stopped working: Alpine owns the toggle between a row's two sibling forms (buttons vs. the reason prompt), and hx-boost="true" on <body> had htmx *also* intercepting the same submit -- both ended up fighting over it. Fixed by marking every write-action <form> across the mobile app hx-boost="false" (link navigation, where the smooth-navigation feature actually matters, is untouched). The one exception worth calling out: coach/lineup.html's form uses three submit buttons sharing one <form> via formaction overrides -- htmx's boost reads the form's own action rather than the submitter's formaction override, so a boosted click there would always have posted to the wrong endpoint regardless of the Alpine conflict. Also: - A reason for Out is now mandatory, not just captured -- empty and punctuation-only "answers" (a bare ".", "-", "??") are rejected server-side (the authoritative check) with textarea required/minlength as a client-side nudge on top. - Maybe can now carry an optional reason too, visible to the same audience as Out's (this member/family, and Coach mode's bench attendance) -- one shared reason form in event_detail.html's per-person row, its hidden status input following whichever of Maybe/Out was tapped. - The 3-way In/Maybe/Out row (and the 2-way hero In/Out) now use min-w-0 on every button so flex-1 actually splits the row evenly -- a longer label's own intrinsic width was winning it a bigger share otherwise. - HomeView's "Needs your answer" list now excludes events whose registration deadline has already passed -- replying is no longer possible there (same rule EventDetailView.post already enforces), so it doesn't belong in a "still needs a reply" list. hero_attendance is unaffected -- it always shows the true next event, falling back to a read-only pill once its own deadline closes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -289,6 +289,24 @@ class HomeViewTests(TestCase):
|
||||
needs_answer_events = {attendance.event for attendance in response.context["needs_answer"]}
|
||||
self.assertEqual(needs_answer_events, {awaiting, maybe})
|
||||
|
||||
def test_needs_your_answer_excludes_events_with_a_closed_registration_deadline(self):
|
||||
# A distinct, already-answered earlier event so it becomes the hero --
|
||||
# otherwise the closed-deadline event below would become the hero
|
||||
# itself (still shown there, just read-only) rather than reaching
|
||||
# needs_answer's own exclusion at all.
|
||||
hero_event = self.make_event(title="Soonest", start=self.future)
|
||||
Attendance.objects.create(event=hero_event, member=self.member, status=Attendance.AttendanceStatus.PRESENT)
|
||||
closed = self.make_event(title="Deadline passed", start=self.future + datetime.timedelta(days=2), deadline=timezone.now() - datetime.timedelta(hours=1))
|
||||
open_deadline = self.make_event(title="Deadline still open", start=self.future + datetime.timedelta(days=3), deadline=timezone.now() + datetime.timedelta(hours=1))
|
||||
Attendance.objects.create(event=closed, member=self.member, status=Attendance.AttendanceStatus.NO_RESPONSE)
|
||||
Attendance.objects.create(event=open_deadline, member=self.member, status=Attendance.AttendanceStatus.NO_RESPONSE)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get("home")
|
||||
|
||||
needs_answer_events = {attendance.event for attendance in response.context["needs_answer"]}
|
||||
self.assertEqual(needs_answer_events, {open_deadline})
|
||||
|
||||
def test_needs_your_answer_is_capped_at_five_with_a_remaining_count(self):
|
||||
# A distinct, already-answered earlier event so it becomes the hero and
|
||||
# none of the seven "Practice N" events below get excluded as the hero.
|
||||
@@ -524,7 +542,7 @@ class EventDetailRsvpTests(TestCase):
|
||||
other_event = Event.objects.create(club=self.club, title="Away game", start=timezone.now() + datetime.timedelta(days=8))
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self._post(other_event, {"status": "absent"})
|
||||
self._post(other_event, {"status": "absent", "note": "Sick"})
|
||||
|
||||
self.assertEqual(Attendance.objects.get(event=other_event, member=self.member).status, Attendance.AttendanceStatus.ABSENT)
|
||||
|
||||
@@ -565,6 +583,25 @@ class EventDetailRsvpTests(TestCase):
|
||||
self.attendance.refresh_from_db()
|
||||
self.assertEqual(self.attendance.status, Attendance.AttendanceStatus.MAYBE)
|
||||
|
||||
def test_posting_maybe_without_a_reason_is_allowed(self):
|
||||
# Unlike Out, a reason is optional for Maybe -- no 400 without one.
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(self.event, {"status": "maybe"})
|
||||
|
||||
self.assertRedirects(response, reverse("mobile:home"), fetch_redirect_response=False)
|
||||
self.attendance.refresh_from_db()
|
||||
self.assertEqual(self.attendance.note, "")
|
||||
|
||||
def test_posting_maybe_with_a_reason_stores_the_note(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self._post(self.event, {"status": "maybe", "note": "Might have to leave early"})
|
||||
|
||||
self.attendance.refresh_from_db()
|
||||
self.assertEqual(self.attendance.status, Attendance.AttendanceStatus.MAYBE)
|
||||
self.assertEqual(self.attendance.note, "Might have to leave early")
|
||||
|
||||
def test_posting_absent_with_a_reason_stores_the_note(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
@@ -602,6 +639,31 @@ class EventDetailRsvpTests(TestCase):
|
||||
self.attendance.refresh_from_db()
|
||||
self.assertEqual(self.attendance.note, "")
|
||||
|
||||
def test_absent_without_a_reason_is_rejected(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(self.event, {"status": "absent"})
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.attendance.refresh_from_db()
|
||||
self.assertEqual(self.attendance.status, Attendance.AttendanceStatus.NO_RESPONSE)
|
||||
|
||||
def test_absent_with_a_whitespace_only_reason_is_rejected(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(self.event, {"status": "absent", "note": " \n "})
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_absent_with_a_punctuation_only_reason_is_rejected(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(self.event, {"status": "absent", "note": "..."})
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.attendance.refresh_from_db()
|
||||
self.assertEqual(self.attendance.status, Attendance.AttendanceStatus.NO_RESPONSE)
|
||||
|
||||
def test_rejects_an_unknown_status_value(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
@@ -2158,6 +2220,16 @@ class CoachAttendanceViewTests(TestCase):
|
||||
self.assertContains(response, "Anna Player")
|
||||
self.assertContains(response, "9")
|
||||
|
||||
def test_shows_a_maybe_reason_alongside_an_absent_one(self):
|
||||
self.attendance.status = Attendance.AttendanceStatus.MAYBE
|
||||
self.attendance.note = "Might be a few minutes late"
|
||||
self.attendance.save()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertContains(response, "Might be a few minutes late")
|
||||
|
||||
def test_save_records_check_ins_via_record_check_in(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user