Skip to content

Commit 9c179bf

Browse files
committed
REST: Add 'web_url' link to API responses
This provides an easy way for clients to navigate to the web view. The URL is added to four resources: bundles, comments, cover letters and series. We could also extend this to projects and users in the future, but the latter would require renaming an existing property while the latter would require a public "user" page which does not currently exists. Signed-off-by: Stephen Finucane <[email protected]> Reviewed-by: Daniel Axtens <[email protected]>
1 parent 98ac7c2 commit 9c179bf

File tree

11 files changed

+165
-34
lines changed

11 files changed

+165
-34
lines changed

patchwork/api/bundle.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
from django.db.models import Q
2121
from rest_framework.generics import ListAPIView
2222
from rest_framework.generics import RetrieveAPIView
23-
from rest_framework.serializers import HyperlinkedModelSerializer
2423
from rest_framework.serializers import SerializerMethodField
2524

25+
from patchwork.api.base import BaseHyperlinkedModelSerializer
2626
from patchwork.api.base import PatchworkPermission
2727
from patchwork.api.filters import BundleFilterSet
2828
from patchwork.api.embedded import PatchSerializer
@@ -32,22 +32,30 @@
3232
from patchwork.models import Bundle
3333

3434

35-
class BundleSerializer(HyperlinkedModelSerializer):
35+
class BundleSerializer(BaseHyperlinkedModelSerializer):
3636

37+
web_url = SerializerMethodField()
3738
project = ProjectSerializer(read_only=True)
3839
mbox = SerializerMethodField()
3940
owner = UserSerializer(read_only=True)
4041
patches = PatchSerializer(many=True, read_only=True)
4142

43+
def get_web_url(self, instance):
44+
request = self.context.get('request')
45+
return request.build_absolute_uri(instance.get_absolute_url())
46+
4247
def get_mbox(self, instance):
4348
request = self.context.get('request')
4449
return request.build_absolute_uri(instance.get_mbox_url())
4550

4651
class Meta:
4752
model = Bundle
48-
fields = ('id', 'url', 'project', 'name', 'owner', 'patches',
49-
'public', 'mbox')
53+
fields = ('id', 'url', 'web_url', 'project', 'name', 'owner',
54+
'patches', 'public', 'mbox')
5055
read_only_fields = ('owner', 'patches', 'mbox')
56+
versioned_fields = {
57+
'1.1': ('web_url', ),
58+
}
5159
extra_kwargs = {
5260
'url': {'view_name': 'api-bundle-detail'},
5361
}

patchwork/api/comment.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@
3030

3131
class CommentListSerializer(BaseHyperlinkedModelSerializer):
3232

33+
web_url = SerializerMethodField()
3334
subject = SerializerMethodField()
3435
headers = SerializerMethodField()
3536
submitter = PersonSerializer(read_only=True)
3637

38+
def get_web_url(self, instance):
39+
request = self.context.get('request')
40+
return request.build_absolute_uri(instance.get_absolute_url())
41+
3742
def get_subject(self, comment):
3843
return email.parser.Parser().parsestr(comment.headers,
3944
True).get('Subject', '')
@@ -54,9 +59,12 @@ def get_headers(self, comment):
5459

5560
class Meta:
5661
model = Comment
57-
fields = ('id', 'msgid', 'date', 'subject', 'submitter', 'content',
58-
'headers')
62+
fields = ('id', 'web_url', 'msgid', 'date', 'subject', 'submitter',
63+
'content', 'headers')
5964
read_only_fields = fields
65+
versioned_fields = {
66+
'1.1': ('web_url', ),
67+
}
6068

6169

6270
class CommentList(ListAPIView):

patchwork/api/cover.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,17 @@
3434

3535
class CoverLetterListSerializer(BaseHyperlinkedModelSerializer):
3636

37+
web_url = SerializerMethodField()
3738
project = ProjectSerializer(read_only=True)
3839
submitter = PersonSerializer(read_only=True)
3940
mbox = SerializerMethodField()
4041
series = SeriesSerializer(many=True, read_only=True)
4142
comments = SerializerMethodField()
4243

44+
def get_web_url(self, instance):
45+
request = self.context.get('request')
46+
return request.build_absolute_uri(instance.get_absolute_url())
47+
4348
def get_mbox(self, instance):
4449
request = self.context.get('request')
4550
return request.build_absolute_uri(instance.get_mbox_url())
@@ -50,11 +55,11 @@ def get_comments(self, cover):
5055

5156
class Meta:
5257
model = CoverLetter
53-
fields = ('id', 'url', 'project', 'msgid', 'date', 'name', 'submitter',
54-
'mbox', 'series', 'comments')
58+
fields = ('id', 'url', 'web_url', 'project', 'msgid', 'date', 'name',
59+
'submitter', 'mbox', 'series', 'comments')
5560
read_only_fields = fields
5661
versioned_fields = {
57-
'1.1': ('mbox', 'comments'),
62+
'1.1': ('web_url', 'mbox', 'comments'),
5863
}
5964
extra_kwargs = {
6065
'url': {'view_name': 'api-cover-detail'},

patchwork/api/embedded.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333

3434
class MboxMixin(BaseHyperlinkedModelSerializer):
35-
"""Embed an link to the mbox URL.
35+
"""Embed a link to the mbox URL.
3636
3737
This field is just way too useful to leave out of even the embedded
3838
serialization.
@@ -45,12 +45,25 @@ def get_mbox(self, instance):
4545
return request.build_absolute_uri(instance.get_mbox_url())
4646

4747

48-
class BundleSerializer(MboxMixin, BaseHyperlinkedModelSerializer):
48+
class WebURLMixin(BaseHyperlinkedModelSerializer):
49+
"""Embed a link to the web URL."""
50+
51+
web_url = SerializerMethodField()
52+
53+
def get_web_url(self, instance):
54+
request = self.context.get('request')
55+
return request.build_absolute_uri(instance.get_absolute_url())
56+
57+
58+
class BundleSerializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer):
4959

5060
class Meta:
5161
model = models.Bundle
52-
fields = ('id', 'url', 'name', 'mbox')
62+
fields = ('id', 'url', 'web_url', 'name', 'mbox')
5363
read_only_fields = fields
64+
versioned_field = {
65+
'1.1': ('web_url', ),
66+
}
5467
extra_kwargs = {
5568
'url': {'view_name': 'api-bundle-detail'},
5669
}
@@ -75,26 +88,30 @@ class Meta:
7588
}
7689

7790

78-
class CoverLetterSerializer(MboxMixin, BaseHyperlinkedModelSerializer):
91+
class CoverLetterSerializer(MboxMixin, WebURLMixin,
92+
BaseHyperlinkedModelSerializer):
7993

8094
class Meta:
8195
model = models.CoverLetter
82-
fields = ('id', 'url', 'msgid', 'date', 'name', 'mbox')
96+
fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox')
8397
read_only_fields = fields
8498
versioned_field = {
85-
'1.1': ('mbox', ),
99+
'1.1': ('web_url', 'mbox', ),
86100
}
87101
extra_kwargs = {
88102
'url': {'view_name': 'api-cover-detail'},
89103
}
90104

91105

92-
class PatchSerializer(MboxMixin, BaseHyperlinkedModelSerializer):
106+
class PatchSerializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer):
93107

94108
class Meta:
95109
model = models.Patch
96-
fields = ('id', 'url', 'msgid', 'date', 'name', 'mbox')
110+
fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox')
97111
read_only_fields = fields
112+
versioned_field = {
113+
'1.1': ('web_url', ),
114+
}
98115
extra_kwargs = {
99116
'url': {'view_name': 'api-patch-detail'},
100117
}
@@ -127,12 +144,16 @@ class Meta:
127144
}
128145

129146

130-
class SeriesSerializer(MboxMixin, BaseHyperlinkedModelSerializer):
147+
class SeriesSerializer(MboxMixin, WebURLMixin,
148+
BaseHyperlinkedModelSerializer):
131149

132150
class Meta:
133151
model = models.Series
134152
fields = ('id', 'url', 'date', 'name', 'version', 'mbox')
135153
read_only_fields = fields
154+
versioned_field = {
155+
'1.1': ('web_url', ),
156+
}
136157
extra_kwargs = {
137158
'url': {'view_name': 'api-series-detail'},
138159
}

patchwork/api/patch.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def get_queryset(self):
7777

7878
class PatchListSerializer(BaseHyperlinkedModelSerializer):
7979

80+
web_url = SerializerMethodField()
8081
project = ProjectSerializer(read_only=True)
8182
state = StateField()
8283
submitter = PersonSerializer(read_only=True)
@@ -88,6 +89,10 @@ class PatchListSerializer(BaseHyperlinkedModelSerializer):
8889
checks = SerializerMethodField()
8990
tags = SerializerMethodField()
9091

92+
def get_web_url(self, instance):
93+
request = self.context.get('request')
94+
return request.build_absolute_uri(instance.get_absolute_url())
95+
9196
def get_mbox(self, instance):
9297
request = self.context.get('request')
9398
return request.build_absolute_uri(instance.get_mbox_url())
@@ -110,15 +115,15 @@ def get_tags(self, instance):
110115

111116
class Meta:
112117
model = Patch
113-
fields = ('id', 'url', 'project', 'msgid', 'date', 'name',
118+
fields = ('id', 'url', 'web_url', 'project', 'msgid', 'date', 'name',
114119
'commit_ref', 'pull_url', 'state', 'archived', 'hash',
115120
'submitter', 'delegate', 'mbox', 'series', 'comments',
116121
'check', 'checks', 'tags')
117-
read_only_fields = ('project', 'msgid', 'date', 'name', 'hash',
118-
'submitter', 'mbox', 'mbox', 'series', 'comments',
122+
read_only_fields = ('web_url', 'project', 'msgid', 'date', 'name',
123+
'hash', 'submitter', 'mbox', 'series', 'comments',
119124
'check', 'checks', 'tags')
120125
versioned_fields = {
121-
'1.1': ('comments', ),
126+
'1.1': ('comments', 'web_url'),
122127
}
123128
extra_kwargs = {
124129
'url': {'view_name': 'api-patch-detail'},

patchwork/api/series.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919

2020
from rest_framework.generics import ListAPIView
2121
from rest_framework.generics import RetrieveAPIView
22-
from rest_framework.serializers import HyperlinkedModelSerializer
2322
from rest_framework.serializers import SerializerMethodField
2423

24+
from patchwork.api.base import BaseHyperlinkedModelSerializer
2525
from patchwork.api.base import PatchworkPermission
2626
from patchwork.api.filters import SeriesFilterSet
2727
from patchwork.api.embedded import CoverLetterSerializer
@@ -31,25 +31,33 @@
3131
from patchwork.models import Series
3232

3333

34-
class SeriesSerializer(HyperlinkedModelSerializer):
34+
class SeriesSerializer(BaseHyperlinkedModelSerializer):
3535

36+
web_url = SerializerMethodField()
3637
project = ProjectSerializer(read_only=True)
3738
submitter = PersonSerializer(read_only=True)
3839
mbox = SerializerMethodField()
3940
cover_letter = CoverLetterSerializer(read_only=True)
4041
patches = PatchSerializer(read_only=True, many=True)
4142

43+
def get_web_url(self, instance):
44+
request = self.context.get('request')
45+
return request.build_absolute_uri(instance.get_absolute_url())
46+
4247
def get_mbox(self, instance):
4348
request = self.context.get('request')
4449
return request.build_absolute_uri(instance.get_mbox_url())
4550

4651
class Meta:
4752
model = Series
48-
fields = ('id', 'url', 'project', 'name', 'date', 'submitter',
49-
'version', 'total', 'received_total', 'received_all',
50-
'mbox', 'cover_letter', 'patches')
53+
fields = ('id', 'url', 'web_url', 'project', 'name', 'date',
54+
'submitter', 'version', 'total', 'received_total',
55+
'received_all', 'mbox', 'cover_letter', 'patches')
5156
read_only_fields = ('date', 'submitter', 'total', 'received_total',
5257
'received_all', 'mbox', 'cover_letter', 'patches')
58+
versioned_fields = {
59+
'1.1': ('web_url', ),
60+
}
5361
extra_kwargs = {
5462
'url': {'view_name': 'api-series-detail'},
5563
}

patchwork/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,9 @@ def latest_series(self):
412412

413413
class CoverLetter(SeriesMixin, Submission):
414414

415+
def get_absolute_url(self):
416+
return reverse('cover-detail', kwargs={'cover_id': self.id})
417+
415418
def get_mbox_url(self):
416419
return reverse('cover-mbox', kwargs={'cover_id': self.id})
417420

@@ -603,6 +606,9 @@ class Comment(EmailMixin, models.Model):
603606
related_query_name='comment',
604607
on_delete=models.CASCADE)
605608

609+
def get_absolute_url(self):
610+
return reverse('comment-redirect', kwargs={'comment_id': self.id})
611+
606612
def save(self, *args, **kwargs):
607613
super(Comment, self).save(*args, **kwargs)
608614
if hasattr(self.submission, 'patch'):
@@ -728,6 +734,12 @@ def add_patch(self, patch, number):
728734
patch=patch,
729735
number=number)
730736

737+
def get_absolute_url(self):
738+
# TODO(stephenfin): We really need a proper series view
739+
return reverse('patch-list',
740+
kwargs={'project_id': self.project.linkname}) + (
741+
'?series=%d' % self.id)
742+
731743
def get_mbox_url(self):
732744
return reverse('series-mbox', kwargs={'series_id': self.id})
733745

patchwork/tests/api/test_bundle.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,22 @@ class TestBundleAPI(APITestCase):
4141
fixtures = ['default_tags']
4242

4343
@staticmethod
44-
def api_url(item=None):
44+
def api_url(item=None, version=None):
45+
kwargs = {}
46+
if version:
47+
kwargs['version'] = version
48+
4549
if item is None:
46-
return reverse('api-bundle-list')
47-
return reverse('api-bundle-detail', args=[item])
50+
return reverse('api-bundle-list', kwargs=kwargs)
51+
kwargs['pk'] = item
52+
return reverse('api-bundle-detail', kwargs=kwargs)
4853

4954
def assertSerialized(self, bundle_obj, bundle_json):
5055
self.assertEqual(bundle_obj.id, bundle_json['id'])
5156
self.assertEqual(bundle_obj.name, bundle_json['name'])
5257
self.assertEqual(bundle_obj.public, bundle_json['public'])
5358
self.assertIn(bundle_obj.get_mbox_url(), bundle_json['mbox'])
59+
self.assertIn(bundle_obj.get_absolute_url(), bundle_json['web_url'])
5460

5561
# nested fields
5662

@@ -109,6 +115,16 @@ def test_list(self):
109115
resp = self.client.get(self.api_url(), {'owner': 'otheruser'})
110116
self.assertEqual(0, len(resp.data))
111117

118+
def test_list_version_1_0(self):
119+
"""Validate that newer fields are dropped for older API versions."""
120+
create_bundle(public=True)
121+
122+
resp = self.client.get(self.api_url(version='1.0'))
123+
self.assertEqual(status.HTTP_200_OK, resp.status_code)
124+
self.assertEqual(1, len(resp.data))
125+
self.assertIn('url', resp.data[0])
126+
self.assertNotIn('web_url', resp.data[0])
127+
112128
def test_detail(self):
113129
"""Validate we can get a specific bundle."""
114130
bundle = create_bundle(public=True)
@@ -117,6 +133,13 @@ def test_detail(self):
117133
self.assertEqual(status.HTTP_200_OK, resp.status_code)
118134
self.assertSerialized(bundle, resp.data)
119135

136+
def test_detail_version_1_0(self):
137+
bundle = create_bundle(public=True)
138+
139+
resp = self.client.get(self.api_url(bundle.id, version='1.0'))
140+
self.assertIn('url', resp.data)
141+
self.assertNotIn('web_url', resp.data)
142+
120143
def test_create_update_delete(self):
121144
"""Ensure creates, updates and deletes aren't allowed"""
122145
user = create_maintainer()

0 commit comments

Comments
 (0)