Newer
Older
#
# 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/>.
#
from datetime import date, datetime, time
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,
CategorySerializer,
FundingCategorySerializer,
HostSerializer,
LanguageSerializer,
MusicFocusSerializer,
NoteSerializer,
ScheduleSerializer,
from program.utils import get_pk_and_slug, get_values, parse_date
logger = logging.getLogger(__name__)

Ernesto Rico Schmidt
committed
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:
"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,
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
"""
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 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
)
if ts.playlist_id is None or ts.playlist_id == 0:
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(viewsets.ModelViewSet):
"""
/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)
"""
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)
"""Returns a single user"""
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)
"""
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)
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():
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
"""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()
serializer_class = ShowSerializer
permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
pagination_class = LimitOffsetPagination
filterset_class = filters.ShowFilterSet
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)
"""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
)
Update a show.
Common users may only update shows they own.
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
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Delete a show.
Only superusers may delete shows.
if not request.user.is_superuser:
return Response(status=status.HTTP_401_UNAUTHORIZED)
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")
return Schedule.objects.filter(show=show_pk)
return Schedule.objects.all()
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)
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
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
return Response(resolution, status=status.HTTP_201_CREATED)
# Otherwise return conflicts
return Response(resolution, status=status.HTTP_409_CONFLICT)
Update a schedule, generate timeslots, test for collisions and resolve them including notes
Only superusers may update schedules
"""
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)
return Response(resolution, status=status.HTTP_409_CONFLICT)
"""
Delete a schedule
Only superusers may delete schedules
"""
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
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
pk, show_pk = get_values(self.kwargs, "pk", "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)
"""
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)
"""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)
# ...or nothing if there isn't one
return Response(status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
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/...
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
"""
serializer_class = NoteSerializer
permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
pagination_class = LimitOffsetPagination
timeslot_pk, show_pk = get_values(self.kwargs, "timeslot_pk", "show_pk")
#
# /shows/1/schedules/1/timeslots/1/note
# /shows/1/timeslots/1/note
#
# Return a note to the timeslot
#
notes = Note.objects.filter(show=show_pk, timeslot=timeslot_pk)
#
# /shows/1/notes
#
# Returns notes to the show
#
notes = Note.objects.filter(show=show_pk)
#
# /notes
#
# Returns all notes
#
else:
notes = Note.objects.all()
if ids := self.request.query_params.get("ids"):
note_ids = list(map(int, ids.split(",")))
notes = notes.filter(id__in=note_ids)
if host := self.request.query_params.get("host"):
if owner := self.request.query_params.get("owner"):
# Filter notes by show owner: all notes the user may edit
notes = notes.filter(show__in=shows)
if user := self.request.query_params.get("user"):
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)
"""
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
#
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)
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)
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]
return Response(status=status.HTTP_400_BAD_REQUEST)
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)

Ingo Leindecker
committed
return Response(status=status.HTTP_204_NO_CONTENT)

Ingo Leindecker
committed
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):

Ingo Leindecker
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)

Ingo Leindecker
committed
"""

Ingo Leindecker
committed
queryset = Category.objects.all()
serializer_class = CategorySerializer
class APITypeViewSet(ActiveInactiveViewSet):

Ingo Leindecker
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)

Ingo Leindecker
committed
"""
queryset = Type.objects.all()
serializer_class = TypeSerializer
class APITopicViewSet(ActiveInactiveViewSet):

Ingo Leindecker
committed
"""
/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)

Ingo Leindecker
committed
"""
queryset = Topic.objects.all()
serializer_class = TopicSerializer

Ingo Leindecker
committed
class APIMusicFocusViewSet(ActiveInactiveViewSet):

Ingo Leindecker
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)

Ingo Leindecker
committed
"""
queryset = MusicFocus.objects.all()
serializer_class = MusicFocusSerializer

Ingo Leindecker
committed
class APIFundingCategoryViewSet(ActiveInactiveViewSet):

Ingo Leindecker
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)

Ingo Leindecker
committed
"""
queryset = FundingCategory.objects.all()
serializer_class = FundingCategorySerializer
class APILanguageViewSet(ActiveInactiveViewSet):

Ingo Leindecker
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)

Ingo Leindecker
committed
"""
queryset = Language.objects.all()
serializer_class = LanguageSerializer
class APIHostViewSet(ActiveInactiveViewSet):

Ingo Leindecker
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)

Ingo Leindecker
committed
"""
queryset = Host.objects.all()
serializer_class = HostSerializer
pagination_class = LimitOffsetPagination