Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
487 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
Integration with Django forms | ||
============================= | ||
|
||
Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation. | ||
|
||
FormMutation | ||
------------ | ||
|
||
.. code:: python | ||
class MyForm(forms.Form): | ||
name = forms.CharField() | ||
class MyMutation(FormMutation): | ||
class Meta: | ||
form_class = MyForm | ||
``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string. | ||
|
||
ModelFormMutation | ||
----------------- | ||
|
||
``ModelFormMutation`` will pull the fields from a ``ModelForm``. | ||
|
||
.. code:: python | ||
class Pet(models.Model): | ||
name = models.CharField() | ||
class PetForm(forms.ModelForm): | ||
class Meta: | ||
model = Pet | ||
fields = ('name',) | ||
# This will get returned when the mutation completes successfully | ||
class PetType(DjangoObjectType): | ||
class Meta: | ||
model = Pet | ||
class PetMutation(ModelFormMutation): | ||
class Meta: | ||
form_class = PetForm | ||
``PetMutation`` will grab the fields from ``PetForm`` and turn them into inputs. If the form is valid then the mutation | ||
will lookup the ``DjangoObjectType`` for the ``Pet`` model and return that under the key ``pet``. Otherwise it will | ||
return a list of errors. | ||
|
||
You can change the input name (default is ``input``) and the return field name (default is the model name lowercase). | ||
|
||
.. code:: python | ||
class PetMutation(ModelFormMutation): | ||
class Meta: | ||
form_class = PetForm | ||
input_field_name = 'data' | ||
return_field_name = 'my_pet' | ||
Form validation | ||
--------------- | ||
|
||
Form mutations will call ``is_valid()`` on your forms. | ||
|
||
If the form is valid then ``form_valid(form, info)`` is called on the mutation. Override this method to change how | ||
the form is saved or to return a different Graphene object type. | ||
|
||
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string | ||
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,5 +11,6 @@ Contents: | |
filtering | ||
authorization | ||
debug | ||
form-mutations | ||
rest-framework | ||
introspection |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
from django import forms | ||
from django.core.exceptions import ImproperlyConfigured | ||
from graphene_django.utils import import_single_dispatch | ||
import graphene | ||
|
||
|
||
singledispatch = import_single_dispatch() | ||
|
||
|
||
def convert_form_to_input_type(form_class): | ||
form = form_class() | ||
|
||
items = { | ||
name: convert_form_field(field) | ||
for name, field in form.fields.items() | ||
} | ||
|
||
return type( | ||
'{}Input'.format(form.__class__.__name__), | ||
(graphene.InputObjectType, ), | ||
items | ||
) | ||
|
||
|
||
@singledispatch | ||
def get_graphene_type_from_form_field(field): | ||
raise ImproperlyConfigured( | ||
"Don't know how to convert the form field %s (%s) " | ||
"to Graphene type" % (field, field.__class__) | ||
) | ||
|
||
|
||
def convert_form_field(field, is_input=True): | ||
""" | ||
Converts a Django form field to a graphql field and marks the field as | ||
required if we are creating an input type and the field itself is required | ||
""" | ||
|
||
graphql_type = get_graphene_type_from_form_field(field) | ||
|
||
kwargs = { | ||
'description': field.help_text, | ||
'required': is_input and field.required, | ||
} | ||
|
||
# if it is a tuple or a list it means that we are returning | ||
# the graphql type and the child type | ||
if isinstance(graphql_type, (list, tuple)): | ||
kwargs['of_type'] = graphql_type[1] | ||
graphql_type = graphql_type[0] | ||
|
||
return graphql_type(**kwargs) | ||
|
||
|
||
@get_graphene_type_from_form_field.register(forms.CharField) | ||
@get_graphene_type_from_form_field.register(forms.ChoiceField) | ||
def convert_form_field_to_string(field): | ||
return graphene.String | ||
|
||
|
||
@get_graphene_type_from_form_field.register(forms.IntegerField) | ||
def convert_form_field_to_int(field): | ||
return graphene.Int | ||
|
||
|
||
@get_graphene_type_from_form_field.register(forms.BooleanField) | ||
def convert_form_field_to_bool(field): | ||
return graphene.Boolean | ||
|
||
|
||
@get_graphene_type_from_form_field.register(forms.FloatField) | ||
@get_graphene_type_from_form_field.register(forms.DecimalField) | ||
def convert_form_field_to_float(field): | ||
return graphene.Float | ||
|
||
|
||
@get_graphene_type_from_form_field.register(forms.DateField) | ||
@get_graphene_type_from_form_field.register(forms.DateTimeField) | ||
def convert_form_field_to_datetime(field): | ||
return graphene.types.datetime.DateTime | ||
|
||
|
||
@get_graphene_type_from_form_field.register(forms.TimeField) | ||
def convert_form_field_to_time(field): | ||
return graphene.types.datetime.Time | ||
|
||
|
||
@get_graphene_type_from_form_field.register(forms.MultipleChoiceField) | ||
def convert_form_field_to_list_of_string(field): | ||
return (graphene.List, graphene.String) |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
from functools import partial | ||
|
||
import six | ||
import graphene | ||
from graphene import Field, Argument | ||
from graphene.types.mutation import MutationMeta | ||
from graphene.types.objecttype import ObjectTypeMeta | ||
from graphene.types.options import Options | ||
from graphene.types.utils import get_field_as, merge | ||
from graphene.utils.is_base_type import is_base_type | ||
from graphene_django.registry import get_global_registry | ||
|
||
from .converter import convert_form_to_input_type | ||
from .types import ErrorType | ||
|
||
|
||
class FormMutationMeta(MutationMeta): | ||
def __new__(cls, name, bases, attrs): | ||
if not is_base_type(bases, FormMutationMeta): | ||
return type.__new__(cls, name, bases, attrs) | ||
|
||
options = Options( | ||
attrs.pop('Meta', None), | ||
name=name, | ||
description=attrs.pop('__doc__', None), | ||
form_class=None, | ||
input_field_name='input', | ||
local_fields=None, | ||
only_fields=(), | ||
exclude_fields=(), | ||
interfaces=(), | ||
registry=None | ||
) | ||
|
||
if not options.form_class: | ||
raise Exception('Missing form_class') | ||
|
||
cls = ObjectTypeMeta.__new__( | ||
cls, name, bases, dict(attrs, _meta=options) | ||
) | ||
|
||
options.fields = merge( | ||
options.interface_fields, options.base_fields, options.local_fields, | ||
{'errors': get_field_as(cls.errors, Field)} | ||
) | ||
|
||
cls.Input = convert_form_to_input_type(options.form_class) | ||
|
||
field_kwargs = {options.input_field_name: Argument(cls.Input, required=True)} | ||
cls.Field = partial( | ||
Field, | ||
cls, | ||
resolver=cls.mutate, | ||
**field_kwargs | ||
) | ||
|
||
return cls | ||
|
||
|
||
class BaseFormMutation(graphene.Mutation): | ||
|
||
@classmethod | ||
def mutate(cls, root, args, context, info): | ||
form = cls.get_form(root, args, context, info) | ||
|
||
if form.is_valid(): | ||
return cls.form_valid(form, info) | ||
else: | ||
return cls.form_invalid(form, info) | ||
|
||
@classmethod | ||
def form_valid(cls, form, info): | ||
form.save() | ||
return cls(errors=[]) | ||
|
||
@classmethod | ||
def form_invalid(cls, form, info): | ||
errors = [ | ||
ErrorType(field=key, messages=value) | ||
for key, value in form.errors.items() | ||
] | ||
return cls(errors=errors) | ||
|
||
@classmethod | ||
def get_form(cls, root, args, context, info): | ||
form_data = args.get(cls._meta.input_field_name) | ||
kwargs = cls.get_form_kwargs(root, args, context, info) | ||
return cls._meta.form_class(data=form_data, **kwargs) | ||
|
||
@classmethod | ||
def get_form_kwargs(cls, root, args, context, info): | ||
return {} | ||
|
||
|
||
class FormMutation(six.with_metaclass(FormMutationMeta, BaseFormMutation)): | ||
|
||
errors = graphene.List(ErrorType) | ||
|
||
|
||
class ModelFormMutationMeta(MutationMeta): | ||
def __new__(cls, name, bases, attrs): | ||
if not is_base_type(bases, ModelFormMutationMeta): | ||
return type.__new__(cls, name, bases, attrs) | ||
|
||
options = Options( | ||
attrs.pop('Meta', None), | ||
name=name, | ||
description=attrs.pop('__doc__', None), | ||
form_class=None, | ||
input_field_name='input', | ||
return_field_name=None, | ||
model=None, | ||
local_fields=None, | ||
only_fields=(), | ||
exclude_fields=(), | ||
interfaces=(), | ||
registry=None | ||
) | ||
|
||
if not options.form_class: | ||
raise Exception('Missing form_class') | ||
|
||
cls = ObjectTypeMeta.__new__( | ||
cls, name, bases, dict(attrs, _meta=options) | ||
) | ||
|
||
options.fields = merge( | ||
options.interface_fields, options.base_fields, options.local_fields, | ||
{'errors': get_field_as(cls.errors, Field)} | ||
) | ||
|
||
cls.Input = convert_form_to_input_type(options.form_class) | ||
|
||
field_kwargs = {options.input_field_name: Argument(cls.Input, required=True)} | ||
cls.Field = partial( | ||
Field, | ||
cls, | ||
resolver=cls.mutate, | ||
**field_kwargs | ||
) | ||
|
||
cls.model = options.model or options.form_class.Meta.model | ||
cls.return_field_name = cls._meta.return_field_name or cls.model._meta.model_name | ||
|
||
registry = get_global_registry() | ||
model_type = registry.get_type_for_model(cls.model) | ||
|
||
options.fields[cls.return_field_name] = graphene.Field(model_type) | ||
|
||
return cls | ||
|
||
|
||
class ModelFormMutation(six.with_metaclass(ModelFormMutationMeta, BaseFormMutation)): | ||
|
||
errors = graphene.List(ErrorType) | ||
|
||
@classmethod | ||
def form_valid(cls, form, info): | ||
obj = form.save() | ||
kwargs = {cls.return_field_name: obj} | ||
return cls(errors=[], **kwargs) |
Empty file.
Oops, something went wrong.