Build M1 Home for the mobile app
Hero card for the soonest upcoming event with a quick In/Out RSVP, a "needs your answer" list, a season-dues card, and a news teaser -- all scoped to the person switcher's current selection. The RSVP quick action posts to the (still placeholder-GET) event-detail URL that a later screen (M2) will build out fully. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
107
mobile/views.py
107
mobile/views.py
@@ -12,15 +12,24 @@ before every screen is built out one at a time.
|
||||
import json
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from club.models import ClubMembership
|
||||
from club.services.access import current_season
|
||||
from club.services.fees import remaining_balance
|
||||
from events.models import Attendance, Event
|
||||
from members.models import Member
|
||||
from members.views import ClubScopedPublicMixin
|
||||
from news.models import News
|
||||
from teams.models import TeamMembership
|
||||
|
||||
from .mixins import PersonScopeMixin
|
||||
from .models import PushSubscription
|
||||
@@ -121,10 +130,88 @@ class _PlaceholderScreen(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
return super().get_context_data(screen_title=self.screen_title, active_tab=self.active_tab, **kwargs)
|
||||
|
||||
|
||||
class HomeView(_PlaceholderScreen):
|
||||
class HomeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
"""M1 -- design_handoff_rosterchief_platform/README.md's M1 section: a
|
||||
hero card for scope_person's soonest upcoming event (with a quick In/Out
|
||||
RSVP -- see EventDetailView.post below), a "needs your answer" list of
|
||||
upcoming events still NO_RESPONSE/MAYBE, a season-dues card when money is
|
||||
owed, and a news teaser. Every card is independently optional -- an
|
||||
empty-state screen is just the four `if`s below all being falsy.
|
||||
"""
|
||||
|
||||
template_name = "mobile/home.html"
|
||||
screen_title = _("Home")
|
||||
active_tab = "home"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
scope_person = self.scope_person
|
||||
now = timezone.now()
|
||||
|
||||
hero_attendance = None
|
||||
rsvp_closed = False
|
||||
needs_answer = []
|
||||
dues_membership = None
|
||||
dues_balance = None
|
||||
dues_invoice = None
|
||||
news_item = None
|
||||
|
||||
if scope_person is not None:
|
||||
upcoming = Attendance.objects.filter(
|
||||
member=scope_person,
|
||||
event__club=self.request.club,
|
||||
event__cancelled=False,
|
||||
event__start__gte=now,
|
||||
).select_related("event", "event__location")
|
||||
|
||||
hero_attendance = upcoming.order_by("event__start").first()
|
||||
if hero_attendance is not None:
|
||||
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")
|
||||
if hero_attendance is not None:
|
||||
needs_answer_qs = needs_answer_qs.exclude(pk=hero_attendance.pk)
|
||||
needs_answer = list(needs_answer_qs)
|
||||
|
||||
season = current_season(self.request.club)
|
||||
if season is not None:
|
||||
membership = (
|
||||
ClubMembership.objects.filter(club=self.request.club, member=scope_person, season=season)
|
||||
.exclude(fee_status=ClubMembership.FeeStatus.WAIVED)
|
||||
.select_related("dues_invoice")
|
||||
.first()
|
||||
)
|
||||
if membership is not None:
|
||||
balance = remaining_balance(membership)
|
||||
if balance > 0:
|
||||
dues_membership = membership
|
||||
dues_balance = balance
|
||||
dues_invoice = getattr(membership, "dues_invoice", None)
|
||||
|
||||
team_ids = list(TeamMembership.objects.filter(member=scope_person, season=season).values_list("team_id", flat=True))
|
||||
else:
|
||||
team_ids = []
|
||||
|
||||
news_item = (
|
||||
News.objects.filter(club=self.request.club, status=News.Status.PUBLISHED, published_at__lte=now)
|
||||
.filter(Q(teams__isnull=True) | Q(teams__id__in=team_ids))
|
||||
.order_by("-published_at")
|
||||
.distinct()
|
||||
.first()
|
||||
)
|
||||
|
||||
return super().get_context_data(
|
||||
hero_attendance=hero_attendance,
|
||||
rsvp_closed=rsvp_closed,
|
||||
needs_answer=needs_answer,
|
||||
dues_membership=dues_membership,
|
||||
dues_balance=dues_balance,
|
||||
dues_invoice=dues_invoice,
|
||||
news_item=news_item,
|
||||
news_team=news_item.teams.first() if news_item is not None else None,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class CalendarView(_PlaceholderScreen):
|
||||
screen_title = _("Calendar")
|
||||
@@ -132,9 +219,27 @@ class CalendarView(_PlaceholderScreen):
|
||||
|
||||
|
||||
class EventDetailView(_PlaceholderScreen):
|
||||
"""GET is still the M2 placeholder (a later screen owns the full detail
|
||||
page); POST is M1's quick In/Out RSVP action, reused by any future screen
|
||||
that posts the same {status, member_id} shape at this URL."""
|
||||
|
||||
screen_title = _("Event")
|
||||
active_tab = "calendar"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
status = request.POST.get("status")
|
||||
if status not in (Attendance.AttendanceStatus.PRESENT, Attendance.AttendanceStatus.ABSENT):
|
||||
return HttpResponseBadRequest(_("Unknown RSVP status."))
|
||||
|
||||
member_id = request.POST.get("member_id") or (str(self.scope_person.pk) if self.scope_person else None)
|
||||
member = next((person for person in self.managed_people if str(person.pk) == member_id), None) if member_id else None
|
||||
if member is None:
|
||||
return HttpResponseBadRequest(_("You can't RSVP for that person."))
|
||||
|
||||
event = get_object_or_404(Event, pk=kwargs["pk"], club=request.club)
|
||||
Attendance.objects.update_or_create(event=event, member=member, defaults={"status": status})
|
||||
return HttpResponseRedirect(reverse("mobile:home"))
|
||||
|
||||
|
||||
class NewsDetailView(_PlaceholderScreen):
|
||||
screen_title = _("News")
|
||||
|
||||
Reference in New Issue
Block a user