[PATCH v3 09/16] REST: Make 'Patch.state' editable

Stephen Finucane stephen at that.guru
Sat Nov 26 05:18:28 AEDT 2016


The 'Patch.state' field is exposed by the API, but is not editable.
Tests exist that suggest the field is editable, but they lie as they
don't actually validate the field is changed (it hasn't). Make this
field editable, using a custom field type to allow said user to submit a
string representation of the state rather than an integer id.

This has the side effect of changing the output representation of the
'state' field for the '/patches' endpoint to a slugified representation,
i.e.:

    "state": "under-review",

Instead of:

    "state": "Under review",

The same slugified representation will be used in PATCH requests. This
seems more consistent with how APIs generally do this stuff making it a
"good thing" (TM).

Signed-off-by: Stephen Finucane <stephen at that.guru>
---
v3:
- Rework test to demonstrate the prior non-editability of the field
  (Daniel Axtens)
- Use less confusing variable names in a list comprehensions (Andy
  Doan)
---
 patchwork/api/base.py            |  5 +++++
 patchwork/api/patch.py           | 30 +++++++++++++++++++++++++-----
 patchwork/tests/test_rest_api.py |  8 ++++++--
 3 files changed, 36 insertions(+), 7 deletions(-)

diff --git a/patchwork/api/base.py b/patchwork/api/base.py
index dbd8148..13a8432 100644
--- a/patchwork/api/base.py
+++ b/patchwork/api/base.py
@@ -23,6 +23,11 @@ from rest_framework import permissions
 from rest_framework.pagination import PageNumberPagination
 from rest_framework.response import Response
 
+from patchwork.models import State
+
+STATE_CHOICES = ['-'.join(x.name.lower().split(' '))
+                 for x in State.objects.all()]
+
 
 class LinkHeaderPagination(PageNumberPagination):
     """Provide pagination based on rfc5988.
diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py
index 8427450..f818f20 100644
--- a/patchwork/api/patch.py
+++ b/patchwork/api/patch.py
@@ -20,25 +20,45 @@
 import email.parser
 
 from django.core.urlresolvers import reverse
-from rest_framework.serializers import HyperlinkedModelSerializer
+from rest_framework.exceptions import ValidationError
 from rest_framework.generics import ListAPIView
 from rest_framework.generics import RetrieveUpdateAPIView
+from rest_framework.serializers import ChoiceField
+from rest_framework.serializers import HyperlinkedModelSerializer
 from rest_framework.serializers import SerializerMethodField
 
 from patchwork.api.base import PatchworkPermission
+from patchwork.api.base import STATE_CHOICES
 from patchwork.models import Patch
+from patchwork.models import State
+
+
+class StateField(ChoiceField):
+    """Avoid the need for a state endpoint."""
+
+    def __init__(self, *args, **kwargs):
+        kwargs['choices'] = STATE_CHOICES
+        super(StateField, self).__init__(*args, **kwargs)
+
+    def to_internal_value(self, data):
+        data = ' '.join(data.split('-'))
+        try:
+            return State.objects.get(name__iexact=data)
+        except State.DoesNotExist:
+            raise ValidationError('Invalid state. Expected one of: %s ' %
+                                  ', '.join(STATE_CHOICES))
+
+    def to_representation(self, obj):
+        return '-'.join(obj.name.lower().split())
 
 
 class PatchListSerializer(HyperlinkedModelSerializer):
     mbox = SerializerMethodField()
-    state = SerializerMethodField()
+    state = StateField()
     tags = SerializerMethodField()
     check = SerializerMethodField()
     checks = SerializerMethodField()
 
-    def get_state(self, instance):
-        return instance.state.name
-
     def get_mbox(self, instance):
         request = self.context.get('request')
         return request.build_absolute_uri(instance.get_mbox_url())
diff --git a/patchwork/tests/test_rest_api.py b/patchwork/tests/test_rest_api.py
index 469fd26..d764945 100644
--- a/patchwork/tests/test_rest_api.py
+++ b/patchwork/tests/test_rest_api.py
@@ -31,6 +31,7 @@ from patchwork.tests.utils import create_maintainer
 from patchwork.tests.utils import create_patch
 from patchwork.tests.utils import create_person
 from patchwork.tests.utils import create_project
+from patchwork.tests.utils import create_state
 from patchwork.tests.utils import create_user
 
 if settings.ENABLE_REST_API:
@@ -368,19 +369,22 @@ class TestPatchAPI(APITestCase):
         # A maintainer can update
         project = create_project()
         patch = create_patch(project=project)
+        state = create_state()
         user = create_maintainer(project)
         self.client.force_authenticate(user=user)
 
+        self.assertNotEqual(Patch.objects.get(id=patch.id).state, state)
         resp = self.client.patch(self.api_url(patch.id),
-                                 {'state': 2})
+                                 {'state': state.name})
         self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertEqual(Patch.objects.get(id=patch.id).state, state)
 
         # A normal user can't
         user = create_user()
         self.client.force_authenticate(user=user)
 
         resp = self.client.patch(self.api_url(patch.id),
-                                 {'state': 2})
+                                 {'state': state.name})
         self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code)
 
     def test_delete(self):
-- 
2.7.4



More information about the Patchwork mailing list