mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-02-16 19:40:36 +03:00
Add Django form-based mutations
This commit is contained in:
parent
f35e445963
commit
f034946cda
1
graphene_django/forms/__init__.py
Normal file
1
graphene_django/forms/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
90
graphene_django/forms/converter.py
Normal file
90
graphene_django/forms/converter.py
Normal file
|
@ -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)
|
157
graphene_django/forms/mutation.py
Normal file
157
graphene_django/forms/mutation.py
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
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 = {cls.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.perform_mutate(form, info)
|
||||||
|
else:
|
||||||
|
errors = [
|
||||||
|
ErrorType(field=key, messages=value)
|
||||||
|
for key, value in form.errors.items()
|
||||||
|
]
|
||||||
|
return cls(errors=errors)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def perform_mutate(cls, form, info):
|
||||||
|
form.save()
|
||||||
|
return cls(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 = {cls.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 perform_mutate(cls, form, info):
|
||||||
|
obj = form.save()
|
||||||
|
kwargs = {cls.return_field_name: obj}
|
||||||
|
return cls(errors=[], **kwargs)
|
0
graphene_django/forms/tests/__init__.py
Normal file
0
graphene_django/forms/tests/__init__.py
Normal file
98
graphene_django/forms/tests/test_coverter.py
Normal file
98
graphene_django/forms/tests/test_coverter.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from ..converter import convert_form_field
|
||||||
|
|
||||||
|
|
||||||
|
def _get_type(form_field, **kwargs):
|
||||||
|
# prevents the following error:
|
||||||
|
# AssertionError: The `source` argument is not meaningful when applied to a `child=` field.
|
||||||
|
# Remove `source=` from the field declaration.
|
||||||
|
# since we are reusing the same child in when testing the required attribute
|
||||||
|
|
||||||
|
if 'child' in kwargs:
|
||||||
|
kwargs['child'] = copy.deepcopy(kwargs['child'])
|
||||||
|
|
||||||
|
field = form_field(**kwargs)
|
||||||
|
|
||||||
|
return convert_form_field(field)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_conversion(form_field, graphene_field, **kwargs):
|
||||||
|
graphene_type = _get_type(form_field, help_text='Custom Help Text', **kwargs)
|
||||||
|
assert isinstance(graphene_type, graphene_field)
|
||||||
|
|
||||||
|
graphene_type_required = _get_type(
|
||||||
|
form_field, help_text='Custom Help Text', required=True, **kwargs
|
||||||
|
)
|
||||||
|
assert isinstance(graphene_type_required, graphene_field)
|
||||||
|
|
||||||
|
return graphene_type
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_unknown_form_field_raise_exception():
|
||||||
|
with raises(Exception) as excinfo:
|
||||||
|
convert_form_field(None)
|
||||||
|
assert 'Don\'t know how to convert the form field' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_charfield_convert_string():
|
||||||
|
assert_conversion(forms.CharField, graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_timefield_convert_time():
|
||||||
|
assert_conversion(forms.TimeField, graphene.types.datetime.Time)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_email_convert_string():
|
||||||
|
assert_conversion(forms.EmailField, graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_slug_convert_string():
|
||||||
|
assert_conversion(forms.SlugField, graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_url_convert_string():
|
||||||
|
assert_conversion(forms.URLField, graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_choicefield_convert_string():
|
||||||
|
assert_conversion(forms.ChoiceField, graphene.String, choices=[])
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_regexfield_convert_string():
|
||||||
|
assert_conversion(forms.RegexField, graphene.String, regex='[0-9]+')
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_uuidfield_convert_string():
|
||||||
|
assert_conversion(forms.UUIDField, graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_integer_convert_int():
|
||||||
|
assert_conversion(forms.IntegerField, graphene.Int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_boolean_convert_boolean():
|
||||||
|
assert_conversion(forms.BooleanField, graphene.Boolean)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_float_convert_float():
|
||||||
|
assert_conversion(forms.FloatField, graphene.Float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_decimal_convert_float():
|
||||||
|
assert_conversion(forms.DecimalField, graphene.Float, max_digits=4, decimal_places=2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_filepath_convert_string():
|
||||||
|
assert_conversion(forms.FilePathField, graphene.String, path='/')
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_multiplechoicefield_convert_to_list_of_string():
|
||||||
|
field = assert_conversion(forms.MultipleChoiceField, graphene.List, choices=[1, 2, 3])
|
||||||
|
|
||||||
|
assert field.of_type == graphene.String
|
49
graphene_django/forms/tests/test_mutation.py
Normal file
49
graphene_django/forms/tests/test_mutation.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
from django import forms
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
from graphene_django.tests.models import Pet
|
||||||
|
from ..mutation import FormMutation, ModelFormMutation
|
||||||
|
|
||||||
|
|
||||||
|
class MyForm(forms.Form):
|
||||||
|
text = forms.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class PetForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
def test_needs_form_class():
|
||||||
|
with raises(Exception) as exc:
|
||||||
|
class MyMutation(FormMutation):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert exc.value.args[0] == 'Missing form_class'
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_fields():
|
||||||
|
class MyMutation(FormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
assert 'errors' in MyMutation._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_input_fields():
|
||||||
|
class MyMutation(FormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
assert 'text' in MyMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_form():
|
||||||
|
class PetMutation(ModelFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
|
||||||
|
assert PetMutation.model == Pet
|
||||||
|
assert PetMutation.return_field_name == 'pet'
|
6
graphene_django/forms/types.py
Normal file
6
graphene_django/forms/types.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorType(graphene.ObjectType):
|
||||||
|
field = graphene.String()
|
||||||
|
messages = graphene.List(graphene.String)
|
Loading…
Reference in New Issue
Block a user