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

Denis Laxalde denis at laxalde.org
Fri Jan 27 01:40:19 AEDT 2017


Hi,

Stephen Finucane wrote:
> 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()]
> +

If I understand this code correctly, this "STATE_CHOICES" remains
constant from when the module gets imported by the server process. It
means that actual choices in the "StateField" below will not match
database data if one modifies the State table.

I don't know how to fix this properly. The issue can be seen with the
following modification in tests:

diff --git a/patchwork/tests/test_rest_api.py 
b/patchwork/tests/test_rest_api.py
index 88b7163..70ef1fe 100644
--- a/patchwork/tests/test_rest_api.py
+++ b/patchwork/tests/test_rest_api.py
@@ -398,6 +398,12 @@ class TestPatchAPI(APITestCase):
          self.assertEqual(status.HTTP_200_OK, resp.status_code)
          self.assertEqual(Patch.objects.get(id=patch.id).state, state)

+        # state not in choices
+        resp = self.client.patch(self.api_url(patch.id), {'state': 
'unknown'})
+        self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code)
+        self.assertEqual(['Invalid state. Expected one of: state_0, 
state_1 '],
+                         resp.json()['state'])
+
      def test_delete(self):
          """Ensure deletions are always rejected."""
          project = create_project()


(This patch was applied as a2993505a132764226d85ad9375abc930725d5f5.)

>  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):
>



More information about the Patchwork mailing list