[PATCH 2/5] REST: Use versioning for modified responses

Stephen Finucane stephen at that.guru
Mon Mar 26 05:28:20 AEDT 2018


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 at that.guru>
---
 patchwork/api/base.py               | 27 +++++++++++++++++++++++++++
 patchwork/api/cover.py              |  7 +++++--
 patchwork/api/embedded.py           | 25 ++++++++++++++-----------
 patchwork/api/project.py            |  7 +++++--
 patchwork/tests/api/test_cover.py   | 16 +++++++++++++---
 patchwork/tests/api/test_project.py | 18 +++++++++++++++---
 patchwork/urls.py                   |  2 +-
 7 files changed, 80 insertions(+), 22 deletions(-)

diff --git a/patchwork/api/base.py b/patchwork/api/base.py
index 09b3bef2..8c38d5a1 100644
--- a/patchwork/api/base.py
+++ b/patchwork/api/base.py
@@ -17,12 +17,15 @@
 # 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
diff --git a/patchwork/api/cover.py b/patchwork/api/cover.py
index 10645048..fc7ae97b 100644
--- a/patchwork/api/cover.py
+++ b/patchwork/api/cover.py
@@ -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'},
         }
diff --git a/patchwork/api/embedded.py b/patchwork/api/embedded.py
index 7b5090a0..d79724c4 100644
--- a/patchwork/api/embedded.py
+++ b/patchwork/api/embedded.py
@@ -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')
diff --git a/patchwork/api/project.py b/patchwork/api/project.py
index 597f6056..6f1affad 100644
--- a/patchwork/api/project.py
+++ b/patchwork/api/project.py
@@ -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'},
         }
diff --git a/patchwork/tests/api/test_cover.py b/patchwork/tests/api/test_cover.py
index 6e3d68b8..3135b7e6 100644
--- a/patchwork/tests/api/test_cover.py
+++ b/patchwork/tests/api/test_cover.py
@@ -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 at 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()
diff --git a/patchwork/tests/api/test_project.py b/patchwork/tests/api/test_project.py
index a9e59aa6..129cedb7 100644
--- a/patchwork/tests/api/test_project.py
+++ b/patchwork/tests/api/test_project.py
@@ -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()
diff --git a/patchwork/urls.py b/patchwork/urls.py
index 71934722..0893fe20 100644
--- a/patchwork/urls.py
+++ b/patchwork/urls.py
@@ -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,
-- 
2.14.3



More information about the Patchwork mailing list