"""Views for the pretalx integration app.
Provides read-only schedule, talk, and speaker views scoped to a conference
via the ``conference_slug`` URL kwarg. All views resolve the conference from
the URL and return a 404 if the slug does not match.
"""
import itertools
from typing import TYPE_CHECKING
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.views import View
from django.views.generic import DetailView, ListView, TemplateView
from django_program.conference.models import Conference
from django_program.features import FeatureRequiredMixin
from django_program.pretalx.models import ScheduleSlot, Speaker, Talk
if TYPE_CHECKING:
from datetime import date
from django.db.models import QuerySet
[docs]
class ConferenceMixin:
"""Mixin that resolves the conference from the ``conference_slug`` URL kwarg.
Stores the conference on ``self.conference`` and adds it to the template
context. Returns a 404 if no conference matches the slug.
"""
conference: Conference
kwargs: dict[str, str]
[docs]
def get_conference(self) -> Conference:
"""Look up the conference by slug from the URL.
Returns:
The matched conference instance.
Raises:
Http404: If no conference matches the slug.
"""
return get_object_or_404(Conference, slug=self.kwargs["conference_slug"])
[docs]
def get_context_data(self, **kwargs: object) -> dict[str, object]:
"""Add the conference to the template context.
Args:
**kwargs: Additional context data.
Returns:
The template context dict with the conference included.
"""
context: dict[str, object] = super().get_context_data(**kwargs) # type: ignore[misc]
context["conference"] = self.conference
return context
[docs]
def dispatch(self, request: HttpRequest, *args: str, **kwargs: str) -> HttpResponse:
"""Resolve the conference before dispatching.
Args:
request: The incoming HTTP request.
*args: Positional arguments from the URL resolver.
**kwargs: Keyword arguments from the URL pattern.
Returns:
The HTTP response.
"""
self.conference = self.get_conference()
return super().dispatch(request, *args, **kwargs) # type: ignore[misc]
[docs]
class ScheduleView(ConferenceMixin, FeatureRequiredMixin, TemplateView):
"""Full schedule view grouped by day.
Renders the conference schedule with slots organized by date. Each day
is a ``(date, list[ScheduleSlot])`` tuple ordered by start time.
"""
required_feature = "public_ui"
template_name = "django_program/pretalx/schedule.html"
[docs]
def get_context_data(self, **kwargs: object) -> dict[str, object]:
"""Build schedule context grouped by day.
Returns:
Context dict containing ``conference`` and ``days``.
"""
context = super().get_context_data(**kwargs)
slots = (
ScheduleSlot.objects.filter(
conference=self.conference,
)
.select_related("talk", "room")
.order_by("start", "room__position", "room__name")
)
days: list[tuple[date, list[ScheduleSlot]]] = [
(day, list(day_slots)) for day, day_slots in itertools.groupby(slots, key=lambda s: s.start.date())
]
context["days"] = days
try:
conference_tz = ZoneInfo(self.conference.timezone)
except (ZoneInfoNotFoundError, ValueError): # fmt: skip
conference_tz = timezone.utc
context["today"] = timezone.localdate(timezone=conference_tz)
return context
[docs]
class ScheduleJSONView(ConferenceMixin, FeatureRequiredMixin, View):
"""JSON endpoint for schedule data.
Returns a JSON array of schedule slots suitable for embedding in
JavaScript schedule widgets. Each slot includes title, room, start/end
times, slot type, and the linked talk code when available.
"""
required_feature = "public_ui"
[docs]
def get(self, _request: HttpRequest, **_kwargs: str) -> JsonResponse:
"""Return schedule slots as a JSON array.
Args:
_request: The incoming HTTP request.
**_kwargs: URL keyword arguments (unused).
Returns:
A JSON response with the schedule data.
"""
slots = (
ScheduleSlot.objects.filter(
conference=self.conference,
)
.select_related("talk", "room")
.order_by("start", "room__position", "room__name")
)
data = [
{
"title": slot.display_title,
"room": slot.room.name if slot.room else "",
"start": slot.start.isoformat(),
"end": slot.end.isoformat(),
"slot_type": slot.slot_type,
"talk_code": slot.talk.pretalx_code if slot.talk else "",
}
for slot in slots
]
return JsonResponse(data, safe=False)
[docs]
class TalkDetailView(ConferenceMixin, FeatureRequiredMixin, DetailView):
"""Detail view for a single talk.
Looks up the talk by its Pretalx code within the conference scope.
Prefetches the speakers relation for display.
"""
required_feature = "public_ui"
template_name = "django_program/pretalx/talk_detail.html"
context_object_name = "talk"
[docs]
def get_object(self, queryset: QuerySet[Talk] | None = None) -> Talk: # noqa: ARG002
"""Look up the talk by conference and pretalx_code.
Returns:
The matched Talk instance.
Raises:
Http404: If no talk matches the conference and code.
"""
return get_object_or_404(
Talk.objects.prefetch_related("speakers"),
conference=self.conference,
pretalx_code=self.kwargs["pretalx_code"],
)
[docs]
def get_context_data(self, **kwargs: object) -> dict[str, object]:
"""Add speakers to the template context.
Returns:
Context dict containing ``conference``, ``talk``, and ``speakers``.
"""
context = super().get_context_data(**kwargs)
context["speakers"] = self.object.speakers.all()
return context
[docs]
class SpeakerListView(ConferenceMixin, FeatureRequiredMixin, ListView):
"""List view of all speakers for a conference, ordered by name."""
required_feature = "public_ui"
template_name = "django_program/pretalx/speaker_list.html"
context_object_name = "speakers"
[docs]
def get_queryset(self) -> QuerySet[Speaker]:
"""Return speakers for the current conference ordered by name.
Returns:
A queryset of Speaker instances.
"""
return Speaker.objects.filter(conference=self.conference).order_by("name")
[docs]
class SpeakerDetailView(ConferenceMixin, FeatureRequiredMixin, DetailView):
"""Detail view for a single speaker.
Looks up the speaker by their Pretalx code within the conference scope.
Prefetches talks for display.
"""
required_feature = "public_ui"
template_name = "django_program/pretalx/speaker_detail.html"
context_object_name = "speaker"
[docs]
def get_object(self, queryset: QuerySet[Speaker] | None = None) -> Speaker: # noqa: ARG002
"""Look up the speaker by conference and pretalx_code.
Returns:
The matched Speaker instance.
Raises:
Http404: If no speaker matches the conference and code.
"""
return get_object_or_404(
Speaker.objects.prefetch_related("talks"),
conference=self.conference,
pretalx_code=self.kwargs["pretalx_code"],
)
[docs]
def get_context_data(self, **kwargs: object) -> dict[str, object]:
"""Add talks to the template context.
Returns:
Context dict containing ``conference``, ``speaker``, and ``talks``.
"""
context = super().get_context_data(**kwargs)
context["talks"] = self.object.talks.all()
return context