Skip to content
Snippets Groups Projects
views.py 30.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • #
    # steering, Programme/schedule management for AURA
    #
    # Copyright (C) 2011-2017, 2020, Ernesto Rico Schmidt
    # Copyright (C) 2017-2019, Ingo Leindecker
    #
    # This program is free software: you can redistribute it and/or modify it under
    # the terms of the GNU Affero General Public License as published by the Free
    # Software Foundation, either version 3 of the License, or (at your option) any
    # later version.
    #
    # This program is distributed in the hope that it will be useful, but WITHOUT
    # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    # FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
    # details.
    #
    # You should have received a copy of the GNU Affero General Public License
    # along with this program.  If not, see <http://www.gnu.org/licenses/>.
    #
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
    import json
    
    import logging
    
    from datetime import date, datetime, time
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
    from rest_framework import permissions, status, viewsets
    from rest_framework.pagination import LimitOffsetPagination
    from rest_framework.response import Response
    
    
    from django.contrib.auth.models import User
    
    from django.http import HttpResponse
    
    from django.shortcuts import get_object_or_404
    
    from django.utils import timezone
    
    from django.utils.translation import gettext as _
    
    from program import filters
    
    from program.models import (
        Category,
        FundingCategory,
        Host,
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        Language,
        MusicFocus,
        Note,
    
        Schedule,
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        Show,
        TimeSlot,
        Topic,
        Type,
    
    )
    from program.serializers import (
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        CategorySerializer,
        FundingCategorySerializer,
        HostSerializer,
    
        LanguageSerializer,
        MusicFocusSerializer,
        NoteSerializer,
        ScheduleSerializer,
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        ShowSerializer,
    
        TimeSlotSerializer,
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        TopicSerializer,
        TypeSerializer,
    
        UserSerializer,
    )
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
    from program.utils import get_pk_and_slug, get_values, parse_date
    
    logger = logging.getLogger(__name__)
    
    def json_day_schedule(request, year=None, month=None, day=None):
        if year is None and month is None and day is None:
    
            today = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0)))
    
            today = timezone.make_aware(
                datetime.combine(date(year, month, day), time(0, 0))
            )
    
        timeslots = (
            TimeSlot.objects.get_24h_timeslots(today)
            .select_related("schedule")
            .select_related("show")
        )
    
        schedule = []
        for ts in timeslots:
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            entry = {
    
                "start": ts.start.strftime("%Y-%m-%d_%H:%M:%S"),
                "end": ts.end.strftime("%Y-%m-%d_%H:%M:%S"),
                "title": ts.show.name,
                "id": ts.show.id,
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            }
    
            schedule.append(entry)
    
        return HttpResponse(
            json.dumps(schedule, ensure_ascii=False).encode("utf8"),
            content_type="application/json; charset=utf-8",
        )
    
        Called by
           - engine (playout) to retrieve timeslots within a given timerange
             Expects GET variables 'start' (date) and 'end' (date).
             If start not given, it will be today
    
           - internal calendar to retrieve all timeslots for a week
             Expects GET variable 'start' (date), otherwise start will be today
             If end not given, it returns all timeslots of the next 7 days
    
        if request.GET.get("start") is None:
    
            start = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0)))
    
            start = timezone.make_aware(
                datetime.combine(parse_date(request.GET.get("start")), time(0, 0))
            )
    
        if request.GET.get("end") is None:
    
            # If no end was given, return the next week
    
            timeslots = (
                TimeSlot.objects.get_7d_timeslots(start)
                .select_related("schedule")
                .select_related("show")
            )
    
        else:
            # Otherwise return the given timerange
    
            end = timezone.make_aware(
                datetime.combine(parse_date(request.GET.get("end")), time(23, 59))
            )
            timeslots = (
                TimeSlot.objects.get_timerange_timeslots(start, end)
                .select_related("schedule")
                .select_related("show")
            )
    
            is_repetition = " " + _("REP") if ts.schedule.is_repetition is True else ""
    
            hosts = ", ".join(ts.show.hosts.values_list("name", flat=True))
            categories = ", ".join(ts.show.category.values_list("name", flat=True))
            topics = ", ".join(ts.show.topic.values_list("name", flat=True))
            music_focus = ", ".join(ts.show.music_focus.values_list("name", flat=True))
            languages = ", ".join(ts.show.language.values_list("name", flat=True))
            funding_category = (
                FundingCategory.objects.get(pk=ts.show.funding_category_id)
                if ts.show.funding_category_id
                else None
            )
    
    Ingo Leindecker's avatar
    Ingo Leindecker committed
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            type_ = Type.objects.get(pk=ts.show.type_id)
    
            classname = "default"
    
            if ts.playlist_id is None or ts.playlist_id == 0:
    
                classname = "danger"
    
                "id": ts.id,
                "start": ts.start.strftime("%Y-%m-%dT%H:%M:%S"),
                "end": ts.end.strftime("%Y-%m-%dT%H:%M:%S"),
                "title": ts.show.name + is_repetition,  # For JS Calendar
                "schedule_id": ts.schedule.id,
                "is_repetition": ts.is_repetition,
                "playlist_id": ts.playlist_id,
                "schedule_default_playlist_id": ts.schedule.default_playlist_id,
                "show_default_playlist_id": ts.show.default_playlist_id,
                "show_id": ts.show.id,
                "show_name": ts.show.name + is_repetition,
                "show_hosts": hosts,
                "show_type": type_.name,
                "show_categories": categories,
                "show_topics": topics,
    
                # TODO: replace `show_musicfocus` with `show_music_focus` when engine is updated
    
                "show_musicfocus": music_focus,
                "show_languages": languages,
                # TODO: replace `show_fundingcategory` with `show_funding_category` when engine is
                #  updated
                "show_fundingcategory": funding_category.name,
                "memo": ts.memo,
                "className": classname,
    
        return HttpResponse(
            json.dumps(schedule, ensure_ascii=False).encode("utf8"),
            content_type="application/json; charset=utf-8",
        )
    
    class APIUserViewSet(viewsets.ModelViewSet):
        """
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /users returns oneself. Superusers see all users. Only superusers may create a user (GET, POST)
    
        /users/{pk} retrieves or updates a single user. Non-superusers may only update certain fields
         (GET, PUT)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        Superusers may access and update all users.
    
        permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    
        queryset = User.objects.none()
    
        def get_queryset(self):
            """Constrain access to oneself except for superusers"""
            if self.request.user.is_superuser:
                return User.objects.all()
    
    
            return User.objects.filter(pk=self.request.user.id)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def retrieve(self, request, *args, **kwargs):
    
            pk = get_values(self.kwargs, "pk")
    
            # Common users only see themselves
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            if not request.user.is_superuser and pk != request.user.id:
    
                return Response(status=status.HTTP_401_UNAUTHORIZED)
    
            user = get_object_or_404(User, pk=pk)
    
            serializer = UserSerializer(user)
            return Response(serializer.data)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def create(self, request, *args, **kwargs):
    
            """
            Create a User
            Only superusers may create a user
            """
    
            if not request.user.is_superuser:
                return Response(status=status.HTTP_401_UNAUTHORIZED)
    
            serializer = UserSerializer(data=request.data)
    
            if serializer.is_valid():
                serializer.save()
    
                return Response(serializer.data, status=status.HTTP_201_CREATED)
    
    
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def update(self, request, *args, **kwargs):
    
            pk = get_values(self.kwargs, "pk")
    
            serializer = UserSerializer(data=request.data)
    
            # Common users may only edit themselves
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            if not request.user.is_superuser and pk != request.user.id:
    
                return Response(
                    serializer.initial_data, status=status.HTTP_401_UNAUTHORIZED
                )
    
            user = get_object_or_404(User, pk=pk)
    
            serializer = UserSerializer(
                user, data=request.data, context={"user": request.user}
            )
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                serializer.save()
    
                return Response(serializer.data)
    
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def destroy(self, request, *args, **kwargs):
    
            """Deleting users is prohibited: Set 'is_active' to False instead"""
    
            return Response(status=status.HTTP_400_BAD_REQUEST)
    
    class APIShowViewSet(viewsets.ModelViewSet):
        """
    
        Returns a list of available shows.
        Only superusers may add and delete shows.
    
        queryset = Show.objects.all()
    
        permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    
        pagination_class = LimitOffsetPagination
    
        filterset_class = filters.ShowFilterSet
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def create(self, request, *args, **kwargs):
    
            Create a show.
    
            Only superusers may create a show.
    
            if not request.user.is_superuser:
                return Response(status=status.HTTP_401_UNAUTHORIZED)
    
            serializer = ShowSerializer(data=request.data)
    
            if serializer.is_valid():
                serializer.save()
    
                return Response(serializer.data, status=status.HTTP_201_CREATED)
    
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def retrieve(self, request, *args, **kwargs):
    
            pk, slug = get_pk_and_slug(self.kwargs)
    
            show = (
                get_object_or_404(Show, pk=pk)
                if pk
                else get_object_or_404(Show, slug=slug)
                if slug
                else None
            )
    
            serializer = ShowSerializer(show)
    
    Ingo Leindecker's avatar
    Ingo Leindecker committed
    
    
            return Response(serializer.data)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def update(self, request, *args, **kwargs):
    
            Update a show.
    
            Common users may only update shows they own.
    
            pk = get_values(self.kwargs, "pk")
    
            if not request.user.is_superuser and pk not in request.user.shows.values_list(
                "id", flat=True
            ):
    
                return Response(status=status.HTTP_401_UNAUTHORIZED)
    
            show = get_object_or_404(Show, pk=pk)
    
            serializer = ShowSerializer(
                show, data=request.data, context={"user": request.user}
            )
    
                # Common users mustn't edit the show's name
                if not request.user.is_superuser:
    
                    serializer.validated_data["name"] = show.name
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                serializer.save()
    
                return Response(serializer.data)
    
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def destroy(self, request, *args, **kwargs):
    
            Delete a show.
    
            Only superusers may delete shows.
    
            if not request.user.is_superuser:
                return Response(status=status.HTTP_401_UNAUTHORIZED)
    
            pk = get_values(self.kwargs, "pk")
    
            Show.objects.get(pk=pk).delete()
    
            return Response(status=status.HTTP_204_NO_CONTENT)
    
    
    
    class APIScheduleViewSet(viewsets.ModelViewSet):
        """
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /schedules/ returns all schedules (GET)
        /schedules/{pk} returns the given schedule (GET)
        /shows/{show_pk}/schedules returns schedules of the show (GET, POST)
        /shows/{show_pk}/schedules/{pk} returns schedules by its ID (GET, PUT, DELETE)
    
    
        Only superusers may create and update schedules
        """
    
        queryset = Schedule.objects.none()
        serializer_class = ScheduleSerializer
        permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    
        def get_queryset(self):
    
            show_pk = get_values(self.kwargs, "show_pk")
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            if show_pk:
    
                return Schedule.objects.filter(show=show_pk)
    
            return Schedule.objects.all()
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        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)
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def create(self, request, *args, **kwargs):
    
            Create a schedule, generate timeslots, test for collisions and resolve them including notes
    
    
            Only superusers may add schedules
    
            TODO: Perhaps directly insert into database if no conflicts found
    
            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 creating when calling /shows/{show_pk}/schedules/ and with ehe `schedule` JSON
            # object
            if show_pk is None or "schedule" not in request.data:
    
                return Response(status=status.HTTP_400_BAD_REQUEST)
    
            # First create submit -> return projected timeslots and collisions
    
            if "solutions" not in request.data:
                return Response(
                    Schedule.make_conflicts(request.data["schedule"], pk, show_pk),
                    status=status.HTTP_409_CONFLICT,
                )
    
            # Otherwise try to resolve
            resolution = Schedule.resolve_conflicts(request.data, pk, show_pk)
    
            # If resolution went well
    
            if "projected" not in resolution:
    
                return Response(resolution, status=status.HTTP_201_CREATED)
    
            # Otherwise return conflicts
    
            return Response(resolution, status=status.HTTP_409_CONFLICT)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def update(self, request, *args, **kwargs):
    
            Update a schedule, generate timeslots, test for collisions and resolve them including notes
    
    
            Only superusers may update schedules
            """
    
    
            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:
    
                return Response(status=status.HTTP_400_BAD_REQUEST)
    
    
            # 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()
    
                serializer = ScheduleSerializer(schedule)
                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()
    
                serializer = ScheduleSerializer(schedule)
                return Response(serializer.data)
    
    
            # First update submit -> return projected timeslots and collisions
    
            if "solutions" not in request.data:
                return Response(
                    Schedule.make_conflicts(request.data["schedule"], pk, show_pk),
                    status=status.HTTP_409_CONFLICT,
                )
    
    
            # Otherwise try to resolve
            resolution = Schedule.resolve_conflicts(request.data, pk, show_pk)
    
            # If resolution went well
    
            if "projected" not in resolution:
    
                return Response(resolution)
    
            # Otherwise return conflicts
    
            return Response(resolution, status=status.HTTP_409_CONFLICT)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def destroy(self, request, *args, **kwargs):
    
            """
            Delete a schedule
            Only superusers may delete schedules
            """
    
    
            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()
    
    
            return Response(status=status.HTTP_204_NO_CONTENT)
    
    
    
    class APITimeSlotViewSet(viewsets.ModelViewSet):
        """
    
        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.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)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def retrieve(self, request, *args, **kwargs):
    
            pk, show_pk = get_values(self.kwargs, "pk", "show_pk")
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            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)
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def create(self, request, *args, **kwargs):
    
            """
            Timeslots may only be created by adding/updating schedules
            TODO: Adding single timeslot which fits to schedule?
            """
    
            return Response(status=status.HTTP_400_BAD_REQUEST)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def update(self, request, *args, **kwargs):
    
            pk, show_pk, schedule_pk = get_values(
                self.kwargs, "pk", "show_pk", "schedule_pk"
            )
    
            if (
                not request.user.is_superuser
                and show_pk not in request.user.shows.values_lis("id", flat=True)
            ):
    
                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
            )
    
            serializer = TimeSlotSerializer(timeslot, data=request.data)
    
            if serializer.is_valid():
                serializer.save()
    
    
                # Return the next repetition
                # We do this because the Dashboard needs to update the repetition timeslot as well
                # but with another playlist containing the recording instead of the original playlist
                ts = TimeSlot.objects.filter(show=show_pk, start__gt=timeslot.start)[0]
                if ts.is_repetition:
                    serializer = TimeSlotSerializer(ts)
    
                    return Response(serializer.data)
    
    
                # ...or nothing if there isn't one
                return Response(status=status.HTTP_200_OK)
    
    
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def destroy(self, request, *args, **kwargs):
    
            Deletes a timeslot.
    
            Only superusers may delete timeslots.
    
    
            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/...
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            if show_pk is None:
    
                return Response(status=status.HTTP_400_BAD_REQUEST)
    
            TimeSlot.objects.get(pk=pk).delete()
    
    
            return Response(status=status.HTTP_204_NO_CONTENT)
    
    
    
    class APINoteViewSet(viewsets.ModelViewSet):
        """
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /notes/ returns all notes (GET)
        /notes/{pk} returns a single note (if owned) (GET)
        /notes/?ids={...} returns given notes (if owned) (GET)
        /notes/?host={host} returns notes assigned to a given host (GET)
        /notes/?owner={owner} returns notes editable by a given user (GET)
        /notes/?user={user} returns notes created by a given user (GET)
        /shows/{show_pk}/notes returns all notes of a show (GET)
        /shows/{show_pk}/notes/{pk} returns a note by its ID (GET)
        /shows/{show_pk}/timeslots/{timeslot_pk}/note/ returns a note of the timeslot (GET)
        /shows/{show_pk}/timeslots/{timeslot_pk}/note/{pk} returns a note by its ID (GET)
    
        /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{timeslot_pk}/note returns a note to the
         timeslot (GET, POST).
        /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{timeslot_pk}/note/{pk} returns a note by
         its ID (GET, PUT, DELETE)
    
        queryset = Note.objects.none()
    
        permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    
        pagination_class = LimitOffsetPagination
    
        def get_queryset(self):
    
            timeslot_pk, show_pk = get_values(self.kwargs, "timeslot_pk", "show_pk")
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            # Endpoints
    
    
            #
            #     /shows/1/schedules/1/timeslots/1/note
            #     /shows/1/timeslots/1/note
            #
            #     Return a note to the timeslot
            #
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            if show_pk and timeslot_pk:
    
                notes = Note.objects.filter(show=show_pk, timeslot=timeslot_pk)
    
    
            #
            #     /shows/1/notes
            #
            #     Returns notes to the show
            #
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            elif show_pk and timeslot_pk is None:
    
                notes = Note.objects.filter(show=show_pk)
    
    
            #
            #     /notes
            #
            #     Returns all notes
            #
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            # Filters
    
            if ids := self.request.query_params.get("ids"):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                # Filter notes by their IDs
    
                note_ids = list(map(int, ids.split(",")))
    
                notes = notes.filter(id__in=note_ids)
    
    
            if host := self.request.query_params.get("host"):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                # Filter notes by host
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                notes = notes.filter(host=int(host))
    
            if owner := self.request.query_params.get("owner"):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                # Filter notes by show owner: all notes the user may edit
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                shows = Show.objects.filter(owners=int(owner))
    
                notes = notes.filter(show__in=shows)
    
    
            if user := self.request.query_params.get("user"):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                # Filter notes by their creator
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                notes = notes.filter(user=int(user))
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def create(self, request, *args, **kwargs):
    
            show_pk, schedule_pk, timeslot_pk = get_values(
                self.kwargs, "show_pk", "schedule_pk", "timelost_pk"
            )
    
            if (
                not request.user.is_superuser
                and show_pk not in request.user.shows.values_list("id", flat=True)
            ):
    
                return Response(status=status.HTTP_401_UNAUTHORIZED)
    
            # Only create a note if show_id, timeslot_id and schedule_id is given
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            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=request.data, context={"user_id": request.user.id}
            )
    
                hosts = Host.objects.filter(
                    shows__in=request.user.shows.values_list("id", flat=True)
                )
                if not request.user.is_superuser and request.data["host"] not in hosts:
                    serializer.validated_data["host"] = None
    
                return Response(serializer.data, status=status.HTTP_201_CREATED)
    
    
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def retrieve(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
    
            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
            #
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            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
            #
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            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)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def update(self, request, *args, **kwargs):
    
            pk, show_pk, schedule_pk, timeslot_pk = get_values(
                self.kwargs, "pk", "show_pk", "schedule_pk", "timeslot_pk"
            )
    
            if (
                not request.user.is_superuser
                and show_pk not in request.user.shows.values_list("id", flat=True)
            ):
    
                return Response(status=status.HTTP_401_UNAUTHORIZED)
    
            # Allow PUT only when calling
            #  /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{timeslot_pk}/note/{pk}
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
            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)
    
            serializer = NoteSerializer(note, data=request.data)
    
    
            if serializer.is_valid():
    
                hosts = Host.objects.filter(
                    shows__in=request.user.shows.values_list("id", flat=True)
                )
    
                # Don't assign a host the user mustn't edit. Reassign the original value instead
    
                if not request.user.is_superuser and int(request.data["host"]) not in hosts:
                    serializer.validated_data["host"] = Host.objects.filter(
                        pk=note.host_id
                    )[0]
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                serializer.save()
    
                return Response(serializer.data)
    
    
            return Response(status=status.HTTP_400_BAD_REQUEST)
    
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        def destroy(self, request, *args, **kwargs):
    
            pk, show_pk, schedule_pk, timeslot_pk = get_values(
                self.kwargs, "pk", "show_pk", "schedule_pk", "timeslot_pk"
            )
    
            if (
                not request.user.is_superuser
                and show_pk not in request.user.shows.values_list("id", flat=True)
            ):
    
                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()
    
            return Response(status=status.HTTP_204_NO_CONTENT)
    
    class ActiveInactiveViewSet(viewsets.ModelViewSet):
    
        permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    
        def get_queryset(self: viewsets.ModelViewSet):
            """Filters"""
    
    
            if self.request.query_params.get("active") == "true":
    
                return self.queryset.model.objects.filter(is_active=True)
    
    
            if self.request.query_params.get("active") == "false":
    
                return self.queryset.model.objects.filter(is_active=False)
    
            return self.queryset.model.objects.all()
    
    
    
    class APICategoryViewSet(ActiveInactiveViewSet):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /categories/ returns all categories (GET, POST)
        /categories/?active=true returns all active categories (GET)
        /categories/?active=false returns all inactive categories (GET)
        /categories/{pk} Returns a category by its ID (GET, PUT, DELETE)
    
        queryset = Category.objects.all()
        serializer_class = CategorySerializer
    
    
    
    class APITypeViewSet(ActiveInactiveViewSet):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /types/ returns all types (GET, POST)
        /types/?active=true returns all active types (GET)
        /types/?active=false returns all inactive types (GET)
        /types/{pk} returns a type by its ID (GET, PUT, DELETE)
    
    class APITopicViewSet(ActiveInactiveViewSet):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /topics/: Returns all topics (GET, POST)
        /topics/?active=true Returns all active topics (GET)
    
        /topics/?active=false Returns all inactive topics (GET)
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /topics/{pk}: Returns a topic by its ID (GET, PUT, DELETE)
    
        """
    
        queryset = Topic.objects.all()
        serializer_class = TopicSerializer
    
    class APIMusicFocusViewSet(ActiveInactiveViewSet):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /musicfocus/ returns all music focuses (GET, POST)
        /musicfocus/?active=true: returns all active music focuses (GET)
        /musicfocus/?active=false: returns all inactive music focuses (GET)
        /musicfocus/{pk}: returns a music focus by its ID (GET, PUT, DELETE)
    
        """
    
        queryset = MusicFocus.objects.all()
        serializer_class = MusicFocusSerializer
    
    class APIFundingCategoryViewSet(ActiveInactiveViewSet):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /fundingcategories/: returns all funding categories (GET, POST)
        /fundingcategories/?active=true returns all active funding categories (GET)
        /fundingcategories/?active=false returns all inactive funding categories (GET)
        /fundingcategories/{pk} returns a funding category by its ID (GET, PUT, DELETE)
    
        queryset = FundingCategory.objects.all()
        serializer_class = FundingCategorySerializer
    
    class APILanguageViewSet(ActiveInactiveViewSet):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /languages/ returns all languages (GET, POST)
        /languages/?active=true returns all active languages (GET)
        /languages/?active=false returns all inactive languages (GET)
        /languages/{pk} returns a language by its ID (GET, PUT, DELETE)
    
        """
    
        queryset = Language.objects.all()
        serializer_class = LanguageSerializer
    
    class APIHostViewSet(ActiveInactiveViewSet):
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
        /hosts/ returns all hosts (GET, POST)
        /hosts/?active=true returns all active hosts (GET)
        /hosts/?active=false returns all inactive hosts (GET)
        /hosts/{pk} returns a host by its ID (GET, PUT, DELETE)
    
        """
    
        queryset = Host.objects.all()
        serializer_class = HostSerializer
    
        pagination_class = LimitOffsetPagination