]> git.ipfire.org Git - thirdparty/patchwork.git/commitdiff
REST: Use versioning for modified responses
authorStephen Finucane <stephen@that.guru>
Sun, 25 Mar 2018 18:28:20 +0000 (19:28 +0100)
committerStephen Finucane <stephen@that.guru>
Sat, 7 Apr 2018 16:43:26 +0000 (17:43 +0100)
This ensures clients are getting a consistent response if they request
the old version of the API. We do this by way of extensions to the
'HyperlinkedModelSerializer' class rather than duplicating the
serializers as it results in far less duplication. This approach won't
work for a MAJOR version bump but, all going well, it will be a while
before we have to deal with one of these.

The only two fields added since API 1.0 was released, 'cover.mbox' and
'project.subject_match', are handled accordingly.

Signed-off-by: Stephen Finucane <stephen@that.guru>
patchwork/api/base.py
patchwork/api/cover.py
patchwork/api/embedded.py
patchwork/api/project.py
patchwork/tests/api/test_cover.py
patchwork/tests/api/test_project.py
patchwork/urls.py

index 09b3bef2677eca06d150ec3d277f016d6c2fca12..8c38d5a1d5f4920d47e46b6a3aaad4916b3ddd8e 100644 (file)
 # along with Patchwork; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+from distutils.version import StrictVersion
+
 from django.conf import settings
 from django.shortcuts import get_object_or_404
 from rest_framework import permissions
 from rest_framework.pagination import PageNumberPagination
 from rest_framework.response import Response
 from rest_framework.serializers import HyperlinkedIdentityField
+from rest_framework.serializers import HyperlinkedModelSerializer
 
 
 class LinkHeaderPagination(PageNumberPagination):
@@ -91,3 +94,27 @@ class CheckHyperlinkedIdentityField(HyperlinkedIdentityField):
             request=request,
             format=format,
         )
+
+
+class BaseHyperlinkedModelSerializer(HyperlinkedModelSerializer):
+
+    def to_representation(self, instance):
+        data = super(BaseHyperlinkedModelSerializer, self).to_representation(
+            instance)
+
+        request = self.context.get('request')
+        if not request or not request.version:
+            # without version information, we have to assume the latest
+            return data
+
+        requested_version = StrictVersion(request.version)
+
+        for version in getattr(self.Meta, 'versioned_fields', {}):
+            # if the user has requested a version lower that than in which the
+            # field was added, we drop it
+            required_version = StrictVersion(version)
+            if required_version > requested_version:
+                for field in self.Meta.versioned_fields[version]:
+                    data.pop(field)
+
+        return data
index 10645048aaac51affd1e6d1393fd51382a5db48f..fc7ae97ba8f2de523eb96c467f688210e0e70376 100644 (file)
@@ -21,9 +21,9 @@ import email.parser
 
 from rest_framework.generics import ListAPIView
 from rest_framework.generics import RetrieveAPIView
-from rest_framework.serializers import HyperlinkedModelSerializer
 from rest_framework.serializers import SerializerMethodField
 
+from patchwork.api.base import BaseHyperlinkedModelSerializer
 from patchwork.api.filters import CoverLetterFilter
 from patchwork.api.embedded import PersonSerializer
 from patchwork.api.embedded import ProjectSerializer
@@ -31,7 +31,7 @@ from patchwork.api.embedded import SeriesSerializer
 from patchwork.models import CoverLetter
 
 
-class CoverLetterListSerializer(HyperlinkedModelSerializer):
+class CoverLetterListSerializer(BaseHyperlinkedModelSerializer):
 
     project = ProjectSerializer(read_only=True)
     submitter = PersonSerializer(read_only=True)
@@ -47,6 +47,9 @@ class CoverLetterListSerializer(HyperlinkedModelSerializer):
         fields = ('id', 'url', 'project', 'msgid', 'date', 'name', 'submitter',
                   'mbox', 'series')
         read_only_fields = fields
+        versioned_fields = {
+            '1.1': ('mbox', ),
+        }
         extra_kwargs = {
             'url': {'view_name': 'api-cover-detail'},
         }
index 7b5090a03dad086bb8e0dbf08c06dd11db8d0661..d79724c4e5dcebe6046b38135a93a40671bd1c90 100644 (file)
@@ -24,14 +24,14 @@ nested fields.
 """
 
 from rest_framework.serializers import CharField
-from rest_framework.serializers import HyperlinkedModelSerializer
 from rest_framework.serializers import SerializerMethodField
 
+from patchwork.api.base import BaseHyperlinkedModelSerializer
 from patchwork.api.base import CheckHyperlinkedIdentityField
 from patchwork import models
 
 
-class MboxMixin(HyperlinkedModelSerializer):
+class MboxMixin(BaseHyperlinkedModelSerializer):
     """Embed an link to the mbox URL.
 
     This field is just way too useful to leave out of even the embedded
@@ -45,7 +45,7 @@ class MboxMixin(HyperlinkedModelSerializer):
         return request.build_absolute_uri(instance.get_mbox_url())
 
 
-class BundleSerializer(MboxMixin, HyperlinkedModelSerializer):
+class BundleSerializer(MboxMixin, BaseHyperlinkedModelSerializer):
 
     class Meta:
         model = models.Bundle
@@ -56,7 +56,7 @@ class BundleSerializer(MboxMixin, HyperlinkedModelSerializer):
         }
 
 
-class CheckSerializer(HyperlinkedModelSerializer):
+class CheckSerializer(BaseHyperlinkedModelSerializer):
 
     url = CheckHyperlinkedIdentityField('api-check-detail')
 
@@ -75,18 +75,21 @@ class CheckSerializer(HyperlinkedModelSerializer):
         }
 
 
-class CoverLetterSerializer(MboxMixin, HyperlinkedModelSerializer):
+class CoverLetterSerializer(MboxMixin, BaseHyperlinkedModelSerializer):
 
     class Meta:
         model = models.CoverLetter
         fields = ('id', 'url', 'msgid', 'date', 'name', 'mbox')
         read_only_fields = fields
+        versioned_field = {
+            '1.1': ('mbox', ),
+        }
         extra_kwargs = {
             'url': {'view_name': 'api-cover-detail'},
         }
 
 
-class PatchSerializer(MboxMixin, HyperlinkedModelSerializer):
+class PatchSerializer(MboxMixin, BaseHyperlinkedModelSerializer):
 
     class Meta:
         model = models.Patch
@@ -97,7 +100,7 @@ class PatchSerializer(MboxMixin, HyperlinkedModelSerializer):
         }
 
 
-class PersonSerializer(HyperlinkedModelSerializer):
+class PersonSerializer(BaseHyperlinkedModelSerializer):
 
     class Meta:
         model = models.Person
@@ -108,7 +111,7 @@ class PersonSerializer(HyperlinkedModelSerializer):
         }
 
 
-class ProjectSerializer(HyperlinkedModelSerializer):
+class ProjectSerializer(BaseHyperlinkedModelSerializer):
 
     link_name = CharField(max_length=255, source='linkname')
     list_id = CharField(max_length=255, source='listid')
@@ -124,7 +127,7 @@ class ProjectSerializer(HyperlinkedModelSerializer):
         }
 
 
-class SeriesSerializer(MboxMixin, HyperlinkedModelSerializer):
+class SeriesSerializer(MboxMixin, BaseHyperlinkedModelSerializer):
 
     class Meta:
         model = models.Series
@@ -135,7 +138,7 @@ class SeriesSerializer(MboxMixin, HyperlinkedModelSerializer):
         }
 
 
-class UserSerializer(HyperlinkedModelSerializer):
+class UserSerializer(BaseHyperlinkedModelSerializer):
 
     class Meta:
         model = models.User
@@ -146,7 +149,7 @@ class UserSerializer(HyperlinkedModelSerializer):
         }
 
 
-class UserProfileSerializer(HyperlinkedModelSerializer):
+class UserProfileSerializer(BaseHyperlinkedModelSerializer):
 
     username = CharField(source='user.username')
     first_name = CharField(source='user.first_name')
index 597f605674d0c129b700e93d7f3a820a92ff88dd..6f1affad9cbcc6b1f43c3b7cb8edbfb260f3e654 100644 (file)
@@ -21,14 +21,14 @@ from django.shortcuts import get_object_or_404
 from rest_framework.generics import ListAPIView
 from rest_framework.generics import RetrieveUpdateAPIView
 from rest_framework.serializers import CharField
-from rest_framework.serializers import HyperlinkedModelSerializer
 
+from patchwork.api.base import BaseHyperlinkedModelSerializer
 from patchwork.api.base import PatchworkPermission
 from patchwork.api.embedded import UserProfileSerializer
 from patchwork.models import Project
 
 
-class ProjectSerializer(HyperlinkedModelSerializer):
+class ProjectSerializer(BaseHyperlinkedModelSerializer):
 
     link_name = CharField(max_length=255, source='linkname')
     list_id = CharField(max_length=255, source='listid')
@@ -42,6 +42,9 @@ class ProjectSerializer(HyperlinkedModelSerializer):
                   'web_url', 'scm_url', 'webscm_url', 'maintainers',
                   'subject_match')
         read_only_fields = ('name', 'maintainers', 'subject_match')
+        versioned_fields = {
+            '1.1': ('subject_match', ),
+        }
         extra_kwargs = {
             'url': {'view_name': 'api-project-detail'},
         }
index 6e3d68b8e199f65117ebd99ad1aacd162adf4db8..3135b7e66bb3a8402665587ef93ff01a2c95525c 100644 (file)
@@ -42,10 +42,14 @@ class TestCoverLetterAPI(APITestCase):
     fixtures = ['default_tags']
 
     @staticmethod
-    def api_url(item=None):
+    def api_url(item=None, version=None):
+        kwargs = {}
+        if version:
+            kwargs['version'] = version
+
         if item is None:
-            return reverse('api-cover-list')
-        return reverse('api-cover-detail', args=[item])
+            return reverse('api-cover-list', kwargs=kwargs)
+        return reverse('api-cover-detail', args=[item], kwargs=kwargs)
 
     def assertSerialized(self, cover_obj, cover_json):
         self.assertEqual(cover_obj.id, cover_json['id'])
@@ -97,6 +101,12 @@ class TestCoverLetterAPI(APITestCase):
             'submitter': 'test@example.org'})
         self.assertEqual(0, len(resp.data))
 
+        # test old version of API
+        resp = self.client.get(self.api_url(version='1.0'))
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertEqual(1, len(resp.data))
+        self.assertNotIn('mbox', resp.data[0])
+
     def test_detail(self):
         """Validate we can get a specific cover letter."""
         cover_obj = create_cover()
index a9e59aa6fcac3e66543e4e7a5f19d8daf7ca1a3d..129cedb7493fccda46caa6a77f90990e8e196bcb 100644 (file)
@@ -40,16 +40,22 @@ else:
 class TestProjectAPI(APITestCase):
 
     @staticmethod
-    def api_url(item=None):
+    def api_url(item=None, version=None):
+        kwargs = {}
+        if version:
+            kwargs['version'] = version
+
         if item is None:
-            return reverse('api-project-list')
-        return reverse('api-project-detail', args=[item])
+            return reverse('api-project-list', kwargs=kwargs)
+        return reverse('api-project-detail', args=[item], kwargs=kwargs)
 
     def assertSerialized(self, project_obj, project_json):
         self.assertEqual(project_obj.id, project_json['id'])
         self.assertEqual(project_obj.name, project_json['name'])
         self.assertEqual(project_obj.linkname, project_json['link_name'])
         self.assertEqual(project_obj.listid, project_json['list_id'])
+        self.assertEqual(project_obj.subject_match,
+                         project_json['subject_match'])
 
         # nested fields
 
@@ -74,6 +80,12 @@ class TestProjectAPI(APITestCase):
         self.assertEqual(1, len(resp.data))
         self.assertSerialized(project, resp.data[0])
 
+        # test old version of API
+        resp = self.client.get(self.api_url(version='1.0'))
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertEqual(1, len(resp.data))
+        self.assertNotIn('subject_match', resp.data[0])
+
     def test_detail(self):
         """Validate we can get a specific project."""
         project = create_project()
index 719347220a867de4da513f2cd29a981be23c4155..0893fe20a80f3183e9d35e478bdde2c50a6a0691 100644 (file)
@@ -280,7 +280,7 @@ if settings.ENABLE_REST_API:
     ]
 
     urlpatterns += [
-        url(r'^api/(?:(?P<version>(1.0))/)?', include(api_patterns)),
+        url(r'^api/(?:(?P<version>(1.0|1.1))/)?', include(api_patterns)),
 
         # token change
         url(r'^user/generate-token/$', user_views.generate_token,