Skip to content
Snippets Groups Projects
Commit 3507bbdb authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt :koala:
Browse files

refactor: move subroute filtering into viewset mixin

Most of the queryset filtering for subroutes formerly implemented
through custom get_queryset methods can happen semi-automated through a
mixin, making the behaviour re-usable and allowing us strip a lot of
additional logic for retrieving objects.

The use of the viewset get_object method also allows us to implement
object access with permission classes in the future, and should make a
whole lot of other code obselete.

This change is primarily motivated by the fact that documentation of the
API will become a lot easier if the subroutes don’t impose arbitrary
restrictions on the specific subpath under which an operation is
allowed. For instance updating a schedule should be possible through
either /schedules/<pk>/ or
/shows/<show_pk>/schedules/<schedule_pk>/ as long as all the necessary
data is present in the request body.
parent f7926bd9
No related branches found
No related tags found
1 merge request!21Add API documentation
......@@ -23,6 +23,7 @@ from datetime import date, datetime, time
from typing import Dict, Optional, Tuple, Union
import requests
from rest_framework import exceptions
from django.utils import timezone
from steering.settings import CBA_AJAX_URL, CBA_API_KEY, DEBUG
......@@ -126,3 +127,22 @@ class DisabledObjectPermissionCheckMixin:
def check_object_permissions(self, request, obj):
pass
class NestedObjectFinderMixin:
ROUTE_FILTER_LOOKUPS = {}
def _get_route_filters(self) -> Dict[str, int]:
filter_kwargs = {}
for key, value in self.kwargs.items():
if key in self.ROUTE_FILTER_LOOKUPS:
try:
filter_kwargs[self.ROUTE_FILTER_LOOKUPS[key]] = int(value)
except ValueError:
raise exceptions.ValidationError(
detail=f"{key} must map to an integer value.", code="invalid-pk"
)
return filter_kwargs
def get_queryset(self):
return super().get_queryset().filter(**self._get_route_filters())
......@@ -59,7 +59,12 @@ from program.serializers import (
TypeSerializer,
UserSerializer,
)
from program.utils import DisabledObjectPermissionCheckMixin, get_values, parse_date
from program.utils import (
DisabledObjectPermissionCheckMixin,
NestedObjectFinderMixin,
get_values,
parse_date,
)
logger = logging.getLogger(__name__)
......@@ -357,40 +362,25 @@ class APIShowViewSet(DisabledObjectPermissionCheckMixin, viewsets.ModelViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class APIScheduleViewSet(viewsets.ModelViewSet):
class APIScheduleViewSet(
DisabledObjectPermissionCheckMixin,
NestedObjectFinderMixin,
viewsets.ModelViewSet,
):
"""
Returns a list of schedules.
Only superusers may create and update schedules.
"""
ROUTE_FILTER_LOOKUPS = {
"show_pk": "show",
}
queryset = Schedule.objects.all()
serializer_class = ScheduleSerializer
permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
def get_queryset(self):
queryset = super().get_queryset()
# subroute filters
show_pk = get_values(self.kwargs, "show_pk")
if show_pk:
queryset = queryset.filter(show=show_pk)
return queryset
def retrieve(self, request, *args, **kwargs):
pk, show_pk = get_values(self.kwargs, "pk", "show_pk")
schedule = (
get_object_or_404(Schedule, pk=pk, show=show_pk)
if show_pk
else get_object_or_404(Schedule, pk=pk)
)
serializer = ScheduleSerializer(schedule)
return Response(serializer.data)
def create(self, request, *args, **kwargs):
"""
Create a schedule, generate timeslots, test for collisions and resolve them including notes
......@@ -438,18 +428,16 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
if not request.user.is_superuser:
return Response(status=status.HTTP_401_UNAUTHORIZED)
pk, show_pk = get_values(self.kwargs, "pk", "show_pk")
# Only allow updating when calling /shows/{show_pk}/schedules/{pk}/ and with the `schedule`
# JSON object
if show_pk is None or "schedule" not in request.data:
# Only allow updating when with the `schedule` JSON object
if "schedule" not in request.data:
return Response(status=status.HTTP_400_BAD_REQUEST)
schedule = self.get_object()
# If default playlist id or repetition are given, just update
if default_playlist_id := request.data.get("schedule").get(
"default_playlist_id"
):
schedule = get_object_or_404(Schedule, pk=pk, show=show_pk)
schedule.default_playlist_id = int(default_playlist_id)
schedule.save()
......@@ -457,7 +445,6 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
return Response(serializer.data)
if is_repetition := request.data.get("schedule").get("is_repetition"):
schedule = get_object_or_404(Schedule, pk=pk, show=show_pk)
schedule.is_repetition = bool(is_repetition)
schedule.save()
......@@ -468,11 +455,15 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
if "solutions" not in request.data:
# TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
return Response(
Schedule.make_conflicts(request.data["schedule"], pk, show_pk)
Schedule.make_conflicts(
request.data["schedule"], schedule.pk, schedule.show.pk
)
)
# Otherwise try to resolve
resolution = Schedule.resolve_conflicts(request.data, pk, show_pk)
resolution = Schedule.resolve_conflicts(
request.data, schedule.pk, schedule.show.pk
)
# If resolution went well
if "projected" not in resolution:
......@@ -492,13 +483,7 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
if not request.user.is_superuser:
return Response(status=status.HTTP_401_UNAUTHORIZED)
pk, show_pk = get_values(self.kwargs, "pk", "show_pk")
# Only allow deleting when calling /shows/{show_pk}/schedules/{pk}
if show_pk is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
Schedule.objects.get(pk=pk).delete()
self.get_object().delete()
return Response(status=status.HTTP_204_NO_CONTENT)
......@@ -507,6 +492,8 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
# by creating or updating a schedule.
# There might be a use case for adding a single timeslot without any conflicts though.
class APITimeSlotViewSet(
DisabledObjectPermissionCheckMixin,
NestedObjectFinderMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
......@@ -522,41 +509,21 @@ class APITimeSlotViewSet(
Timeslots may only be added by creating/updating a schedule.
"""
ROUTE_FILTER_LOOKUPS = {
"show_pk": "show",
"schedule_pk": "schedule",
}
permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
serializer_class = TimeSlotSerializer
pagination_class = LimitOffsetPagination
queryset = TimeSlot.objects.all().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")
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")
if show_pk:
timeslot = get_object_or_404(TimeSlot, pk=pk, show=show_pk)
else:
timeslot = get_object_or_404(TimeSlot, pk=pk)
serializer = TimeSlotSerializer(timeslot)
return Response(serializer.data)
def update(self, request, *args, **kwargs):
"""Link a playlist_id to a timeslot"""
pk, show_pk, schedule_pk = get_values(
self.kwargs, "pk", "show_pk", "schedule_pk"
)
show_pk = get_values(self.kwargs, "show_pk")
if (
not request.user.is_superuser
......@@ -564,15 +531,7 @@ class APITimeSlotViewSet(
):
return Response(status=status.HTTP_401_UNAUTHORIZED)
# Update is only allowed when calling /shows/1/schedules/1/timeslots/1 and if user owns the
# show
if schedule_pk is None or show_pk is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
timeslot = get_object_or_404(
TimeSlot, pk=pk, schedule=schedule_pk, show=show_pk
)
timeslot = self.get_object()
serializer = TimeSlotSerializer(timeslot, data=request.data)
if serializer.is_valid():
serializer.save()
......@@ -600,48 +559,38 @@ class APITimeSlotViewSet(
if not request.user.is_superuser:
return Response(status=status.HTTP_401_UNAUTHORIZED)
pk, show_pk = get_values(self.kwargs, "pk", "show_pk")
# Only allow when calling endpoint starting with /shows/1/...
if show_pk is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
TimeSlot.objects.get(pk=pk).delete()
self.get_object().delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class APINoteViewSet(viewsets.ModelViewSet):
class APINoteViewSet(
DisabledObjectPermissionCheckMixin,
NestedObjectFinderMixin,
viewsets.ModelViewSet,
):
"""
Returns a list of notes.
Superusers may access and update all notes.
"""
ROUTE_FILTER_LOOKUPS = {
"show_pk": "show",
"timeslot_pk": "timeslot",
}
queryset = Note.objects.all()
serializer_class = NoteSerializer
permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
pagination_class = LimitOffsetPagination
filter_class = filters.NoteFilterSet
def get_queryset(self):
queryset = super().get_queryset()
# subroute filters
show_pk, timeslot_pk = get_values(self.kwargs, "show_pk", "timeslot_pk")
if show_pk:
queryset = queryset.filter(show=show_pk)
if timeslot_pk:
queryset = queryset.filter(timeslot=timeslot_pk)
return queryset
def create(self, request, *args, **kwargs):
"""Create a note"""
show_pk, schedule_pk, timeslot_pk = get_values(
self.kwargs, "show_pk", "schedule_pk", "timeslot_pk"
)
"""
Only superusers can create new notes.
"""
show_pk, timeslot_pk = get_values(self.kwargs, "show_pk", "timeslot_pk")
if (
not request.user.is_superuser
......@@ -649,10 +598,6 @@ class APINoteViewSet(viewsets.ModelViewSet):
):
return Response(status=status.HTTP_401_UNAUTHORIZED)
# Only create a note if show_id, timeslot_id and schedule_id is given
if show_pk is None or schedule_pk is None or timeslot_pk is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
serializer = NoteSerializer(
data={"show": show_pk, "timeslot": timeslot_pk} | request.data,
context={"user_id": request.user.id},
......@@ -670,52 +615,11 @@ class APINoteViewSet(viewsets.ModelViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, *args, **kwargs):
def update(self, request, *args, **kwargs):
"""
Returns a single note
Called by:
/notes/1
/shows/1/notes/1
/shows/1/timeslots/1/note/1
/shows/1/schedules/1/timeslots/1/note/1
Only superusers can update existing notes.
"""
pk, show_pk, schedule_pk, timeslot_pk = get_values(
self.kwargs, "pk", "show_pk", "schedule_pk", "timeslot_pk"
)
#
# /shows/1/notes/1
#
# Returns a note to a show
#
if show_pk and timeslot_pk is None and schedule_pk is None:
note = get_object_or_404(Note, pk=pk, show=show_pk)
#
# /shows/1/timeslots/1/note/1
# /shows/1/schedules/1/timeslots/1/note/1
#
# Return a note to a timeslot
#
elif show_pk and timeslot_pk:
note = get_object_or_404(Note, pk=pk, show=show_pk, timeslot=timeslot_pk)
#
# /notes/1
#
# Returns the given note
#
else:
note = get_object_or_404(Note, pk=pk)
serializer = NoteSerializer(note)
return Response(serializer.data)
def update(self, request, *args, **kwargs):
pk, show_pk, schedule_pk, timeslot_pk = get_values(
self.kwargs, "pk", "show_pk", "schedule_pk", "timeslot_pk"
)
show_pk = get_values(self.kwargs, "show_pk")
if (
not request.user.is_superuser
......@@ -723,13 +627,7 @@ class APINoteViewSet(viewsets.ModelViewSet):
):
return Response(status=status.HTTP_401_UNAUTHORIZED)
# Allow PUT only when calling
# /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{timeslot_pk}/note/{pk}
if show_pk is None or schedule_pk is None or timeslot_pk is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
note = get_object_or_404(Note, pk=pk, timeslot=timeslot_pk, show=show_pk)
note = self.get_object()
serializer = NoteSerializer(note, data=request.data)
if serializer.is_valid():
......@@ -748,9 +646,10 @@ class APINoteViewSet(viewsets.ModelViewSet):
return Response(status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, *args, **kwargs):
pk, show_pk, schedule_pk, timeslot_pk = get_values(
self.kwargs, "pk", "show_pk", "schedule_pk", "timeslot_pk"
)
"""
Only superusers can delete existing notes.
"""
show_pk = get_values(self.kwargs, "show_pk")
if (
not request.user.is_superuser
......@@ -758,10 +657,7 @@ class APINoteViewSet(viewsets.ModelViewSet):
):
return Response(status=status.HTTP_401_UNAUTHORIZED)
if pk is None or show_pk is None or schedule_pk is None or timeslot_pk is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
Note.objects.get(pk=pk).delete()
self.get_object().delete()
return Response(status=status.HTTP_204_NO_CONTENT)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment