# # 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/>. # import json import logging from datetime import date, datetime, time from rest_framework import mixins, 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, Language, MusicFocus, Note, Schedule, Show, TimeSlot, Topic, Type, ) from program.serializers import ( CategorySerializer, FundingCategorySerializer, HostSerializer, LanguageSerializer, MusicFocusSerializer, NoteSerializer, ScheduleSerializer, ShowSerializer, TimeSlotSerializer, TopicSerializer, TypeSerializer, UserSerializer, ) 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))) else: 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: 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, } schedule.append(entry) return HttpResponse( json.dumps(schedule, ensure_ascii=False).encode("utf8"), content_type="application/json; charset=utf-8", ) def json_playout(request): """ 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))) else: 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") ) schedule = [] for ts in timeslots: 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 ) type_ = Type.objects.get(pk=ts.show.type_id) classname = "default" if ts.playlist_id is None or ts.playlist_id == 0: classname = "danger" entry = { "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, } schedule.append(entry) return HttpResponse( json.dumps(schedule, ensure_ascii=False).encode("utf8"), content_type="application/json; charset=utf-8", ) class APIUserViewSet( mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet, ): """ /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) Superusers may access and update all users. """ permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] serializer_class = UserSerializer 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) def retrieve(self, request, *args, **kwargs): """Returns a single user""" pk = get_values(self.kwargs, "pk") # Common users only see themselves 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) 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) def update(self, request, *args, **kwargs): pk = get_values(self.kwargs, "pk") serializer = UserSerializer(data=request.data) # Common users may only edit themselves 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} ) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, 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() serializer_class = ShowSerializer permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] pagination_class = LimitOffsetPagination filterset_class = filters.ShowFilterSet 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) def retrieve(self, request, *args, **kwargs): """Returns a single show""" 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) return Response(serializer.data) 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} ) if serializer.is_valid(): # Common users mustn't edit the show's name if not request.user.is_superuser: serializer.validated_data["name"] = show.name serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): """ /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") if show_pk: return Schedule.objects.filter(show=show_pk) return Schedule.objects.all() 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 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) 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) 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) # TODO: Create is currently not implemented because timeslots are supposed to be inserted # by creating or updating a schedule. # There might be a use case for adding a single timeslot without any conflicts though. class APITimeSlotViewSet( mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet, ): """ 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) 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" ) 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) 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/... 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): """ /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) Superusers may access and update all notes """ queryset = Note.objects.none() serializer_class = NoteSerializer permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] pagination_class = LimitOffsetPagination def get_queryset(self): timeslot_pk, show_pk = get_values(self.kwargs, "timeslot_pk", "show_pk") # Endpoints # # /shows/1/schedules/1/timeslots/1/note # /shows/1/timeslots/1/note # # Return a note to the timeslot # if show_pk and timeslot_pk: notes = Note.objects.filter(show=show_pk, timeslot=timeslot_pk) # # /shows/1/notes # # Returns notes to the show # elif show_pk and timeslot_pk is None: notes = Note.objects.filter(show=show_pk) # # /notes # # Returns all notes # else: notes = Note.objects.all() # Filters if ids := self.request.query_params.get("ids"): # 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"): # Filter notes by host notes = notes.filter(host=int(host)) if owner := self.request.query_params.get("owner"): # Filter notes by show owner: all notes the user may edit shows = Show.objects.filter(owners=int(owner)) notes = notes.filter(show__in=shows) if user := self.request.query_params.get("user"): # Filter notes by their creator notes = notes.filter(user=int(user)) return notes def create(self, request, *args, **kwargs): """Create a note""" 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 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} ) if serializer.is_valid(): 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 serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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 # 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" ) 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} 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] serializer.save() return Response(serializer.data) 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" ) 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): """ /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): """ /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) """ queryset = Type.objects.all() serializer_class = TypeSerializer class APITopicViewSet(ActiveInactiveViewSet): """ /topics/: Returns all topics (GET, POST) /topics/?active=true Returns all active topics (GET) /topics/?active=false Returns all inactive topics (GET) /topics/{pk}: Returns a topic by its ID (GET, PUT, DELETE) """ queryset = Topic.objects.all() serializer_class = TopicSerializer class APIMusicFocusViewSet(ActiveInactiveViewSet): """ /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): """ /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): """ /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): """ /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