diff options
author | 2019-04-27 17:30:20 +0200 | |
---|---|---|
committer | 2019-04-27 17:30:20 +0200 | |
commit | b9db880d6229ddb7bf793bc8ca50b37e471956a7 (patch) | |
tree | 1f77c73ca64e3da992aa00946e54c26e4faee594 /pydis_site | |
parent | Adding CreationTests to test_nomination and making related changes in the Vie... (diff) |
Finalizing and adding documentation to the nomination endpoint and simplifying the nomination serializer
Diffstat (limited to 'pydis_site')
-rw-r--r-- | pydis_site/apps/api/serializers.py | 14 | ||||
-rw-r--r-- | pydis_site/apps/api/viewsets/bot/nomination.py | 173 |
2 files changed, 161 insertions, 26 deletions
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index d58f1fa7..abf49393 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -270,9 +270,6 @@ class UserSerializer(BulkSerializerMixin, ModelSerializer): class NominationSerializer(ModelSerializer): """A class providing (de-)serialization of `Nomination` instances.""" - actor = PrimaryKeyRelatedField(queryset=User.objects.all()) - user = PrimaryKeyRelatedField(queryset=User.objects.all()) - class Meta: """Metadata defined for the Django REST Framework.""" @@ -280,14 +277,3 @@ class NominationSerializer(ModelSerializer): fields = ( 'id', 'active', 'actor', 'reason', 'user', 'inserted_at', 'unnominate_reason', 'unwatched_at') - depth = 1 - - def validate(self, attrs): - active = attrs.get("active") - - unnominate_reason = attrs.get("unnominate_reason") - if active and unnominate_reason: - raise ValidationError( - {'unnominate_reason': "An active nomination can't have an unnominate reason"} - ) - return attrs diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 1059ffcd..888f049e 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -1,49 +1,192 @@ +from collections import ChainMap + from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework.mixins import ( + CreateModelMixin, + ListModelMixin, + RetrieveModelMixin, +) from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import GenericViewSet from pydis_site.apps.api.models.bot import Nomination from pydis_site.apps.api.serializers import NominationSerializer -class NominationViewSet(ModelViewSet): - """View providing CRUD operations on helper nominations done through the bot.""" - +class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): + """ + View providing CRUD operations on helper nominations done through the bot. + + ## Routes + ### GET /bot/nominations + Retrieve all nominations. + May be filtered and ordered by the query parameters. + + #### Query parameters + - **active** `bool`: whether the nomination is still active + - **actor__id** `int`: snowflake of the user who nominated the user + - **user__id** `int`: snowflake of the user who received the nomination + - **ordering** `str`: comma-separated sequence of fields to order the returned results + + Invalid query parameters are ignored. + + #### Response format + [ + { + 'id': 1, + 'active': false, + 'actor': 336843820513755157, + 'reason': 'They know how to explain difficult concepts', + 'user': 336843820513755157, + 'inserted_at': '2019-04-25T14:02:37.775587Z', + 'unnominate_reason': 'They were helpered after a staff-vote', + 'unwatched_at': '2019-04-26T15:12:22.123587Z' + } + ] + + #### Status codes + - 200: returned on success + + ### GET /bot/nominations/<id:int> + Retrieve a single nomination by ID. + + ### Response format + { + 'id': 1, + 'active': true, + 'actor': 336843820513755157, + 'reason': 'They know how to explain difficult concepts', + 'user': 336843820513755157, + 'inserted_at': '2019-04-25T14:02:37.775587Z', + 'unnominate_reason': 'They were helpered after a staff-vote', + 'unwatched_at': '2019-04-26T15:12:22.123587Z' + } + + ### Status codes + - 200: returned on succes + - 404: returned if an nomination with the given `id` could not be found + + ### POST /bot/nominations + Create a new, active nomination returns the created nominations. + The `user`, `reason` and `actor` fields are required and the `user` + and `actor` need to know by the site. Providing other valid fields + is not allowed and invalid fields are ignored. A `user` is only + allowed one active nomination at a time. + + #### Request body + { + 'actor': 409107086526644234 + 'reason': 'He would make a great helper', + 'user': 409107086526644234 + } + + ### Response format + See `GET /bot/nominations/<id:int>` + + #### Status codes + - 201: returned on success + - 400: returned on failure for one of the following reasons: + - A user already has an active nomination; + - The `user` or `actor` are unknown to the site; + - The request contained a field that cannot be set at creation. + + ### PATCH /bot/nominations/<id:int> + Update the nomination with the given `id` and return the updated nomination. + For active nominations, only the `reason` may be updated; for inactive + nominations, both the `reason` and `unnominate_reason` may be updated. + + #### Request body + { + 'reason': 'He would make a great helper', + 'unnominate_reason': 'He needs some time to mature his Python knowledge' + } + + ### Response format + See `GET /bot/nominations/<id:int>` + + ## Status codes + - 200: returned on success + - 400: if a field in the request body is invalid or disallowed + - 404: if an infraction with the given `id` could not be found + + ### PATCH /bot/nominations/<id:int>/end_nomination + Ends an active nomination and returns the updated nomination. + + The `unnominate_reason` field is the only allowed and required field + for this operation. The nomination will automatically be marked as + `active = false` and the datetime of this operation will be added to + the `unwatched_at` field. + + #### Request body + { + 'unnominate_reason': 'He needs some time to mature his Python knowledge' + } + + ### Response format + See `GET /bot/nominations/<id:int>` + + #### Status codes + - 200: returned on success + - 400: returned on failure for the following reasons: + - `unnominate_reason` is missing from the request body; + - Any other field is present in the request body; + - The nomination was already inactiive. + - 404: if an infraction with the given `id` could not be found + """ serializer_class = NominationSerializer - queryset = Nomination.objects.prefetch_related('actor', 'user') + queryset = Nomination.objects.all() filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) filter_fields = ('user__id', 'actor__id', 'active') frozen_fields = ('id', 'actor', 'inserted_at', 'user', 'unwatched_at', 'active') frozen_on_create = ('unwatched_at', 'unnominate_reason', 'active', 'inserted_at') def create(self, request, *args, **kwargs): + """ + DRF method for creating a Nomination. + + Called by the Django Rest Framework in response to the corresponding HTTP request. + """ for field in request.data: if field in self.frozen_on_create: raise ValidationError({field: ['This field cannot be set at creation.']}) - serializer = self.get_serializer(data=request.data) + user_id = request.data.get("user") + if Nomination.objects.filter(active=True, user__id=user_id): + raise ValidationError({'active': ['There can only be one active nomination.']}) + + serializer = self.get_serializer( + data=ChainMap( + request.data, + {"active": True} + ) + ) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - def update(self, request, *args, **kwargs): + def partial_update(self, request, *args, **kwargs): """ DRF method for updating a Nomination. Called by the Django Rest Framework in response to the corresponding HTTP request. """ - for field in request.data: if field in self.frozen_fields: raise ValidationError({field: ['This field cannot be updated.']}) instance = self.get_object() + + if instance.active and request.data.get('unnominate_reason'): + raise ValidationError( + {'unnominate_reason': ["An active nomination can't have an unnominate reason."]} + ) + serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() @@ -52,23 +195,29 @@ class NominationViewSet(ModelViewSet): @action(detail=True, methods=['patch']) def end_nomination(self, request, pk=None): + """ + DRF action for ending an active nomination. + + Creates an API endpoint /bot/nominations/{id}/end_nomination to end a nomination. See + the class docstring for documentation. + """ for field in request.data: if field != "unnominate_reason": - raise ValidationError({field: ['This field cannot be set at end_nomination']}) + raise ValidationError({field: ['This field cannot be set at end_nomination.']}) if "unnominate_reason" not in request.data: raise ValidationError( - {'unnominate_reason': ['This field is required when ending a nomination']} + {'unnominate_reason': ['This field is required when ending a nomination.']} ) instance = self.get_object() if not instance.active: raise ValidationError({'active': ['A nomination must be active to be ended.']}) - instance.active = False - instance.unwatched_at = timezone.now() serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) + instance.active = False + instance.unwatched_at = timezone.now() serializer.save() instance.save() |