From 0c8ba5eb8737caea62df865e9e2a76d9a0a09835 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sun, 2 Feb 2020 02:10:33 +0100 Subject: [PATCH] Add REST API to webinterface --- api/serializers.py | 45 +++++++++++++++++++++++++++++++++++++----- api/urls.py | 3 ++- api/views.py | 46 ++++++++++++++++++++++++++++++++++++------- contest/models.py | 5 +++++ contest/validators.py | 1 - cqtu/settings.py | 5 +++++ requirements.txt | 1 + 7 files changed, 92 insertions(+), 14 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 5b9bc4d..575fd6a 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,6 +1,9 @@ +from django import forms +from django.utils import timezone from rest_framework import serializers from contest.models import Contest, Band, Frequency, QSO, EntryCategory, User, ShadowCall, Reference +from contest.validators import CallLogValidator class ContestSerializer(serializers.ModelSerializer): @@ -39,20 +42,52 @@ class ReferenceSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer): - ref = ReferenceSerializer() - cat = EntryCategorySerializer() + # ref = ReferenceSerializer() + # cat = EntryCategorySerializer() class Meta: model = User fields = ('id', 'ref', 'cat', 'location', 'opName', 'regTime', 'dncall', 'qrv2m', 'qrv70cm', 'extra2m70cm') + read_only_fields = ('ref', 'location', 'regTime') + def validate(self, attrs): + contest = Contest.get_current_contest() + if contest.deadline < timezone.now(): + raise serializers.ValidationError("The deadline for changing the entry category has passed") -class QSOSerializer(serializers.ModelSerializer): - # owner = UserSerializer() + return attrs + +class QSOSerializer(serializers.ModelSerializer): class Meta: model = QSO - fields = ('id', 'owner', 'time', 'call', 'callRef', 'remarks') + fields = ('id', 'owner', 'time', 'ownNo', 'band', 'call', 'reportTX', 'reportRX', 'refStr', 'remarks') + read_only_fields = ('owner',) + + def validate_call(self, value): + val = value.strip().upper() + try: + CallLogValidator()(val) + except forms.ValidationError as e: + raise serializers.ValidationError({'errors': e.error_list}) + + return val + + def validate(self, attrs): + ownNo = attrs['ownNo'] + try: + o = QSO.objects.get(owner=self.context['request'].user, ownNo=ownNo) + if not (self.instance and self.instance.id and self.instance.id == o.id): + raise serializers.ValidationError("You already logged a QSO with the number %s" % ownNo) + except QSO.DoesNotExist: + pass + + band = attrs.get('band') + if band: + if band.contest.deadline < timezone.now(): + raise serializers.ValidationError("The deadline for logging and editing QSOs has passed") + + return attrs class ShadowCallSerializer(serializers.ModelSerializer): diff --git a/api/urls.py b/api/urls.py index 408cad4..57fd854 100644 --- a/api/urls.py +++ b/api/urls.py @@ -2,7 +2,7 @@ from django.conf.urls import include, url from rest_framework import routers from .views import ContestViewSet, BandViewSet, FrequencyViewSet, EntryCategoryViewSet, ReferenceViewSet, QSOViewSet, \ - ShadowCallViewSet + ShadowCallViewSet, UserProfileViewSet router = routers.DefaultRouter() router.register('contests', ContestViewSet) @@ -12,6 +12,7 @@ router.register('entrycategories', EntryCategoryViewSet) router.register('references', ReferenceViewSet) router.register('qsos', QSOViewSet, basename='qso') router.register('shadowcalls', ShadowCallViewSet) +router.register('profile', UserProfileViewSet, basename='profile') urlpatterns = [ url(r'^', include(router.urls)), diff --git a/api/views.py b/api/views.py index 86d5cae..c91a9be 100644 --- a/api/views.py +++ b/api/views.py @@ -1,52 +1,84 @@ -from rest_framework import viewsets +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import viewsets, generics, filters from rest_framework.permissions import IsAuthenticated, IsAdminUser - +from rest_framework.response import Response from .serializers import ContestSerializer, BandSerializer, FrequencySerializer, EntryCategorySerializer, \ - ReferenceSerializer, QSOSerializer, ShadowCallSerializer + ReferenceSerializer, QSOSerializer, ShadowCallSerializer, UserSerializer from contest.models import Contest, Band, Frequency, EntryCategory, Reference, QSO, ShadowCall class ContestViewSet(viewsets.ReadOnlyModelViewSet): + """ + Resource to list and view all available contests. Use `current/` to get the current Contest. + """ queryset = Contest.objects.all() serializer_class = ContestSerializer + filterset_fields = ['shortName'] + + def get_object(self): + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + if self.kwargs.get(lookup_url_kwarg) == "current": + obj = Contest.get_current_contest() + self.check_object_permissions(self.request, obj) + else: + obj = super(ContestViewSet, self).get_object() + + return obj class BandViewSet(viewsets.ReadOnlyModelViewSet): queryset = Band.objects.all() serializer_class = BandSerializer + filterset_fields = ['name', 'contest'] class FrequencyViewSet(viewsets.ReadOnlyModelViewSet): queryset = Frequency.objects.all() serializer_class = FrequencySerializer + filterset_fields = ['band', 'channel'] class EntryCategoryViewSet(viewsets.ReadOnlyModelViewSet): queryset = EntryCategory.objects.all() serializer_class = EntryCategorySerializer + filterset_fields = ['name'] class ReferenceViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [IsAdminUser] queryset = Reference.objects.all() serializer_class = ReferenceSerializer + filterset_fields = ['name'] -class QSOViewSet(viewsets.ReadOnlyModelViewSet): +class QSOViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] serializer_class = QSOSerializer + filterset_fields = ['time', 'ownNo', 'band', 'call', 'refStr'] def get_queryset(self): return QSO.objects.filter(owner=self.request.user) + def perform_create(self, serializer): + return serializer.save(owner=self.request.user) + -class UserProfile(viewsets.ReadOnlyModelViewSet): - """Return the currently authenticated user as a single item""" - pass +class UserProfileViewSet(generics.UpdateAPIView, viewsets.GenericViewSet): + permission_classes = [IsAuthenticated] + serializer_class = UserSerializer + + def list(self, request, format=None): + user = request.user + serializer = UserSerializer(user) + return Response(serializer.data) + + def get_queryset(self): + return self.request.user class ShadowCallViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [IsAdminUser] queryset = ShadowCall.objects.all() serializer_class = ShadowCallSerializer + filterset_fields = ['username', 'ref'] diff --git a/contest/models.py b/contest/models.py index 8893efb..fecaa4e 100644 --- a/contest/models.py +++ b/contest/models.py @@ -22,6 +22,11 @@ class Contest(models.Model): def __str__(self): return self.name + @classmethod + def get_current_contest(cls): + return cls.objects.get(id=1) + + class Reference(models.Model): name = models.CharField(max_length=20, unique=True, db_index=True) description = models.TextField() diff --git a/contest/validators.py b/contest/validators.py index 687c0b4..c157429 100644 --- a/contest/validators.py +++ b/contest/validators.py @@ -25,4 +25,3 @@ class CallLogValidator(validators.RegexValidator): 'e.g. DL7BST, DN1BER-1, DL/OE1FOO, DN1FTW-1/p' ) flags = re.ASCII if six.PY3 else 0 - diff --git a/cqtu/settings.py b/cqtu/settings.py index b1760a9..35d3b9a 100644 --- a/cqtu/settings.py +++ b/cqtu/settings.py @@ -42,6 +42,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'crispy_forms', 'rest_framework', + 'django_filters', # local 'contest', @@ -143,3 +144,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap3' MESSAGE_TAGS = { messages.ERROR: 'danger', } + +REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] +} diff --git a/requirements.txt b/requirements.txt index d864032..66fed1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django<1.12 django-crispy-forms django-rest-framework +django-filter