Skip to content

Commit a480a39

Browse files
authored
Merge pull request #326 from pizzapanther/drf-serializer-update
DRF Serializer update
2 parents 70dffa9 + 91a99ee commit a480a39

File tree

3 files changed

+152
-17
lines changed

3 files changed

+152
-17
lines changed

docs/rest-framework.rst

+47
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,50 @@ You can create a Mutation based on a serializer by using the
1919
class Meta:
2020
serializer_class = MySerializer
2121
22+
Create/Update Operations
23+
---------------------
24+
25+
By default ModelSerializers accept create and update operations. To
26+
customize this use the `model_operations` attribute. The update
27+
operation looks up models by the primary key by default. You can
28+
customize the look up with the lookup attribute.
29+
30+
Other default attributes:
31+
32+
`partial = False`: Accept updates without all the input fields.
33+
34+
.. code:: python
35+
36+
from graphene_django.rest_framework.mutation import SerializerMutation
37+
38+
class AwesomeModelMutation(SerializerMutation):
39+
class Meta:
40+
serializer_class = MyModelSerializer
41+
model_operations = ['create', 'update']
42+
lookup_field = 'id'
43+
44+
Overriding Update Queries
45+
-------------------------
46+
47+
Use the method `get_serializer_kwargs` to override how
48+
updates are applied.
49+
50+
.. code:: python
51+
52+
from graphene_django.rest_framework.mutation import SerializerMutation
53+
54+
class AwesomeModelMutation(SerializerMutation):
55+
class Meta:
56+
serializer_class = MyModelSerializer
57+
58+
@classmethod
59+
def get_serializer_kwargs(cls, root, info, **input):
60+
if 'id' in input:
61+
instance = Post.objects.filter(id=input['id'], owner=info.context.user).first()
62+
if instance:
63+
return {'instance': instance, 'data': input, 'partial': True}
64+
65+
else:
66+
raise http.Http404
67+
68+
return {'data': input, 'partial': True}

graphene_django/rest_framework/mutation.py

+49-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from collections import OrderedDict
22

3+
from django.shortcuts import get_object_or_404
4+
35
import graphene
46
from graphene.types import Field, InputField
57
from graphene.types.mutation import MutationOptions
@@ -15,6 +17,9 @@
1517

1618

1719
class SerializerMutationOptions(MutationOptions):
20+
lookup_field = None
21+
model_class = None
22+
model_operations = ['create', 'update']
1823
serializer_class = None
1924

2025

@@ -44,18 +49,34 @@ class Meta:
4449
)
4550

4651
@classmethod
47-
def __init_subclass_with_meta__(cls, serializer_class=None,
52+
def __init_subclass_with_meta__(cls, lookup_field=None,
53+
serializer_class=None, model_class=None,
54+
model_operations=['create', 'update'],
4855
only_fields=(), exclude_fields=(), **options):
4956

5057
if not serializer_class:
5158
raise Exception('serializer_class is required for the SerializerMutation')
5259

60+
if 'update' not in model_operations and 'create' not in model_operations:
61+
raise Exception('model_operations must contain "create" and/or "update"')
62+
5363
serializer = serializer_class()
64+
if model_class is None:
65+
serializer_meta = getattr(serializer_class, 'Meta', None)
66+
if serializer_meta:
67+
model_class = getattr(serializer_meta, 'model', None)
68+
69+
if lookup_field is None and model_class:
70+
lookup_field = model_class._meta.pk.name
71+
5472
input_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=True)
5573
output_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False)
5674

5775
_meta = SerializerMutationOptions(cls)
76+
_meta.lookup_field = lookup_field
77+
_meta.model_operations = model_operations
5878
_meta.serializer_class = serializer_class
79+
_meta.model_class = model_class
5980
_meta.fields = yank_fields_from_attrs(
6081
output_fields,
6182
_as=Field,
@@ -67,9 +88,35 @@ def __init_subclass_with_meta__(cls, serializer_class=None,
6788
)
6889
super(SerializerMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options)
6990

91+
@classmethod
92+
def get_serializer_kwargs(cls, root, info, **input):
93+
lookup_field = cls._meta.lookup_field
94+
model_class = cls._meta.model_class
95+
96+
if model_class:
97+
if 'update' in cls._meta.model_operations and lookup_field in input:
98+
instance = get_object_or_404(model_class, **{
99+
lookup_field: input[lookup_field]})
100+
elif 'create' in cls._meta.model_operations:
101+
instance = None
102+
else:
103+
raise Exception(
104+
'Invalid update operation. Input parameter "{}" required.'.format(
105+
lookup_field
106+
))
107+
108+
return {
109+
'instance': instance,
110+
'data': input,
111+
'context': {'request': info.context}
112+
}
113+
114+
return {'data': input, 'context': {'request': info.context}}
115+
70116
@classmethod
71117
def mutate_and_get_payload(cls, root, info, **input):
72-
serializer = cls._meta.serializer_class(data=input)
118+
kwargs = cls.get_serializer_kwargs(root, info, **input)
119+
serializer = cls._meta.serializer_class(**kwargs)
73120

74121
if serializer.is_valid():
75122
return cls.perform_mutate(serializer, info)

graphene_django/rest_framework/tests/test_mutation.py

+56-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import datetime
22

3-
from graphene import Field
3+
from graphene import Field, ResolveInfo
44
from graphene.types.inputobjecttype import InputObjectType
55
from py.test import raises
66
from py.test import mark
@@ -10,12 +10,29 @@
1010
from ..models import MyFakeModel
1111
from ..mutation import SerializerMutation
1212

13+
def mock_info():
14+
return ResolveInfo(
15+
None,
16+
None,
17+
None,
18+
None,
19+
schema=None,
20+
fragments=None,
21+
root_value=None,
22+
operation=None,
23+
variable_values=None,
24+
context=None
25+
)
26+
1327

1428
class MyModelSerializer(serializers.ModelSerializer):
1529
class Meta:
1630
model = MyFakeModel
1731
fields = '__all__'
1832

33+
class MyModelMutation(SerializerMutation):
34+
class Meta:
35+
serializer_class = MyModelSerializer
1936

2037
class MySerializer(serializers.Serializer):
2138
text = serializers.CharField()
@@ -92,7 +109,7 @@ class MyMutation(SerializerMutation):
92109
class Meta:
93110
serializer_class = MySerializer
94111

95-
result = MyMutation.mutate_and_get_payload(None, None, **{
112+
result = MyMutation.mutate_and_get_payload(None, mock_info(), **{
96113
'text': 'value',
97114
'model': {
98115
'cool_name': 'other_value'
@@ -102,34 +119,58 @@ class Meta:
102119

103120

104121
@mark.django_db
105-
def test_model_mutate_and_get_payload_success():
106-
class MyMutation(SerializerMutation):
107-
class Meta:
108-
serializer_class = MyModelSerializer
109-
110-
result = MyMutation.mutate_and_get_payload(None, None, **{
122+
def test_model_add_mutate_and_get_payload_success():
123+
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{
111124
'cool_name': 'Narf',
112125
})
113126
assert result.errors is None
114127
assert result.cool_name == 'Narf'
115128
assert isinstance(result.created, datetime.datetime)
116129

130+
@mark.django_db
131+
def test_model_update_mutate_and_get_payload_success():
132+
instance = MyFakeModel.objects.create(cool_name="Narf")
133+
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{
134+
'id': instance.id,
135+
'cool_name': 'New Narf',
136+
})
137+
assert result.errors is None
138+
assert result.cool_name == 'New Narf'
139+
140+
@mark.django_db
141+
def test_model_invalid_update_mutate_and_get_payload_success():
142+
class InvalidModelMutation(SerializerMutation):
143+
class Meta:
144+
serializer_class = MyModelSerializer
145+
model_operations = ['update']
146+
147+
with raises(Exception) as exc:
148+
result = InvalidModelMutation.mutate_and_get_payload(None, mock_info(), **{
149+
'cool_name': 'Narf',
150+
})
151+
152+
assert '"id" required' in str(exc.value)
153+
117154
def test_mutate_and_get_payload_error():
118155

119156
class MyMutation(SerializerMutation):
120157
class Meta:
121158
serializer_class = MySerializer
122159

123160
# missing required fields
124-
result = MyMutation.mutate_and_get_payload(None, None, **{})
161+
result = MyMutation.mutate_and_get_payload(None, mock_info(), **{})
125162
assert len(result.errors) > 0
126163

127164
def test_model_mutate_and_get_payload_error():
128-
129-
class MyMutation(SerializerMutation):
130-
class Meta:
131-
serializer_class = MyModelSerializer
132-
133165
# missing required fields
134-
result = MyMutation.mutate_and_get_payload(None, None, **{})
166+
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
135167
assert len(result.errors) > 0
168+
169+
def test_invalid_serializer_operations():
170+
with raises(Exception) as exc:
171+
class MyModelMutation(SerializerMutation):
172+
class Meta:
173+
serializer_class = MyModelSerializer
174+
model_operations = ['Add']
175+
176+
assert 'model_operations' in str(exc.value)

0 commit comments

Comments
 (0)