Skip to content

Commit 4e7b269

Browse files
authored
Merge pull request #450 from graphql-python/form_mutations
Form mutations
2 parents 546a82b + a9d819e commit 4e7b269

File tree

12 files changed

+411
-30
lines changed

12 files changed

+411
-30
lines changed

docs/form-mutations.rst

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Integration with Django forms
2+
=============================
3+
4+
Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation.
5+
*Note: the API is experimental and will likely change in the future.*
6+
7+
FormMutation
8+
------------
9+
10+
.. code:: python
11+
12+
class MyForm(forms.Form):
13+
name = forms.CharField()
14+
15+
class MyMutation(FormMutation):
16+
class Meta:
17+
form_class = MyForm
18+
19+
``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string.
20+
21+
ModelFormMutation
22+
-----------------
23+
24+
``ModelFormMutation`` will pull the fields from a ``ModelForm``.
25+
26+
.. code:: python
27+
28+
class Pet(models.Model):
29+
name = models.CharField()
30+
31+
class PetForm(forms.ModelForm):
32+
class Meta:
33+
model = Pet
34+
fields = ('name',)
35+
36+
# This will get returned when the mutation completes successfully
37+
class PetType(DjangoObjectType):
38+
class Meta:
39+
model = Pet
40+
41+
class PetMutation(DjangoModelFormMutation):
42+
class Meta:
43+
form_class = PetForm
44+
45+
``PetMutation`` will grab the fields from ``PetForm`` and turn them into inputs. If the form is valid then the mutation
46+
will lookup the ``DjangoObjectType`` for the ``Pet`` model and return that under the key ``pet``. Otherwise it will
47+
return a list of errors.
48+
49+
You can change the input name (default is ``input``) and the return field name (default is the model name lowercase).
50+
51+
.. code:: python
52+
53+
class PetMutation(DjangoModelFormMutation):
54+
class Meta:
55+
form_class = PetForm
56+
input_field_name = 'data'
57+
return_field_name = 'my_pet'
58+
59+
Form validation
60+
---------------
61+
62+
Form mutations will call ``is_valid()`` on your forms.
63+
64+
If the form is valid then ``form_valid(form, info)`` is called on the mutation. Override this method to change how
65+
the form is saved or to return a different Graphene object type.
66+
67+
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
68+
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ Contents:
1212
authorization
1313
debug
1414
rest-framework
15+
form-mutations
1516
introspection

graphene_django/filter/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def get_filtering_args_from_filterset(filterset_class, type):
88
a Graphene Field. These arguments will be available to
99
filter against in the GraphQL
1010
"""
11-
from ..form_converter import convert_form_field
11+
from ..forms.converter import convert_form_field
1212

1313
args = {}
1414
for name, filter_field in six.iteritems(filterset_class.base_filters):

graphene_django/forms/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa

graphene_django/form_converter.py renamed to graphene_django/forms/converter.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
from django import forms
2-
from django.forms.fields import BaseTemporalField
2+
from django.core.exceptions import ImproperlyConfigured
33

4-
from graphene import ID, Boolean, Float, Int, List, String, UUID
5-
from graphene.types.datetime import Date, DateTime, Time
4+
from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time
65

76
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
8-
from .utils import import_single_dispatch
7+
from ..utils import import_single_dispatch
8+
99

1010
singledispatch = import_single_dispatch()
1111

1212

1313
@singledispatch
1414
def convert_form_field(field):
15-
raise Exception(
15+
raise ImproperlyConfigured(
1616
"Don't know how to convert the Django form field %s (%s) "
1717
"to Graphene type" %
1818
(field, field.__class__)
1919
)
2020

2121

22-
@convert_form_field.register(BaseTemporalField)
22+
@convert_form_field.register(forms.fields.BaseTemporalField)
2323
@convert_form_field.register(forms.CharField)
2424
@convert_form_field.register(forms.EmailField)
2525
@convert_form_field.register(forms.SlugField)
File renamed without changes.

graphene_django/forms/mutation.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# from django import forms
2+
from collections import OrderedDict
3+
4+
import graphene
5+
from graphene import Field, InputField
6+
from graphene.relay.mutation import ClientIDMutation
7+
from graphene.types.mutation import MutationOptions
8+
# from graphene.types.inputobjecttype import (
9+
# InputObjectTypeOptions,
10+
# InputObjectType,
11+
# )
12+
from graphene.types.utils import yank_fields_from_attrs
13+
from graphene_django.registry import get_global_registry
14+
15+
from .converter import convert_form_field
16+
from .types import ErrorType
17+
18+
19+
def fields_for_form(form, only_fields, exclude_fields):
20+
fields = OrderedDict()
21+
for name, field in form.fields.items():
22+
is_not_in_only = only_fields and name not in only_fields
23+
is_excluded = (
24+
name in exclude_fields # or
25+
# name in already_created_fields
26+
)
27+
28+
if is_not_in_only or is_excluded:
29+
continue
30+
31+
fields[name] = convert_form_field(field)
32+
return fields
33+
34+
35+
class BaseDjangoFormMutation(ClientIDMutation):
36+
class Meta:
37+
abstract = True
38+
39+
@classmethod
40+
def mutate_and_get_payload(cls, root, info, **input):
41+
form = cls.get_form(root, info, **input)
42+
43+
if form.is_valid():
44+
return cls.perform_mutate(form, info)
45+
else:
46+
errors = [
47+
ErrorType(field=key, messages=value)
48+
for key, value in form.errors.items()
49+
]
50+
51+
return cls(errors=errors)
52+
53+
@classmethod
54+
def get_form(cls, root, info, **input):
55+
form_kwargs = cls.get_form_kwargs(root, info, **input)
56+
return cls._meta.form_class(**form_kwargs)
57+
58+
@classmethod
59+
def get_form_kwargs(cls, root, info, **input):
60+
kwargs = {'data': input}
61+
62+
pk = input.pop('id', None)
63+
if pk:
64+
instance = cls._meta.model._default_manager.get(pk=pk)
65+
kwargs['instance'] = instance
66+
67+
return kwargs
68+
69+
70+
# class DjangoFormInputObjectTypeOptions(InputObjectTypeOptions):
71+
# form_class = None
72+
73+
74+
# class DjangoFormInputObjectType(InputObjectType):
75+
# class Meta:
76+
# abstract = True
77+
78+
# @classmethod
79+
# def __init_subclass_with_meta__(cls, form_class=None,
80+
# only_fields=(), exclude_fields=(), _meta=None, **options):
81+
# if not _meta:
82+
# _meta = DjangoFormInputObjectTypeOptions(cls)
83+
# assert isinstance(form_class, forms.Form), (
84+
# 'form_class must be an instance of django.forms.Form'
85+
# )
86+
# _meta.form_class = form_class
87+
# form = form_class()
88+
# fields = fields_for_form(form, only_fields, exclude_fields)
89+
# super(DjangoFormInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, fields=fields, **options)
90+
91+
92+
class DjangoFormMutationOptions(MutationOptions):
93+
form_class = None
94+
95+
96+
class DjangoFormMutation(BaseDjangoFormMutation):
97+
class Meta:
98+
abstract = True
99+
100+
errors = graphene.List(ErrorType)
101+
102+
@classmethod
103+
def __init_subclass_with_meta__(cls, form_class=None,
104+
only_fields=(), exclude_fields=(), **options):
105+
106+
if not form_class:
107+
raise Exception('form_class is required for DjangoFormMutation')
108+
109+
form = form_class()
110+
input_fields = fields_for_form(form, only_fields, exclude_fields)
111+
output_fields = fields_for_form(form, only_fields, exclude_fields)
112+
113+
_meta = DjangoFormMutationOptions(cls)
114+
_meta.form_class = form_class
115+
_meta.fields = yank_fields_from_attrs(
116+
output_fields,
117+
_as=Field,
118+
)
119+
120+
input_fields = yank_fields_from_attrs(
121+
input_fields,
122+
_as=InputField,
123+
)
124+
super(DjangoFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options)
125+
126+
@classmethod
127+
def perform_mutate(cls, form, info):
128+
form.save()
129+
return cls(errors=[])
130+
131+
132+
class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions):
133+
model = None
134+
return_field_name = None
135+
136+
137+
class DjangoModelFormMutation(BaseDjangoFormMutation):
138+
class Meta:
139+
abstract = True
140+
141+
errors = graphene.List(ErrorType)
142+
143+
@classmethod
144+
def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_name=None,
145+
only_fields=(), exclude_fields=(), **options):
146+
147+
if not form_class:
148+
raise Exception('form_class is required for DjangoModelFormMutation')
149+
150+
if not model:
151+
model = form_class._meta.model
152+
153+
if not model:
154+
raise Exception('model is required for DjangoModelFormMutation')
155+
156+
form = form_class()
157+
input_fields = fields_for_form(form, only_fields, exclude_fields)
158+
input_fields['id'] = graphene.ID()
159+
160+
registry = get_global_registry()
161+
model_type = registry.get_type_for_model(model)
162+
return_field_name = return_field_name
163+
if not return_field_name:
164+
model_name = model.__name__
165+
return_field_name = model_name[:1].lower() + model_name[1:]
166+
167+
output_fields = OrderedDict()
168+
output_fields[return_field_name] = graphene.Field(model_type)
169+
170+
_meta = DjangoModelDjangoFormMutationOptions(cls)
171+
_meta.form_class = form_class
172+
_meta.model = model
173+
_meta.return_field_name = return_field_name
174+
_meta.fields = yank_fields_from_attrs(
175+
output_fields,
176+
_as=Field,
177+
)
178+
179+
input_fields = yank_fields_from_attrs(
180+
input_fields,
181+
_as=InputField,
182+
)
183+
super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(
184+
_meta=_meta,
185+
input_fields=input_fields,
186+
**options
187+
)
188+
189+
@classmethod
190+
def perform_mutate(cls, form, info):
191+
obj = form.save()
192+
kwargs = {cls._meta.return_field_name: obj}
193+
return cls(errors=[], **kwargs)

graphene_django/forms/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)