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:
@@ -221,7 +221,16 @@ class HomeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
deadline = hero_attendance.event.deadline
|
||||
rsvp_closed = deadline is not None and deadline < now
|
||||
|
||||
needs_answer_qs = upcoming.filter(status__in=[Attendance.AttendanceStatus.NO_RESPONSE, Attendance.AttendanceStatus.MAYBE]).order_by("event__start")
|
||||
# Deadline already passed -> replying is no longer possible (see
|
||||
# EventDetailView.post's own deadline check), so it doesn't belong
|
||||
# in a "still needs a reply" list -- unlike hero_attendance above,
|
||||
# which always shows the true next event regardless of RSVP state
|
||||
# and falls back to a read-only pill once its own deadline closes.
|
||||
needs_answer_qs = (
|
||||
upcoming.filter(status__in=[Attendance.AttendanceStatus.NO_RESPONSE, Attendance.AttendanceStatus.MAYBE])
|
||||
.filter(Q(event__deadline__isnull=True) | Q(event__deadline__gte=now))
|
||||
.order_by("event__start")
|
||||
)
|
||||
if hero_attendance is not None:
|
||||
needs_answer_qs = needs_answer_qs.exclude(pk=hero_attendance.pk)
|
||||
needs_answer_total = needs_answer_qs.count()
|
||||
@@ -444,14 +453,30 @@ class EventDetailView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
if event.deadline is not None and event.deadline < timezone.now():
|
||||
return HttpResponseBadRequest(_("Replies are closed for this event."))
|
||||
|
||||
# A reason is only ever meaningful attached to "Out" -- clearing it the
|
||||
# moment someone flips back to In/Maybe avoids a stale "sick" note
|
||||
# hanging around under an answer it no longer explains. Private by
|
||||
# A reason is only ever meaningful attached to Out/Maybe -- clearing it
|
||||
# the moment someone flips to In avoids a stale "sick" note hanging
|
||||
# around under an answer it no longer explains. Private by
|
||||
# construction, not by a visibility flag: nothing renders another
|
||||
# member's own note anywhere -- only this member/family's own screens
|
||||
# (event_detail's "Your answers") and Coach mode's bench attendance
|
||||
# (mobile/templates/mobile/coach/attendance.html) ever read it.
|
||||
note = request.POST.get("note", "").strip() if status == Attendance.AttendanceStatus.ABSENT else ""
|
||||
note = ""
|
||||
if status == Attendance.AttendanceStatus.ABSENT:
|
||||
note = request.POST.get("note", "").strip()
|
||||
# Rejects blank and punctuation-only "answers" (a bare ".", "-",
|
||||
# "??") -- mandatory for Out specifically, unlike Maybe below.
|
||||
# Backend-scoped, not the pretty inline-error UX this codebase
|
||||
# gives ModelForm submissions elsewhere -- matches this view's
|
||||
# own existing style (see "Unknown RSVP status"/"Replies are
|
||||
# closed" above, both plain 400s a normal user should never
|
||||
# actually see, since the template only ever offers Out through
|
||||
# the reason form to begin with).
|
||||
if not any(char.isalnum() for char in note):
|
||||
return HttpResponseBadRequest(_("Please enter a reason."))
|
||||
elif status == Attendance.AttendanceStatus.MAYBE:
|
||||
# Optional here -- Maybe doesn't owe anyone an explanation the way
|
||||
# a firm no does, but the same field carries it if given one.
|
||||
note = request.POST.get("note", "").strip()
|
||||
Attendance.objects.update_or_create(event=event, member=member, defaults={"status": status, "note": note})
|
||||
|
||||
if request.POST.get("next") == "event_detail":
|
||||
|
||||
Reference in New Issue
Block a user