[PATCH 4/6] REST: Embed nested element bodies instead of URLs
Stephen Finucane
stephen at that.guru
Tue May 16 09:14:33 AEST 2017
In developing a client for the Patchwork REST API, git-pw, it was noted
that it should be possible to embed some information about nested
resources in order to prevent the need for additional requests [1]. It
was seen that this would be particularly beneficial for list operations,
where each element in the N sized list could theoretically require an
additional request for each of the M nested fields, resulting in N * (M
+ 1) total requests.
Upon experimenting with the 2.0 RC1 API, this optimization was found to
be less of a nice-to-have (and possibly something for the 2.1 release)
and more of a must-have, particularly once one took network latency for
each request into account. During testing with 'git-pw', simple list
operations were found to take an average of 31 requests per operation,
of which only one for was the resource endpoint itself ('GET
/api/series'). As each of these requests took ~2 seconds a piece,
listing was essentially broken.
While local caching could be used to offset some of this demand, this
will result in (a) significantly larger, more complex clients or (b)
instances that strain under the load of dumb clients making multiple
requests per operation. Instead, the server should be smarter about
embedding the data that would actually be required by clients.
Resolve the issue by embedding summarized versions of various nested
fields instead of merely linking to them. Nesting is only a single level
deep, to avoid large/complex database queries and with the expectation
that only these basic fields (resource names, dates, etc.) would be
required. These summary serializers are kept in their own module, to
encourage consistent results throughout the API and to prevent circular
import errors.
This will have the side effect of slightly increasing load on the server
due to the additional serialization required. However, this load is
largely mitigated through the avoidance of deeper nesting as noted
above. In addition, any increase in load seen will be a fraction of the
demand that repeat requests will incur. While it would be possible to
make nesting optional (by way of an 'embed' or 'expand' parameter), it
is expected that this would be an atypical request and would result in
far more complicated serialization code.
[1] https://github.com/stephenfin/git-pw/blob/21e0e593/git_pw/patch.py#L88-L89
Signed-off-by: Stephen Finucane <stephen at that.guru>
---
patchwork/api/base.py | 19 +++++
patchwork/api/bundle.py | 9 ++-
patchwork/api/check.py | 28 ++-----
patchwork/api/cover.py | 17 ++--
patchwork/api/embedded.py | 162 +++++++++++++++++++++++++++++++++++++++
patchwork/api/event.py | 36 ++++-----
patchwork/api/patch.py | 19 +++--
patchwork/api/person.py | 5 +-
patchwork/api/project.py | 8 +-
patchwork/api/series.py | 12 ++-
patchwork/tests/test_rest_api.py | 55 ++++++++-----
11 files changed, 279 insertions(+), 91 deletions(-)
create mode 100644 patchwork/api/embedded.py
diff --git a/patchwork/api/base.py b/patchwork/api/base.py
index 0797990..09b3bef 100644
--- a/patchwork/api/base.py
+++ b/patchwork/api/base.py
@@ -22,6 +22,7 @@ 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
class LinkHeaderPagination(PageNumberPagination):
@@ -72,3 +73,21 @@ class MultipleFieldLookupMixin(object):
filter_kwargs[field_name] = self.kwargs[field]
return get_object_or_404(queryset, **filter_kwargs)
+
+
+class CheckHyperlinkedIdentityField(HyperlinkedIdentityField):
+
+ def get_url(self, obj, view_name, request, format):
+ # Unsaved objects will not yet have a valid URL.
+ if obj.pk is None:
+ return None
+
+ return self.reverse(
+ view_name,
+ kwargs={
+ 'patch_id': obj.patch.id,
+ 'check_id': obj.id,
+ },
+ request=request,
+ format=format,
+ )
diff --git a/patchwork/api/bundle.py b/patchwork/api/bundle.py
index 5fa79b8..4f21ae3 100644
--- a/patchwork/api/bundle.py
+++ b/patchwork/api/bundle.py
@@ -25,12 +25,18 @@ from rest_framework.serializers import SerializerMethodField
from patchwork.api.base import PatchworkPermission
from patchwork.api.filters import BundleFilter
+from patchwork.api.embedded import PatchSerializer
+from patchwork.api.embedded import ProjectSerializer
+from patchwork.api.embedded import UserSerializer
from patchwork.models import Bundle
class BundleSerializer(HyperlinkedModelSerializer):
+ project = ProjectSerializer(read_only=True)
mbox = SerializerMethodField()
+ owner = UserSerializer(read_only=True)
+ patches = PatchSerializer(many=True, read_only=True)
def get_mbox(self, instance):
request = self.context.get('request')
@@ -43,9 +49,6 @@ class BundleSerializer(HyperlinkedModelSerializer):
read_only_fields = ('owner', 'patches', 'mbox')
extra_kwargs = {
'url': {'view_name': 'api-bundle-detail'},
- 'project': {'view_name': 'api-project-detail'},
- 'owner': {'view_name': 'api-user-detail'},
- 'patches': {'view_name': 'api-patch-detail'},
}
diff --git a/patchwork/api/check.py b/patchwork/api/check.py
index d368265..66b4601 100644
--- a/patchwork/api/check.py
+++ b/patchwork/api/check.py
@@ -20,13 +20,13 @@
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import ListCreateAPIView
from rest_framework.generics import RetrieveAPIView
-from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import CurrentUserDefault
from rest_framework.serializers import HiddenField
from rest_framework.serializers import HyperlinkedModelSerializer
-from rest_framework.serializers import HyperlinkedIdentityField
+from patchwork.api.base import CheckHyperlinkedIdentityField
from patchwork.api.base import MultipleFieldLookupMixin
+from patchwork.api.embedded import UserSerializer
from patchwork.api.filters import CheckFilter
from patchwork.models import Check
from patchwork.models import Patch
@@ -40,29 +40,11 @@ class CurrentPatchDefault(object):
return self.patch
-class CheckHyperlinkedIdentityField(HyperlinkedIdentityField):
-
- def get_url(self, obj, view_name, request, format):
- # Unsaved objects will not yet have a valid URL.
- if obj.pk is None:
- return None
-
- return self.reverse(
- view_name,
- kwargs={
- 'patch_id': obj.patch.id,
- 'check_id': obj.id,
- },
- request=request,
- format=format,
- )
-
-
class CheckSerializer(HyperlinkedModelSerializer):
- user = HyperlinkedRelatedField(
- 'api-user-detail', read_only=True, default=CurrentUserDefault())
- patch = HiddenField(default=CurrentPatchDefault())
+
url = CheckHyperlinkedIdentityField('api-check-detail')
+ patch = HiddenField(default=CurrentPatchDefault())
+ user = UserSerializer(read_only=True, default=CurrentUserDefault())
def run_validation(self, data):
for val, label in Check.STATE_CHOICES:
diff --git a/patchwork/api/cover.py b/patchwork/api/cover.py
index e45680b..797cadf 100644
--- a/patchwork/api/cover.py
+++ b/patchwork/api/cover.py
@@ -23,18 +23,20 @@ import django
from rest_framework.generics import ListAPIView
from rest_framework.generics import RetrieveAPIView
from rest_framework.serializers import HyperlinkedModelSerializer
-from rest_framework.serializers import HyperlinkedRelatedField
from rest_framework.serializers import SerializerMethodField
from patchwork.api.filters import CoverLetterFilter
+from patchwork.api.embedded import PersonSerializer
+from patchwork.api.embedded import ProjectSerializer
+from patchwork.api.embedded import SeriesSerializer
from patchwork.models import CoverLetter
class CoverLetterListSerializer(HyperlinkedModelSerializer):
- series = HyperlinkedRelatedField(
- many=True,
- read_only=True,
- view_name='api-series-detail')
+
+ project = ProjectSerializer(read_only=True)
+ submitter = PersonSerializer(read_only=True)
+ series = SeriesSerializer(many=True, read_only=True)
class Meta:
model = CoverLetter
@@ -43,8 +45,6 @@ class CoverLetterListSerializer(HyperlinkedModelSerializer):
read_only_fields = fields
extra_kwargs = {
'url': {'view_name': 'api-cover-detail'},
- 'project': {'view_name': 'api-project-detail'},
- 'submitter': {'view_name': 'api-person-detail'},
}
@@ -58,8 +58,7 @@ class CoverLetterDetailSerializer(CoverLetterListSerializer):
class Meta:
model = CoverLetter
fields = CoverLetterListSerializer.Meta.fields + ('headers', 'content')
- read_only_fields = CoverLetterListSerializer.Meta.read_only_fields + (
- 'headers', 'content')
+ read_only_fields = fields
extra_kwargs = CoverLetterListSerializer.Meta.extra_kwargs
diff --git a/patchwork/api/embedded.py b/patchwork/api/embedded.py
new file mode 100644
index 0000000..122422a
--- /dev/null
+++ b/patchwork/api/embedded.py
@@ -0,0 +1,162 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2017 Stephen Finucane <stephen at that.guru>
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""Serializers for embedded use.
+
+A collection of serializers. None of the serializers here should reference
+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 CheckHyperlinkedIdentityField
+from patchwork import models
+
+
+class MboxMixin(HyperlinkedModelSerializer):
+ """Embed an link to the mbox URL.
+
+ This field is just way too useful to leave out of even the embedded
+ serialization.
+ """
+
+ mbox = SerializerMethodField()
+
+ def get_mbox(self, instance):
+ request = self.context.get('request')
+ return request.build_absolute_uri(instance.get_mbox_url())
+
+
+class BundleSerializer(MboxMixin, HyperlinkedModelSerializer):
+
+ class Meta:
+ model = models.Bundle
+ fields = ('id', 'url', 'name', 'mbox')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-bundle-detail'},
+ }
+
+
+class CheckSerializer(HyperlinkedModelSerializer):
+
+ url = CheckHyperlinkedIdentityField('api-check-detail')
+
+ def to_representation(self, instance):
+ data = super(CheckSerializer, self).to_representation(instance)
+ data['state'] = instance.get_state_display()
+ return data
+
+ class Meta:
+ model = models.Check
+ fields = ('id', 'url', 'date', 'state', 'target_url', 'context')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-check-detail'},
+
+ }
+
+
+class CoverLetterSerializer(HyperlinkedModelSerializer):
+
+ class Meta:
+ model = models.CoverLetter
+ fields = ('id', 'url', 'msgid', 'date', 'name')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-cover-detail'},
+ }
+
+
+class PatchSerializer(MboxMixin, HyperlinkedModelSerializer):
+
+ class Meta:
+ model = models.Patch
+ fields = ('id', 'url', 'msgid', 'date', 'name', 'mbox')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-patch-detail'},
+ }
+
+
+class PersonSerializer(HyperlinkedModelSerializer):
+
+ class Meta:
+ model = models.Person
+ fields = ('id', 'url', 'name', 'email')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-person-detail'},
+ }
+
+
+class ProjectSerializer(HyperlinkedModelSerializer):
+
+ link_name = CharField(max_length=255, source='linkname')
+ list_id = CharField(max_length=255, source='listid')
+ list_email = CharField(max_length=200, source='listemail')
+
+ class Meta:
+ model = models.Project
+ fields = ('id', 'url', 'name', 'link_name', 'list_id', 'list_email',
+ 'web_url', 'scm_url', 'webscm_url')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-project-detail'},
+ }
+
+
+class SeriesSerializer(MboxMixin, HyperlinkedModelSerializer):
+
+ class Meta:
+ model = models.Series
+ fields = ('id', 'url', 'date', 'name', 'version', 'mbox')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-series-detail'},
+ }
+
+
+class UserSerializer(HyperlinkedModelSerializer):
+
+ class Meta:
+ model = models.User
+ fields = ('id', 'url', 'username', 'first_name', 'last_name', 'email')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-user-detail'},
+ }
+
+
+class UserProfileSerializer(HyperlinkedModelSerializer):
+
+ username = CharField(source='user.username')
+ first_name = CharField(source='user.first_name')
+ last_name = CharField(source='user.last_name')
+ email = CharField(source='user.email')
+
+ class Meta:
+ model = models.UserProfile
+ fields = ('id', 'url', 'username', 'first_name', 'last_name', 'email')
+ read_only_fields = fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-user-detail'},
+ }
diff --git a/patchwork/api/event.py b/patchwork/api/event.py
index a060022..cc9270a 100644
--- a/patchwork/api/event.py
+++ b/patchwork/api/event.py
@@ -20,20 +20,32 @@
from collections import OrderedDict
from rest_framework.generics import ListAPIView
-from rest_framework.reverse import reverse
-from rest_framework.serializers import HyperlinkedModelSerializer
+from rest_framework.serializers import ModelSerializer
from rest_framework.serializers import SerializerMethodField
+from patchwork.api.embedded import CheckSerializer
+from patchwork.api.embedded import CoverLetterSerializer
+from patchwork.api.embedded import PatchSerializer
+from patchwork.api.embedded import ProjectSerializer
+from patchwork.api.embedded import SeriesSerializer
+from patchwork.api.embedded import UserSerializer
from patchwork.api.filters import EventFilter
from patchwork.api.patch import StateField
from patchwork.models import Event
-class EventSerializer(HyperlinkedModelSerializer):
+class EventSerializer(ModelSerializer):
+ project = ProjectSerializer(read_only=True)
+ patch = PatchSerializer(read_only=True)
+ series = SeriesSerializer(read_only=True)
+ cover = CoverLetterSerializer(read_only=True)
previous_state = StateField()
current_state = StateField()
+ previous_delegate = UserSerializer()
+ current_delegate = UserSerializer()
created_check = SerializerMethodField()
+ created_check = CheckSerializer()
_category_map = {
Event.CATEGORY_COVER_CREATED: ['cover'],
@@ -48,15 +60,6 @@ class EventSerializer(HyperlinkedModelSerializer):
Event.CATEGORY_SERIES_COMPLETED: ['series'],
}
- def get_created_check(self, instance):
- if not instance.patch or not instance.created_check:
- return
-
- return self.context.get('request').build_absolute_uri(
- reverse('api-check-detail', kwargs={
- 'patch_id': instance.patch.id,
- 'check_id': instance.created_check.id}))
-
def to_representation(self, instance):
data = super(EventSerializer, self).to_representation(instance)
payload = OrderedDict()
@@ -80,15 +83,6 @@ class EventSerializer(HyperlinkedModelSerializer):
'cover', 'previous_state', 'current_state',
'previous_delegate', 'current_delegate', 'created_check')
read_only_fields = fields
- extra_kwargs = {
- 'project': {'view_name': 'api-project-detail'},
- 'patch': {'view_name': 'api-patch-detail'},
- 'series': {'view_name': 'api-series-detail'},
- 'cover': {'view_name': 'api-cover-detail'},
- 'previous_delegate': {'view_name': 'api-user-detail'},
- 'current_delegate': {'view_name': 'api-user-detail'},
- 'created_check': {'view_name': 'api-check-detail'},
- }
class EventList(ListAPIView):
diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py
index 7247b11..f0c7225 100644
--- a/patchwork/api/patch.py
+++ b/patchwork/api/patch.py
@@ -29,6 +29,10 @@ from rest_framework.serializers import SerializerMethodField
from patchwork.api.base import PatchworkPermission
from patchwork.api.filters import PatchFilter
+from patchwork.api.embedded import PersonSerializer
+from patchwork.api.embedded import ProjectSerializer
+from patchwork.api.embedded import SeriesSerializer
+from patchwork.api.embedded import UserSerializer
from patchwork.models import Patch
from patchwork.models import State
from patchwork.parser import clean_subject
@@ -73,11 +77,16 @@ class StateField(RelatedField):
class PatchListSerializer(HyperlinkedModelSerializer):
- mbox = SerializerMethodField()
+
+ project = ProjectSerializer(read_only=True)
state = StateField()
- tags = SerializerMethodField()
+ submitter = PersonSerializer(read_only=True)
+ delegate = UserSerializer()
+ mbox = SerializerMethodField()
+ series = SeriesSerializer(many=True, read_only=True)
check = SerializerMethodField()
checks = SerializerMethodField()
+ tags = SerializerMethodField()
def get_mbox(self, instance):
request = self.context.get('request')
@@ -106,15 +115,11 @@ class PatchListSerializer(HyperlinkedModelSerializer):
'checks', 'tags')
extra_kwargs = {
'url': {'view_name': 'api-patch-detail'},
- 'project': {'view_name': 'api-project-detail'},
- 'submitter': {'view_name': 'api-person-detail'},
- 'delegate': {'view_name': 'api-user-detail'},
- 'series': {'view_name': 'api-series-detail',
- 'lookup_url_kwarg': 'pk'},
}
class PatchDetailSerializer(PatchListSerializer):
+
headers = SerializerMethodField()
prefixes = SerializerMethodField()
diff --git a/patchwork/api/person.py b/patchwork/api/person.py
index 574fa84..d002aff 100644
--- a/patchwork/api/person.py
+++ b/patchwork/api/person.py
@@ -22,17 +22,20 @@ from rest_framework.generics import ListAPIView
from rest_framework.generics import RetrieveAPIView
from rest_framework.permissions import IsAuthenticated
+from patchwork.api.embedded import UserSerializer
from patchwork.models import Person
class PersonSerializer(HyperlinkedModelSerializer):
+
+ user = UserSerializer(read_only=True)
+
class Meta:
model = Person
fields = ('id', 'url', 'name', 'email', 'user')
read_only_fields = fields
extra_kwargs = {
'url': {'view_name': 'api-person-detail'},
- 'user': {'view_name': 'api-user-detail'},
}
diff --git a/patchwork/api/project.py b/patchwork/api/project.py
index 8fb8984..11d6504 100644
--- a/patchwork/api/project.py
+++ b/patchwork/api/project.py
@@ -22,19 +22,19 @@ 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 rest_framework.serializers import HyperlinkedRelatedField
from patchwork.api.base import PatchworkPermission
+from patchwork.api.embedded import UserProfileSerializer
from patchwork.models import Project
class ProjectSerializer(HyperlinkedModelSerializer):
+
link_name = CharField(max_length=255, source='linkname')
list_id = CharField(max_length=255, source='listid')
list_email = CharField(max_length=200, source='listemail')
- maintainers = HyperlinkedRelatedField(
- many=True, read_only=True, view_name='api-user-detail',
- source='maintainer_project')
+ maintainers = UserProfileSerializer(many=True, read_only=True,
+ source='maintainer_project')
class Meta:
model = Project
diff --git a/patchwork/api/series.py b/patchwork/api/series.py
index 01f1cbb..12f9277 100644
--- a/patchwork/api/series.py
+++ b/patchwork/api/series.py
@@ -24,12 +24,20 @@ from rest_framework.serializers import SerializerMethodField
from patchwork.api.base import PatchworkPermission
from patchwork.api.filters import SeriesFilter
+from patchwork.api.embedded import CoverLetterSerializer
+from patchwork.api.embedded import PatchSerializer
+from patchwork.api.embedded import PersonSerializer
+from patchwork.api.embedded import ProjectSerializer
from patchwork.models import Series
class SeriesSerializer(HyperlinkedModelSerializer):
+ project = ProjectSerializer(read_only=True)
+ submitter = PersonSerializer(read_only=True)
mbox = SerializerMethodField()
+ cover_letter = CoverLetterSerializer(read_only=True)
+ patches = PatchSerializer(read_only=True, many=True)
def get_mbox(self, instance):
request = self.context.get('request')
@@ -44,10 +52,6 @@ class SeriesSerializer(HyperlinkedModelSerializer):
'received_all', 'mbox', 'cover_letter', 'patches')
extra_kwargs = {
'url': {'view_name': 'api-series-detail'},
- 'project': {'view_name': 'api-project-detail'},
- 'submitter': {'view_name': 'api-person-detail'},
- 'cover_letter': {'view_name': 'api-cover-detail'},
- 'patches': {'view_name': 'api-patch-detail'},
}
diff --git a/patchwork/tests/test_rest_api.py b/patchwork/tests/test_rest_api.py
index 4dcfb3a..70410d0 100644
--- a/patchwork/tests/test_rest_api.py
+++ b/patchwork/tests/test_rest_api.py
@@ -60,6 +60,9 @@ class TestProjectAPI(APITestCase):
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'])
+
+ # nested fields
+
self.assertEqual(len(project_json['maintainers']),
project_obj.maintainer_project.all().count())
@@ -175,8 +178,9 @@ class TestPersonAPI(APITestCase):
else:
self.assertEqual(person_obj.user.username, person_json['name'])
self.assertEqual(person_obj.user.email, person_json['email'])
- self.assertIn(TestUserAPI.api_url(person_obj.user.id),
- person_json['user'])
+ # nested fields
+ self.assertEqual(person_obj.user.id,
+ person_json['user']['id'])
def test_list(self):
"""This API requires authenticated users."""
@@ -307,10 +311,13 @@ class TestPatchAPI(APITestCase):
self.assertEqual(patch_obj.msgid, patch_json['msgid'])
self.assertEqual(patch_obj.state.name, patch_json['state'])
self.assertIn(patch_obj.get_mbox_url(), patch_json['mbox'])
- self.assertIn(TestPersonAPI.api_url(patch_obj.submitter.id),
- patch_json['submitter'])
- self.assertIn(TestProjectAPI.api_url(patch_obj.project.id),
- patch_json['project'])
+
+ # nested fields
+
+ self.assertEqual(patch_obj.submitter.id,
+ patch_json['submitter']['id'])
+ self.assertEqual(patch_obj.project.id,
+ patch_json['project']['id'])
def test_list(self):
"""Validate we can list a patch."""
@@ -450,8 +457,11 @@ class TestCoverLetterAPI(APITestCase):
def assertSerialized(self, cover_obj, cover_json):
self.assertEqual(cover_obj.id, cover_json['id'])
self.assertEqual(cover_obj.name, cover_json['name'])
- self.assertIn(TestPersonAPI.api_url(cover_obj.submitter.id),
- cover_json['submitter'])
+
+ # nested fields
+
+ self.assertEqual(cover_obj.submitter.id,
+ cover_json['submitter']['id'])
def test_list(self):
"""Validate we can list cover letters."""
@@ -512,16 +522,20 @@ class TestSeriesAPI(APITestCase):
self.assertEqual(series_obj.id, series_json['id'])
self.assertEqual(series_obj.name, series_json['name'])
self.assertIn(series_obj.get_mbox_url(), series_json['mbox'])
- self.assertIn(TestProjectAPI.api_url(series_obj.project.id),
- series_json['project'])
- self.assertIn(TestPersonAPI.api_url(series_obj.submitter.id),
- series_json['submitter'])
+
+ # nested fields
+
+ self.assertEqual(series_obj.project.id,
+ series_json['project']['id'])
+ self.assertEqual(series_obj.submitter.id,
+ series_json['submitter']['id'])
self.assertEqual(series_obj.patches.count(),
len(series_json['patches']))
+
if series_obj.cover_letter:
- self.assertIn(
- TestCoverLetterAPI.api_url(series_obj.cover_letter.id),
- series_json['cover_letter'])
+ self.assertEqual(
+ series_obj.cover_letter.id,
+ series_json['cover_letter']['id'])
def test_list(self):
"""Validate we can list series."""
@@ -692,12 +706,15 @@ class TestBundleAPI(APITestCase):
self.assertEqual(bundle_obj.name, bundle_json['name'])
self.assertEqual(bundle_obj.public, bundle_json['public'])
self.assertIn(bundle_obj.get_mbox_url(), bundle_json['mbox'])
+
+ # nested fields
+
self.assertEqual(bundle_obj.patches.count(),
len(bundle_json['patches']))
- self.assertIn(TestUserAPI.api_url(bundle_obj.owner.id),
- bundle_json['owner'])
- self.assertIn(TestProjectAPI.api_url(bundle_obj.project.id),
- bundle_json['project'])
+ self.assertEqual(bundle_obj.owner.id,
+ bundle_json['owner']['id'])
+ self.assertEqual(bundle_obj.project.id,
+ bundle_json['project']['id'])
def test_list(self):
"""Validate we can list bundles."""
--
2.9.3
More information about the Patchwork
mailing list