diff --git a/program/filters.py b/program/filters.py index c73e507f3f60b4edb62898df08faf0fc0aabc55a..8458ebf805d64f8d9adbd0605e45d7c0c2cb70b0 100644 --- a/program/filters.py +++ b/program/filters.py @@ -1,5 +1,8 @@ +import datetime + from django_filters import rest_framework as filters +from django import forms from django.contrib.auth.models import User from django.db.models import Q, QuerySet from django.utils import timezone @@ -90,3 +93,88 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): "topic", "type", ] + + +class TimeSlotFilterSet(filters.FilterSet): + order = filters.OrderingFilter( + fields=[field.name for field in models.TimeSlot._meta.get_fields()] + ) + surrounding = filters.BooleanFilter( + method="filter_surrounding", + label="Return surrounding timeslots", + help_text=( + "Returns the 10 nearest timeslots for the current date if set to true. " + "No filtering is performed if set to false. " + "If specified without a value true is assumed." + ), + ) + # The start/end filters will always be applied even if no query parameter has been set. + # This is because we enforce a value in the clean_start and clean_end methods + # of the filterset form. + start = filters.DateFilter( + method="filter_start", + help_text=( + "Only returns timeslots after that start on or after the specified date. " + "By default, this is set to the current date." + ), + ) + end = filters.DateFilter( + method="filter_end", + help_text=( + "Only returns timeslots that end on or before the specified date. " + "By default, this is set to value of the start filter + 60 days." + ), + ) + + def filter_surrounding(self, queryset: QuerySet, name: str, value: bool): + if value is not True: + return queryset + start = self.form.cleaned_data.get("start", None) or timezone.now() + nearest_timeslots_in_future = ( + models.TimeSlot.objects.filter(start__gte=start) + .order_by("start") + .values_list("id", flat=True)[:5] + ) + nearest_timeslots_in_past = ( + models.TimeSlot.objects.filter(start__lt=start) + .order_by("-start") + .values_list("id", flat=True)[:5] + ) + relevant_timeslot_ids = list(nearest_timeslots_in_future) + list( + nearest_timeslots_in_past + ) + return queryset.filter(id__in=relevant_timeslot_ids) + + def filter_start(self, queryset: QuerySet, name: str, value: datetime.date): + start = timezone.make_aware(datetime.datetime.combine(value, datetime.time.min)) + return queryset.filter(start__gte=start) + + def filter_end(self, queryset: QuerySet, name: str, value: datetime.date): + end = timezone.make_aware(datetime.datetime.combine(value, datetime.time.max)) + return queryset.filter(end__lte=end) + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + # This is for backwards compatibility as the surrounding-filter was formerly implemented + # by just checking for the existence of the query parameter. + if self.request.GET.get("surrounding", None) == "": + queryset = self.filter_surrounding(queryset, "surrounding", True) + return queryset + + class Meta: + model = models.TimeSlot + fields = [ + "order", + "start", + "end", + "surrounding", + ] + + class form(forms.Form): + def clean_start(self): + start = self.cleaned_data.get("start", None) + return start or timezone.now().date() + + def clean_end(self): + end = self.cleaned_data.get("end", None) + return end or self.cleaned_data["start"] + datetime.timedelta(days=60) diff --git a/program/views.py b/program/views.py index 3a958cfb192945ff2591989deffe2f65fce27b90..9b32e1de24395a1be9420ecacb738f84389fc192 100644 --- a/program/views.py +++ b/program/views.py @@ -20,7 +20,7 @@ import json import logging -from datetime import date, datetime, time, timedelta +from datetime import date, datetime, time from rest_framework import permissions, status, viewsets from rest_framework.pagination import LimitOffsetPagination @@ -496,110 +496,29 @@ class APIScheduleViewSet(viewsets.ModelViewSet): class APITimeSlotViewSet(viewsets.ModelViewSet): """ - /timeslots returns timeslots of the next 60 days (GET). Timeslots may only be added by - creating/updating a schedule - /timeslots/{pk} eturns the given timeslot (GET) - /timeslots/?start={start_date}&end={end_date} returns timeslots within the time range (GET) - /shows/{show_pk}/timeslots returns timeslots of the show (GET, POST) - /shows/{show_pk}/timeslots?surrounding returns the 10 nearest timeslots for the current date - (GET) - /shows/{show_pk}/timeslots/{pk} returns a timeslots by its ID (GET, PUT, DELETE) - /shows/{show_pk}/timeslots/?start={start_date}&end={end_date} returns timeslots of the show - within the time range - /shows/{show_pk}/schedules/{schedule_pk}/timeslots returns all timeslots of the schedule (GET, - POST) - /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{pk} returns a timeslot by its ID (GET, - DELETE). If PUT, the next repetition is returned or nothing if the next timeslot isn't one - /shows/{show_pk}/schedules/{schedule_pk}/timeslots?start={start_date}&end={end_date} returns - all timeslots of the schedule within the time range + Returns a list of timeslots. + + By default, only timeslots ranging from now + 60 days will be displayed. + You may override this default overriding start and/or end parameter. + + Timeslots may only be added by creating/updating a schedule. """ permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] serializer_class = TimeSlotSerializer pagination_class = LimitOffsetPagination - queryset = TimeSlot.objects.none() + queryset = TimeSlot.objects.order_by("-start") + filterset_class = filters.TimeSlotFilterSet def get_queryset(self): + queryset = super().get_queryset() + # subroute filters show_pk, schedule_pk = get_values(self.kwargs, "show_pk", "schedule_pk") - # Filters - - # Return next 60 days by default - start = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0))) - end = start + timedelta(days=60) - - if ("start" in self.request.query_params) and ( - "end" in self.request.query_params - ): - start = timezone.make_aware( - datetime.combine( - parse_date(self.request.query_params.get("start")), time(0, 0) - ) - ) - end = timezone.make_aware( - datetime.combine( - parse_date(self.request.query_params.get("end")), time(23, 59) - ) - ) - - default_order = "-start" - order = self.request.query_params.get("order", default_order) - - # If someone tries to sort by a field that isn't available on the model - # we silently ignore that and use the default sort order. - model_fields = [field.name for field in TimeSlot._meta.get_fields()] - if order.lstrip("-") not in model_fields: - order = default_order - - if "surrounding" in self.request.query_params: - now = timezone.now() - - nearest_timeslots_in_future = ( - TimeSlot.objects.filter(start__gte=now) - .order_by("start") - .values_list("id", flat=True)[:5] - ) - nearest_timeslots_in_past = ( - TimeSlot.objects.filter(start__lt=now) - .order_by("-start") - .values_list("id", flat=True)[:5] - ) - relevant_timeslot_ids = list(nearest_timeslots_in_future) + list( - nearest_timeslots_in_past - ) - - return TimeSlot.objects.filter(id__in=relevant_timeslot_ids).order_by(order) - - # Endpoints - - # - # /shows/1/schedules/1/timeslots/ - # - # Returns timeslots of the given show and schedule - # - if show_pk and schedule_pk: - return TimeSlot.objects.filter( - show=show_pk, schedule=schedule_pk, start__gte=start, end__lte=end - ).order_by(order) - - # - # /shows/1/timeslots/ - # - # Returns timeslots of the show - # - elif show_pk and schedule_pk is None: - return TimeSlot.objects.filter( - show=show_pk, start__gte=start, end__lte=end - ).order_by(order) - - # - # /timeslots/ - # - # Returns all timeslots - # - else: - return TimeSlot.objects.filter(start__gte=start, end__lte=end).order_by( - order - ) + if show_pk: + queryset = queryset.filter(show=show_pk) + if schedule_pk: + queryset = queryset.filter(schedule=schedule_pk) + return queryset def retrieve(self, request, *args, **kwargs): pk, show_pk = get_values(self.kwargs, "pk", "show_pk")