Skip to content

Commit

Permalink
Merge 6cde395 into cec1a84
Browse files Browse the repository at this point in the history
  • Loading branch information
grantmcconnaughey committed Jul 24, 2017
2 parents cec1a84 + 6cde395 commit f4c0d4c
Show file tree
Hide file tree
Showing 10 changed files with 487 additions and 0 deletions.
67 changes: 67 additions & 0 deletions docs/form-mutations.rst
@@ -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.
1 change: 1 addition & 0 deletions docs/index.rst
Expand Up @@ -11,5 +11,6 @@ Contents:
filtering
authorization
debug
form-mutations
rest-framework
introspection
1 change: 1 addition & 0 deletions graphene_django/forms/__init__.py
@@ -0,0 +1 @@
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa
90 changes: 90 additions & 0 deletions graphene_django/forms/converter.py
@@ -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.
161 changes: 161 additions & 0 deletions graphene_django/forms/mutation.py
@@ -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.

0 comments on commit f4c0d4c

Please sign in to comment.