Skip to content

Commit e1ae71a

Browse files
committed
REST: Allow setting of values using embedded serializers
Unfortunately, the use of embedded serializers for some fields breaks the ability to update these fields, either via the HTML interface (where the widget is totally busted) or via a client like 'git-pw'. What we actually want is to be able to update these fields like normal primary key but show them using the embedded serializer. We do just this by using a modified variant of the PrimaryKeyRelatedField and using the serializers simply for displaying. Signed-off-by: Stephen Finucane <stephen@that.guru> Closes: #216 (cherry picked from commit 1a0021a)
1 parent 1bd87ee commit e1ae71a

File tree

3 files changed

+174
-113
lines changed

3 files changed

+174
-113
lines changed

patchwork/api/embedded.py

Lines changed: 165 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,54 @@
2323
nested fields.
2424
"""
2525

26+
from collections import OrderedDict
27+
2628
from rest_framework.serializers import CharField
2729
from rest_framework.serializers import SerializerMethodField
30+
from rest_framework.serializers import PrimaryKeyRelatedField
2831

2932
from patchwork.api.base import BaseHyperlinkedModelSerializer
3033
from patchwork.api.base import CheckHyperlinkedIdentityField
3134
from patchwork import models
3235

3336

37+
class SerializedRelatedField(PrimaryKeyRelatedField):
38+
"""
39+
A read-write field that expects a primary key for writes and returns a
40+
serialized version of the underlying field on reads.
41+
"""
42+
43+
def use_pk_only_optimization(self):
44+
# We're using embedded serializers so we want the whole object
45+
return False
46+
47+
def get_queryset(self):
48+
return self._Serializer.Meta.model.objects.all()
49+
50+
def get_choices(self, cutoff=None):
51+
# Override this so we don't call 'to_representation', which no longer
52+
# returns a flat value
53+
queryset = self.get_queryset()
54+
if queryset is None:
55+
# Ensure that field.choices returns something sensible
56+
# even when accessed with a read-only field.
57+
return {}
58+
59+
if cutoff is not None:
60+
queryset = queryset[:cutoff]
61+
62+
return OrderedDict([
63+
(
64+
item.pk,
65+
self.display_value(item)
66+
)
67+
for item in queryset
68+
])
69+
70+
def to_representation(self, data):
71+
return self._Serializer(context=self.context).to_representation(data)
72+
73+
3474
class MboxMixin(BaseHyperlinkedModelSerializer):
3575
"""Embed a link to the mbox URL.
3676
@@ -55,132 +95,150 @@ def get_web_url(self, instance):
5595
return request.build_absolute_uri(instance.get_absolute_url())
5696

5797

58-
class BundleSerializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer):
59-
60-
class Meta:
61-
model = models.Bundle
62-
fields = ('id', 'url', 'web_url', 'name', 'mbox')
63-
read_only_fields = fields
64-
versioned_field = {
65-
'1.1': ('web_url', ),
66-
}
67-
extra_kwargs = {
68-
'url': {'view_name': 'api-bundle-detail'},
69-
}
98+
class BundleSerializer(SerializedRelatedField):
99+
100+
class _Serializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer):
101+
102+
class Meta:
103+
model = models.Bundle
104+
fields = ('id', 'url', 'web_url', 'name', 'mbox')
105+
read_only_fields = fields
106+
versioned_field = {
107+
'1.1': ('web_url', ),
108+
}
109+
extra_kwargs = {
110+
'url': {'view_name': 'api-bundle-detail'},
111+
}
112+
113+
114+
class CheckSerializer(SerializedRelatedField):
115+
116+
class _Serializer(BaseHyperlinkedModelSerializer):
117+
118+
url = CheckHyperlinkedIdentityField('api-check-detail')
119+
120+
def to_representation(self, instance):
121+
data = super(CheckSerializer._Serializer, self).to_representation(
122+
instance)
123+
data['state'] = instance.get_state_display()
124+
return data
125+
126+
class Meta:
127+
model = models.Check
128+
fields = ('id', 'url', 'date', 'state', 'target_url', 'context')
129+
read_only_fields = fields
130+
extra_kwargs = {
131+
'url': {'view_name': 'api-check-detail'},
132+
}
133+
134+
135+
class CoverLetterSerializer(SerializedRelatedField):
136+
137+
class _Serializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer):
138+
139+
class Meta:
140+
model = models.CoverLetter
141+
fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox')
142+
read_only_fields = fields
143+
versioned_field = {
144+
'1.1': ('web_url', 'mbox', ),
145+
}
146+
extra_kwargs = {
147+
'url': {'view_name': 'api-cover-detail'},
148+
}
149+
150+
151+
class PatchSerializer(SerializedRelatedField):
152+
153+
class _Serializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer):
154+
155+
class Meta:
156+
model = models.Patch
157+
fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox')
158+
read_only_fields = fields
159+
versioned_field = {
160+
'1.1': ('web_url', ),
161+
}
162+
extra_kwargs = {
163+
'url': {'view_name': 'api-patch-detail'},
164+
}
70165

71166

72-
class CheckSerializer(BaseHyperlinkedModelSerializer):
167+
class PersonSerializer(SerializedRelatedField):
73168

74-
url = CheckHyperlinkedIdentityField('api-check-detail')
169+
class _Serializer(BaseHyperlinkedModelSerializer):
75170

76-
def to_representation(self, instance):
77-
data = super(CheckSerializer, self).to_representation(instance)
78-
data['state'] = instance.get_state_display()
79-
return data
171+
class Meta:
172+
model = models.Person
173+
fields = ('id', 'url', 'name', 'email')
174+
read_only_fields = fields
175+
extra_kwargs = {
176+
'url': {'view_name': 'api-person-detail'},
177+
}
80178

81-
class Meta:
82-
model = models.Check
83-
fields = ('id', 'url', 'date', 'state', 'target_url', 'context')
84-
read_only_fields = fields
85-
extra_kwargs = {
86-
'url': {'view_name': 'api-check-detail'},
87179

88-
}
180+
class ProjectSerializer(SerializedRelatedField):
89181

182+
class _Serializer(BaseHyperlinkedModelSerializer):
90183

91-
class CoverLetterSerializer(MboxMixin, WebURLMixin,
92-
BaseHyperlinkedModelSerializer):
184+
link_name = CharField(max_length=255, source='linkname')
185+
list_id = CharField(max_length=255, source='listid')
186+
list_email = CharField(max_length=200, source='listemail')
93187

94-
class Meta:
95-
model = models.CoverLetter
96-
fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox')
97-
read_only_fields = fields
98-
versioned_field = {
99-
'1.1': ('web_url', 'mbox', ),
100-
}
101-
extra_kwargs = {
102-
'url': {'view_name': 'api-cover-detail'},
103-
}
188+
class Meta:
189+
model = models.Project
190+
fields = ('id', 'url', 'name', 'link_name', 'list_id',
191+
'list_email', 'web_url', 'scm_url', 'webscm_url')
192+
read_only_fields = fields
193+
extra_kwargs = {
194+
'url': {'view_name': 'api-project-detail'},
195+
}
104196

105197

106-
class PatchSerializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer):
198+
class SeriesSerializer(SerializedRelatedField):
107199

108-
class Meta:
109-
model = models.Patch
110-
fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox')
111-
read_only_fields = fields
112-
versioned_field = {
113-
'1.1': ('web_url', ),
114-
}
115-
extra_kwargs = {
116-
'url': {'view_name': 'api-patch-detail'},
117-
}
200+
class _Serializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer):
118201

202+
class Meta:
203+
model = models.Series
204+
fields = ('id', 'url', 'date', 'name', 'version', 'mbox')
205+
read_only_fields = fields
206+
versioned_field = {
207+
'1.1': ('web_url', ),
208+
}
209+
extra_kwargs = {
210+
'url': {'view_name': 'api-series-detail'},
211+
}
119212

120-
class PersonSerializer(BaseHyperlinkedModelSerializer):
121213

122-
class Meta:
123-
model = models.Person
124-
fields = ('id', 'url', 'name', 'email')
125-
read_only_fields = fields
126-
extra_kwargs = {
127-
'url': {'view_name': 'api-person-detail'},
128-
}
214+
class UserSerializer(SerializedRelatedField):
129215

216+
class _Serializer(BaseHyperlinkedModelSerializer):
130217

131-
class ProjectSerializer(BaseHyperlinkedModelSerializer):
218+
class Meta:
219+
model = models.User
220+
fields = ('id', 'url', 'username', 'first_name', 'last_name',
221+
'email')
222+
read_only_fields = fields
223+
extra_kwargs = {
224+
'url': {'view_name': 'api-user-detail'},
225+
}
132226

133-
link_name = CharField(max_length=255, source='linkname')
134-
list_id = CharField(max_length=255, source='listid')
135-
list_email = CharField(max_length=200, source='listemail')
136227

137-
class Meta:
138-
model = models.Project
139-
fields = ('id', 'url', 'name', 'link_name', 'list_id', 'list_email',
140-
'web_url', 'scm_url', 'webscm_url')
141-
read_only_fields = fields
142-
extra_kwargs = {
143-
'url': {'view_name': 'api-project-detail'},
144-
}
228+
class UserProfileSerializer(SerializedRelatedField):
145229

230+
class _Serializer(BaseHyperlinkedModelSerializer):
146231

147-
class SeriesSerializer(MboxMixin, WebURLMixin,
148-
BaseHyperlinkedModelSerializer):
232+
username = CharField(source='user.username')
233+
first_name = CharField(source='user.first_name')
234+
last_name = CharField(source='user.last_name')
235+
email = CharField(source='user.email')
149236

150-
class Meta:
151-
model = models.Series
152-
fields = ('id', 'url', 'date', 'name', 'version', 'mbox')
153-
read_only_fields = fields
154-
versioned_field = {
155-
'1.1': ('web_url', ),
156-
}
157-
extra_kwargs = {
158-
'url': {'view_name': 'api-series-detail'},
159-
}
160-
161-
162-
class UserSerializer(BaseHyperlinkedModelSerializer):
163-
164-
class Meta:
165-
model = models.User
166-
fields = ('id', 'url', 'username', 'first_name', 'last_name', 'email')
167-
read_only_fields = fields
168-
extra_kwargs = {
169-
'url': {'view_name': 'api-user-detail'},
170-
}
171-
172-
173-
class UserProfileSerializer(BaseHyperlinkedModelSerializer):
174-
175-
username = CharField(source='user.username')
176-
first_name = CharField(source='user.first_name')
177-
last_name = CharField(source='user.last_name')
178-
email = CharField(source='user.email')
179-
180-
class Meta:
181-
model = models.UserProfile
182-
fields = ('id', 'url', 'username', 'first_name', 'last_name', 'email')
183-
read_only_fields = fields
184-
extra_kwargs = {
185-
'url': {'view_name': 'api-user-detail'},
186-
}
237+
class Meta:
238+
model = models.UserProfile
239+
fields = ('id', 'url', 'username', 'first_name', 'last_name',
240+
'email')
241+
read_only_fields = fields
242+
extra_kwargs = {
243+
'url': {'view_name': 'api-user-detail'},
244+
}

patchwork/tests/api/test_patch.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,7 @@ def test_update(self):
222222
'state': state.name, 'delegate': user.id})
223223
self.assertEqual(status.HTTP_200_OK, resp.status_code, resp)
224224
self.assertEqual(Patch.objects.get(id=patch.id).state, state)
225-
# TODO(stephenfin): This is currently broken due to #216
226-
# self.assertEqual(Patch.objects.get(id=patch.id).delegate, user)
225+
self.assertEqual(Patch.objects.get(id=patch.id).delegate, user)
227226

228227
def test_update_invalid(self):
229228
"""Ensure we handle invalid Patch updates."""
@@ -243,10 +242,9 @@ def test_update_invalid(self):
243242
user_b = create_user()
244243
resp = self.client.patch(self.api_url(patch.id),
245244
{'delegate': user_b.id})
246-
# TODO(stephenfin): This is currently broken due to #216
247-
# self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code)
248-
# self.assertContains(resp, "User '%s' is not a maintainer" % user_b,
249-
# status_code=status.HTTP_400_BAD_REQUEST)
245+
self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code)
246+
self.assertContains(resp, "User '%s' is not a maintainer" % user_b,
247+
status_code=status.HTTP_400_BAD_REQUEST)
250248

251249
def test_delete(self):
252250
"""Ensure deletions are always rejected."""
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
An issue that prevented updating of delegates using the REST API is
5+
resolved. (`#216 <https://github.com/getpatchwork/patchwork/issues/216>`__)

0 commit comments

Comments
 (0)