aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar SebastiaanZ <[email protected]>2019-07-03 21:25:43 +0200
committerGravatar SebastiaanZ <[email protected]>2019-07-03 21:25:43 +0200
commit300c912d108ae2e35cb90983844ae5b785505d2e (patch)
tree97d0cb1ee0d03ea2db5fedfe81b37dea5e2cfdd4
parentRemoving unused import (diff)
Move end nomination operation to PATCH endpoint
-rw-r--r--pydis_site/apps/api/tests/test_nominations.py52
-rw-r--r--pydis_site/apps/api/viewsets/bot/nomination.py108
2 files changed, 79 insertions, 81 deletions
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
index 206dc65b..add5a7e4 100644
--- a/pydis_site/apps/api/tests/test_nominations.py
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -240,7 +240,7 @@ class NominationTests(APISubdomainTestCase):
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
- 'end_reason': ["An active nomination can't have an unnominate reason."]
+ 'end_reason': ["An active nomination can't have an end reason."]
})
def test_returns_200_update_reason_on_inactive(self):
@@ -274,9 +274,10 @@ class NominationTests(APISubdomainTestCase):
host='api'
)
data = {
+ 'active': False,
'end_reason': 'He started playing jazz'
}
- response = self.client.put(url, data=data)
+ response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 200)
nomination = Nomination.objects.get(id=response.json()['id'])
@@ -296,12 +297,13 @@ class NominationTests(APISubdomainTestCase):
host='api'
)
data = {
- 'reason': 'Why does a whale have feet?'
+ 'active': False,
+ 'reason': 'Why does a whale have feet?',
}
- response = self.client.put(url, data=data)
+ response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
- 'reason': ['This field cannot be set at end_nomination.']
+ 'reason': ['This field cannot be set when ending a nomination.']
})
def test_returns_400_on_missing_end_reason_end_nomination(self):
@@ -310,28 +312,30 @@ class NominationTests(APISubdomainTestCase):
args=(self.active_nomination.id,),
host='api'
)
- data = {}
+ data = {
+ 'active': False,
+ }
- response = self.client.put(url, data=data)
+ response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
'end_reason': ['This field is required when ending a nomination.']
})
- def test_returns_400_on_ending_inactive_nomination(self):
+ def test_returns_400_on_invalid_use_of_active(self):
url = reverse(
'bot:nomination-detail',
args=(self.inactive_nomination.id,),
host='api'
)
data = {
- 'end_reason': 'He started playing jazz'
+ 'active': False,
}
- response = self.client.put(url, data=data)
+ response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
- 'active': ['A nomination must be active to be ended.']
+ 'active': ['This field can only be used to end a nomination']
})
def test_returns_404_on_get_unknown_nomination(self):
@@ -360,23 +364,6 @@ class NominationTests(APISubdomainTestCase):
"detail": "Not found."
})
- def test_returns_404_on_end_unknown_nomination(self):
- url = reverse(
- 'bot:nomination-detail',
- args=(9999,),
- host='api'
- )
-
- data = {
- 'end_reason': 'He started playing jazz'
- }
-
- response = self.client.put(url, data=data)
- self.assertEqual(response.status_code, 404)
- self.assertEqual(response.json(), {
- "detail": "Not found."
- })
-
def test_returns_405_on_list_put(self):
url = reverse('bot:nomination-list', host='api')
@@ -422,6 +409,15 @@ class NominationTests(APISubdomainTestCase):
"detail": "Method \"DELETE\" not allowed."
})
+ def test_returns_405_on_detail_put(self):
+ url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api')
+
+ response = self.client.put(url, data={})
+ self.assertEqual(response.status_code, 405)
+ self.assertEqual(response.json(), {
+ "detail": "Method \"PUT\" not allowed."
+ })
+
def test_filter_returns_0_objects_unknown_user__id(self):
url = reverse('bot:nomination-list', host='api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index 2eec6275..fad16aa5 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -67,7 +67,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
... }
### Status codes
- - 200: returned on succes
+ - 200: returned on success
- 404: returned if a nomination with the given `id` could not be found
### POST /bot/nominations
@@ -84,7 +84,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
... 'user': 409107086526644234
... }
- ### Response format
+ #### Response format
See `GET /bot/nominations/<id:int>`
#### Status codes
@@ -95,53 +95,59 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
- 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 `end_reason` may be updated.
+ Update or end the nomination with the given `id` and return the updated nomination.
+
+ The PATCH route can be used for three distinct operations:
+ 1. Updating the `reason` of `active` nomination;
+ 2. Ending an `active` nomination;
+ 3. Updating the `end_reason` or `reason` field of an `inactive` nomination.
+
+ While the response format and status codes are the same for all three operations (see
+ below), the request bodies vary depending on the operation. For all operations holds
+ that providing other valid fields is not allowed and invalid fields are ignored.
+
+ ### 1. Updating the `reason` of `active` nomination
#### Request body
>>> {
... 'reason': 'He would make a great helper',
- ... 'end_reason': 'He needs some time to mature his Python knowledge'
... }
- ### Response format
+ #### Response format
See `GET /bot/nominations/<id:int>`
- ## Status codes
+ #### 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.
+ ### 2. Ending an `active` nomination
- The `end_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 `ended_at` field.
+ #### Request body
+ >>> {
+ ... 'active': False
+ ... 'end_reason': 'They've been added to the Helpers team',
+ ... }
+
+ See operation 1 for the response format and status codes.
+
+ ### 3. Updating the `end_reason` or `reason` field of an `inactive` nomination.
#### Request body
>>> {
- ... 'end_reason': 'He needs some time to mature his Python knowledge'
+ ... 'reason': 'Updated reason for this nomination',
+ ... 'end_reason': 'Updated end_reason for this nomination',
... }
- ### Response format
- See `GET /bot/nominations/<id:int>`
+ Note: The request body may contain either or both fields.
- #### Status codes
- - 200: returned on success
- - 400: returned on failure for the following reasons:
- - `end_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
+ See operation 1 for the response format and status codes.
"""
serializer_class = NominationSerializer
queryset = Nomination.objects.all()
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
filter_fields = ('user__id', 'actor__id', 'active')
- frozen_fields = ('id', 'actor', 'inserted_at', 'user', 'ended_at', 'active')
+ frozen_fields = ('id', 'actor', 'inserted_at', 'user', 'ended_at')
frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at')
def create(self, request, *args, **kwargs):
@@ -180,43 +186,39 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
raise ValidationError({field: ['This field cannot be updated.']})
instance = self.get_object()
-
- if instance.active and request.data.get('end_reason'):
- raise ValidationError(
- {'end_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()
- return Response(serializer.data)
+ data = serializer.validated_data
- def update(self, request, *args, **kwargs):
- """
- DRF action for ending an active nomination.
+ # There are three distinct PATCH scenarios we need to validate.
+ if instance.active and 'active' not in data:
+ # 1. We're updating an active nomination without ending it.
+ if request.data.get('end_reason'):
+ raise ValidationError(
+ {'end_reason': ["An active nomination can't have an end reason."]}
+ )
- 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 != "end_reason":
- raise ValidationError({field: ['This field cannot be set at end_nomination.']})
+ elif instance.active and not data.get('active'):
+ # 2. We're ending an active nomination.
+ if 'reason' in data:
+ raise ValidationError(
+ {'reason': ['This field cannot be set when ending a nomination.']}
+ )
+
+ if 'end_reason' not in request.data:
+ raise ValidationError(
+ {'end_reason': ['This field is required when ending a nomination.']}
+ )
+
+ instance.ended_at = timezone.now()
- if "end_reason" not in request.data:
+ elif 'active' in data:
+ # 3. The `active` field is only allowed when ending a nomination.
raise ValidationError(
- {'end_reason': ['This field is required when ending a nomination.']}
+ {'active': ['This field can only be used to end a nomination']}
)
- instance = self.get_object()
- if not instance.active:
- raise ValidationError({'active': ['A nomination must be active to be ended.']})
-
- serializer = self.get_serializer(instance, data=request.data, partial=True)
- serializer.is_valid(raise_exception=True)
- instance.active = False
- instance.ended_at = timezone.now()
serializer.save()
- instance.save()
return Response(serializer.data)