Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Django form mutations #217

Merged
merged 14 commits into from
Jun 5, 2018
67 changes: 67 additions & 0 deletions docs/form-mutations.rst
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.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ Contents:
authorization
debug
rest-framework
form-mutations
introspection
2 changes: 1 addition & 1 deletion graphene_django/filter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def get_filtering_args_from_filterset(filterset_class, type):
a Graphene Field. These arguments will be available to
filter against in the GraphQL
"""
from ..form_converter import convert_form_field
from ..forms.converter import convert_form_field

args = {}
for name, filter_field in six.iteritems(filterset_class.base_filters):
Expand Down
1 change: 1 addition & 0 deletions graphene_django/forms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from django import forms
from django.forms.fields import BaseTemporalField
from django.core.exceptions import ImproperlyConfigured

from graphene import ID, Boolean, Float, Int, List, String, UUID
import graphene

from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
from .utils import import_single_dispatch

singledispatch = import_single_dispatch()
from ..utils import import_single_dispatch

try:
UUIDField = forms.UUIDField
Expand All @@ -15,16 +13,19 @@ class UUIDField(object):
pass


singledispatch = import_single_dispatch()


@singledispatch
def convert_form_field(field):
raise Exception(
raise ImproperlyConfigured(
"Don't know how to convert the Django form field %s (%s) "
"to Graphene type" %
(field, field.__class__)
)


@convert_form_field.register(BaseTemporalField)
@convert_form_field.register(forms.fields.BaseTemporalField)
@convert_form_field.register(forms.CharField)
@convert_form_field.register(forms.EmailField)
@convert_form_field.register(forms.SlugField)
Expand All @@ -33,43 +34,54 @@ def convert_form_field(field):
@convert_form_field.register(forms.RegexField)
@convert_form_field.register(forms.Field)
def convert_form_field_to_string(field):
return String(description=field.help_text, required=field.required)
return graphene.String(description=field.help_text, required=field.required)


@convert_form_field.register(UUIDField)
def convert_form_field_to_uuid(field):
return UUID(description=field.help_text, required=field.required)
return graphene.UUID(description=field.help_text, required=field.required)


@convert_form_field.register(forms.IntegerField)
@convert_form_field.register(forms.NumberInput)
def convert_form_field_to_int(field):
return Int(description=field.help_text, required=field.required)
return graphene.Int(description=field.help_text, required=field.required)


@convert_form_field.register(forms.BooleanField)
def convert_form_field_to_boolean(field):
return Boolean(description=field.help_text, required=True)
return graphene.Boolean(description=field.help_text, required=True)


@convert_form_field.register(forms.NullBooleanField)
def convert_form_field_to_nullboolean(field):
return Boolean(description=field.help_text)
return graphene.Boolean(description=field.help_text)


@convert_form_field.register(forms.DecimalField)
@convert_form_field.register(forms.FloatField)
def convert_form_field_to_float(field):
return Float(description=field.help_text, required=field.required)
return graphene.Float(description=field.help_text, required=field.required)


@convert_form_field.register(forms.ModelMultipleChoiceField)
@convert_form_field.register(GlobalIDMultipleChoiceField)
def convert_form_field_to_list(field):
return List(ID, required=field.required)
return graphene.List(graphene.ID, required=field.required)


@convert_form_field.register(forms.ModelChoiceField)
@convert_form_field.register(GlobalIDFormField)
def convert_form_field_to_id(field):
return ID(required=field.required)
return graphene.ID(required=field.required)


@convert_form_field.register(forms.DateField)
@convert_form_field.register(forms.DateTimeField)
def convert_form_field_to_datetime(field):
return graphene.types.datetime.DateTime(description=field.help_text, required=field.required)


@convert_form_field.register(forms.TimeField)
def convert_form_field_to_time(field):
return graphene.types.datetime.Time(description=field.help_text, required=field.required)
File renamed without changes.
166 changes: 166 additions & 0 deletions graphene_django/forms/mutation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from collections import OrderedDict

import graphene
from graphene import Field, InputField
from graphene.relay.mutation import ClientIDMutation
from graphene.types.mutation import MutationOptions
from graphene.types.utils import yank_fields_from_attrs
from graphene_django.registry import get_global_registry

from .converter import convert_form_field
from .types import ErrorType


def fields_for_form(form, only_fields, exclude_fields):
fields = OrderedDict()
for name, field in form.fields.items():
is_not_in_only = only_fields and name not in only_fields
is_excluded = (
name in exclude_fields # or
# name in already_created_fields
)

if is_not_in_only or is_excluded:
continue

fields[name] = convert_form_field(field)
return fields


class BaseDjangoFormMutation(ClientIDMutation):
class Meta:
abstract = True

@classmethod
def mutate_and_get_payload(cls, root, info, **input):
form = cls.get_form(root, info, **input)

if form.is_valid():
return cls.perform_mutate(form, info)
else:
errors = [
ErrorType(field=key, messages=value)
for key, value in form.errors.items()
]

return cls(errors=errors)

@classmethod
def get_form(cls, root, info, **input):
form_kwargs = cls.get_form_kwargs(root, info, **input)
return cls._meta.form_class(**form_kwargs)

@classmethod
def get_form_kwargs(cls, root, info, **input):
kwargs = {'data': input}

pk = input.pop('id', None)
if pk:
instance = cls._meta.model._default_manager.get(pk=pk)
kwargs['instance'] = instance

return kwargs


class DjangoFormMutationOptions(MutationOptions):
form_class = None


class DjangoFormMutation(BaseDjangoFormMutation):
class Meta:
abstract = True

errors = graphene.List(ErrorType)

@classmethod
def __init_subclass_with_meta__(cls, form_class=None,
only_fields=(), exclude_fields=(), **options):

if not form_class:
raise Exception('form_class is required for DjangoFormMutation')

form = form_class()
input_fields = fields_for_form(form, only_fields, exclude_fields)
output_fields = fields_for_form(form, only_fields, exclude_fields)

_meta = DjangoFormMutationOptions(cls)
_meta.form_class = form_class
_meta.fields = yank_fields_from_attrs(
output_fields,
_as=Field,
)

input_fields = yank_fields_from_attrs(
input_fields,
_as=InputField,
)
super(DjangoFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options)

@classmethod
def perform_mutate(cls, form, info):
form.save()
return cls(errors=[])


class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions):
model = None
return_field_name = None


class DjangoModelFormMutation(BaseDjangoFormMutation):
class Meta:
abstract = True

errors = graphene.List(ErrorType)

@classmethod
def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_name=None,
only_fields=(), exclude_fields=(), **options):

if not form_class:
raise Exception('form_class is required for DjangoModelFormMutation')

if not model:
model = form_class._meta.model

if not model:
raise Exception('model is required for DjangoModelFormMutation')

form = form_class()
input_fields = fields_for_form(form, only_fields, exclude_fields)
input_fields['id'] = graphene.ID()

registry = get_global_registry()
model_type = registry.get_type_for_model(model)
return_field_name = return_field_name
if not return_field_name:
model_name = model.__name__
return_field_name = model_name[:1].lower() + model_name[1:]

output_fields = OrderedDict()
output_fields[return_field_name] = graphene.Field(model_type)

_meta = DjangoModelDjangoFormMutationOptions(cls)
_meta.form_class = form_class
_meta.model = model
_meta.return_field_name = return_field_name
_meta.fields = yank_fields_from_attrs(
output_fields,
_as=Field,
)

input_fields = yank_fields_from_attrs(
input_fields,
_as=InputField,
)
super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(
_meta=_meta,
input_fields=input_fields,
**options
)

@classmethod
def perform_mutate(cls, form, info):
obj = form.save()
kwargs = {cls._meta.return_field_name: obj}
return cls(errors=[], **kwargs)
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import graphene
from graphene import ID, List, NonNull

from ..form_converter import convert_form_field
from .models import Reporter
from ..converter import convert_form_field


def assert_conversion(django_field, graphene_field, *args):
Expand All @@ -24,15 +23,15 @@ def test_should_unknown_django_field_raise_exception():


def test_should_date_convert_string():
assert_conversion(forms.DateField, graphene.String)
assert_conversion(forms.DateField, graphene.types.datetime.DateTime)


def test_should_time_convert_string():
assert_conversion(forms.TimeField, graphene.String)
assert_conversion(forms.TimeField, graphene.types.datetime.Time)


def test_should_date_time_convert_string():
assert_conversion(forms.DateTimeField, graphene.String)
assert_conversion(forms.DateTimeField, graphene.types.datetime.DateTime)


def test_should_char_convert_string():
Expand Down Expand Up @@ -91,13 +90,13 @@ def test_should_decimal_convert_float():


def test_should_multiple_choice_convert_connectionorlist():
field = forms.ModelMultipleChoiceField(Reporter.objects.all())
field = forms.ModelMultipleChoiceField(queryset=None)
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, List)
assert graphene_type.of_type == ID


def test_should_manytoone_convert_connectionorlist():
field = forms.ModelChoiceField(Reporter.objects.all())
field = forms.ModelChoiceField(queryset=None)
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, graphene.ID)
Loading