aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site
diff options
context:
space:
mode:
authorGravatar SebastiaanZ <[email protected]>2019-04-27 17:30:20 +0200
committerGravatar SebastiaanZ <[email protected]>2019-04-27 17:30:20 +0200
commitb9db880d6229ddb7bf793bc8ca50b37e471956a7 (patch)
tree1f77c73ca64e3da992aa00946e54c26e4faee594 /pydis_site
parentAdding 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.py14
-rw-r--r--pydis_site/apps/api/viewsets/bot/nomination.py173
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()