Skip to content

Commit 14bc1cd

Browse files
committed
Add SerializerMutation base class
1 parent 2fd3cb0 commit 14bc1cd

File tree

4 files changed

+182
-0
lines changed

4 files changed

+182
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from collections import OrderedDict
2+
from functools import partial
3+
4+
import graphene
5+
from graphene.types import Argument, Field
6+
from graphene.types.mutation import Mutation, MutationMeta
7+
from graphene.types.objecttype import (
8+
ObjectTypeMeta,
9+
merge,
10+
yank_fields_from_attrs
11+
)
12+
from graphene.types.options import Options
13+
from graphene.types.utils import get_field_as
14+
from graphene.utils.is_base_type import is_base_type
15+
16+
from .serializer_converter import (
17+
convert_serializer_to_input_type,
18+
convert_serializer_field
19+
)
20+
from .types import ErrorType
21+
22+
23+
class SerializerMutationOptions(Options):
24+
def __init__(self, *args, **kwargs):
25+
super().__init__(*args, serializer_class=None, **kwargs)
26+
27+
28+
class SerializerMutationMeta(MutationMeta):
29+
def __new__(cls, name, bases, attrs):
30+
if not is_base_type(bases, SerializerMutationMeta):
31+
return type.__new__(cls, name, bases, attrs)
32+
33+
options = Options(
34+
attrs.pop('Meta', None),
35+
name=name,
36+
description=attrs.pop('__doc__', None),
37+
serializer_class=None,
38+
local_fields=None,
39+
only_fields=(),
40+
exclude_fields=(),
41+
interfaces=(),
42+
registry=None
43+
)
44+
45+
if not options.serializer_class:
46+
raise Exception('Missing serializer_class')
47+
48+
cls = ObjectTypeMeta.__new__(
49+
cls, name, bases, dict(attrs, _meta=options)
50+
)
51+
52+
serializer_fields = cls.fields_for_serializer(options)
53+
options.serializer_fields = yank_fields_from_attrs(
54+
serializer_fields,
55+
_as=Field,
56+
)
57+
58+
options.fields = merge(
59+
options.interface_fields, options.serializer_fields,
60+
options.base_fields, options.local_fields,
61+
{'errors': get_field_as(cls.errors, Field)}
62+
)
63+
64+
cls.Input = convert_serializer_to_input_type(options.serializer_class)
65+
66+
cls.Field = partial(
67+
Field,
68+
cls,
69+
resolver=cls.mutate,
70+
input=Argument(cls.Input, required=True)
71+
)
72+
73+
return cls
74+
75+
@staticmethod
76+
def fields_for_serializer(options):
77+
serializer = options.serializer_class()
78+
79+
only_fields = options.only_fields
80+
81+
already_created_fields = {
82+
name
83+
for name, _ in options.local_fields.items()
84+
}
85+
86+
fields = OrderedDict()
87+
for name, field in serializer.fields.items():
88+
is_not_in_only = only_fields and name not in only_fields
89+
is_excluded = (
90+
name in options.exclude_fields or
91+
name in already_created_fields
92+
)
93+
94+
if is_not_in_only or is_excluded:
95+
continue
96+
97+
fields[name] = convert_serializer_field(field, is_input=False)
98+
return fields
99+
100+
101+
class SerializerMutation(Mutation, metaclass=SerializerMutationMeta):
102+
errors = graphene.List(
103+
ErrorType,
104+
description='May contain more than one error for '
105+
'same field.'
106+
)
107+
108+
@classmethod
109+
def mutate(cls, instance, args, request, info):
110+
input = args.get('input')
111+
112+
serializer = cls._meta.serializer_class(data=dict(input))
113+
114+
if serializer.is_valid():
115+
return cls.perform_mutate(serializer, info)
116+
else:
117+
errors = [
118+
ErrorType(field=key, messages=value)
119+
for key, value in serializer.errors.items()
120+
]
121+
122+
return cls(errors=errors)
123+
124+
@classmethod
125+
def perform_mutate(cls, serializer, info):
126+
return serializer.save()

graphene_django/rest_framework/serializer_converter.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@
88
singledispatch = import_single_dispatch()
99

1010

11+
def convert_serializer_to_input_type(serializer_class):
12+
serializer = serializer_class()
13+
14+
items = {
15+
name: convert_serializer_field(field)
16+
for name, field in serializer.fields.items()
17+
}
18+
19+
return type(
20+
'{}Input'.format(serializer.__class__.__name__),
21+
(graphene.InputObjectType, ),
22+
items
23+
)
24+
25+
1126
@singledispatch
1227
def convert_serializer_field(field):
1328
raise ImproperlyConfigured(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from py.test import raises
2+
from rest_framework import serializers
3+
4+
from ..mutation import SerializerMutation
5+
6+
7+
class MySerializer(serializers.Serializer):
8+
text = serializers.CharField()
9+
10+
11+
def test_needs_serializer_class():
12+
with raises(Exception) as exc:
13+
class MyMutation(SerializerMutation):
14+
pass
15+
16+
assert exc.value.args[0] == 'Missing serializer_class'
17+
18+
19+
def test_has_fields():
20+
class MyMutation(SerializerMutation):
21+
class Meta:
22+
serializer_class = MySerializer
23+
24+
assert 'text' in MyMutation._meta.fields
25+
assert 'errors' in MyMutation._meta.fields
26+
27+
28+
def test_has_input_fields():
29+
class MyMutation(SerializerMutation):
30+
class Meta:
31+
serializer_class = MySerializer
32+
33+
assert 'text' in MyMutation.Input._meta.fields
34+
35+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import graphene
2+
3+
4+
class ErrorType(graphene.ObjectType):
5+
field = graphene.String()
6+
messages = graphene.List(graphene.String)

0 commit comments

Comments
 (0)