From f034946cda62f5a0e2477c02684664a97a54b1bc Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 10:15:25 -0500 Subject: [PATCH 01/13] Add Django form-based mutations --- graphene_django/forms/__init__.py | 1 + graphene_django/forms/converter.py | 90 +++++++++++ graphene_django/{ => forms}/forms.py | 0 graphene_django/forms/mutation.py | 157 +++++++++++++++++++ graphene_django/forms/tests/__init__.py | 0 graphene_django/forms/tests/test_coverter.py | 98 ++++++++++++ graphene_django/forms/tests/test_mutation.py | 49 ++++++ graphene_django/forms/types.py | 6 + 8 files changed, 401 insertions(+) create mode 100644 graphene_django/forms/__init__.py create mode 100644 graphene_django/forms/converter.py rename graphene_django/{ => forms}/forms.py (100%) create mode 100644 graphene_django/forms/mutation.py create mode 100644 graphene_django/forms/tests/__init__.py create mode 100644 graphene_django/forms/tests/test_coverter.py create mode 100644 graphene_django/forms/tests/test_mutation.py create mode 100644 graphene_django/forms/types.py diff --git a/graphene_django/forms/__init__.py b/graphene_django/forms/__init__.py new file mode 100644 index 000000000..9559be4a4 --- /dev/null +++ b/graphene_django/forms/__init__.py @@ -0,0 +1 @@ +from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py new file mode 100644 index 000000000..9d7b2428a --- /dev/null +++ b/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) diff --git a/graphene_django/forms.py b/graphene_django/forms/forms.py similarity index 100% rename from graphene_django/forms.py rename to graphene_django/forms/forms.py diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py new file mode 100644 index 000000000..d9e27aa0b --- /dev/null +++ b/graphene_django/forms/mutation.py @@ -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) diff --git a/graphene_django/forms/tests/__init__.py b/graphene_django/forms/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/graphene_django/forms/tests/test_coverter.py b/graphene_django/forms/tests/test_coverter.py new file mode 100644 index 000000000..e4a686b3c --- /dev/null +++ b/graphene_django/forms/tests/test_coverter.py @@ -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 diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py new file mode 100644 index 000000000..004f5d31d --- /dev/null +++ b/graphene_django/forms/tests/test_mutation.py @@ -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' diff --git a/graphene_django/forms/types.py b/graphene_django/forms/types.py new file mode 100644 index 000000000..1fe33f38e --- /dev/null +++ b/graphene_django/forms/types.py @@ -0,0 +1,6 @@ +import graphene + + +class ErrorType(graphene.ObjectType): + field = graphene.String() + messages = graphene.List(graphene.String) From 26a4d315c5960fc3e04ab59dff735caac3d7b221 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 10:31:17 -0500 Subject: [PATCH 02/13] Use options correctly --- graphene_django/forms/mutation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index d9e27aa0b..291a7af6e 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -46,7 +46,7 @@ def __new__(cls, name, bases, attrs): cls.Input = convert_form_to_input_type(options.form_class) - field_kwargs = {cls.options.input_field_name: Argument(cls.Input, required=True)} + field_kwargs = {options.input_field_name: Argument(cls.Input, required=True)} cls.Field = partial( Field, cls, @@ -127,7 +127,7 @@ def __new__(cls, name, bases, attrs): cls.Input = convert_form_to_input_type(options.form_class) - field_kwargs = {cls.options.input_field_name: Argument(cls.Input, required=True)} + field_kwargs = {options.input_field_name: Argument(cls.Input, required=True)} cls.Field = partial( Field, cls, From 80a06a035488b43f709373672aef5e53840f01d3 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 10:37:08 -0500 Subject: [PATCH 03/13] Fix flake8 issue --- graphene_django/forms/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/forms/__init__.py b/graphene_django/forms/__init__.py index 9559be4a4..066eec444 100644 --- a/graphene_django/forms/__init__.py +++ b/graphene_django/forms/__init__.py @@ -1 +1 @@ -from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa From 4f904f470088bbafbebe8d5158852a36d9111aa7 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 10:48:09 -0500 Subject: [PATCH 04/13] Test mutation --- graphene_django/forms/tests/test_mutation.py | 26 +++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 004f5d31d..9874ae653 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -1,4 +1,5 @@ from django import forms +from django.test import TestCase from py.test import raises from graphene_django.tests.models import Pet @@ -40,10 +41,23 @@ class Meta: assert 'text' in MyMutation.Input._meta.fields -def test_model_form(): - class PetMutation(ModelFormMutation): - class Meta: - form_class = PetForm +class ModelFormMutationTests(TestCase): + + def test_model_form_mutation(self): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + + self.assertEqual(PetMutation.model, Pet) + self.assertEqual(PetMutation.return_field_name, 'pet') + + def test_model_form_mutation_mutate(self): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + + PetMutation.mutate(None, {'input': {'name': 'Fluffy'}}, None, None) - assert PetMutation.model == Pet - assert PetMutation.return_field_name == 'pet' + self.assertEqual(Pet.objects.count(), 1) + pet = Pet.objects.get() + self.assertEqual(pet.name, 'Fluffy') From 507246468b07bddaaac63f4bf2f72013bdadbc23 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 11:18:58 -0500 Subject: [PATCH 05/13] Document form mutations --- docs/form-mutations.rst | 67 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 68 insertions(+) create mode 100644 docs/form-mutations.rst diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst new file mode 100644 index 000000000..f010d8a46 --- /dev/null +++ b/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 ``perform_mutate(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. diff --git a/docs/index.rst b/docs/index.rst index 256da680d..7c64ae709 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,4 +12,5 @@ Contents: authorization debug rest-framework + form-mutations introspection From f5083cb1901596b4a41ab6484ec3c89013d6c245 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 11:20:59 -0500 Subject: [PATCH 06/13] Change form valid method names --- docs/form-mutations.rst | 2 +- graphene_django/forms/mutation.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst index f010d8a46..a498b5669 100644 --- a/docs/form-mutations.rst +++ b/docs/form-mutations.rst @@ -60,7 +60,7 @@ Form validation Form mutations will call ``is_valid()`` on your forms. -If the form is valid then ``perform_mutate(form, info)`` is called on the mutation. Override this method to change how +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 diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 291a7af6e..43726d4bf 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -64,19 +64,23 @@ 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) + return cls.form_valid(form, info) else: - errors = [ - ErrorType(field=key, messages=value) - for key, value in form.errors.items() - ] - return cls(errors=errors) + return cls.form_invalid(form, info) @classmethod - def perform_mutate(cls, form, info): + 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) @@ -151,7 +155,7 @@ class ModelFormMutation(six.with_metaclass(ModelFormMutationMeta, BaseFormMutati errors = graphene.List(ErrorType) @classmethod - def perform_mutate(cls, form, info): + def form_valid(cls, form, info): obj = form.save() kwargs = {cls.return_field_name: obj} return cls(errors=[], **kwargs) From 666ddb2ff38eae171ed20ef8b467cd2cb0eaa8e0 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Mon, 2 Oct 2017 09:42:57 -0500 Subject: [PATCH 07/13] Merge form converter modules --- graphene_django/filter/utils.py | 2 +- graphene_django/form_converter.py | 75 ------------- graphene_django/forms/converter.py | 100 ++++++++++-------- .../tests/test_converter.py} | 2 +- 4 files changed, 58 insertions(+), 121 deletions(-) delete mode 100644 graphene_django/form_converter.py rename graphene_django/{tests/test_form_converter.py => forms/tests/test_converter.py} (98%) diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index 6b938ce40..cfa5621a1 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -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): diff --git a/graphene_django/form_converter.py b/graphene_django/form_converter.py deleted file mode 100644 index 46a38b32a..000000000 --- a/graphene_django/form_converter.py +++ /dev/null @@ -1,75 +0,0 @@ -from django import forms -from django.forms.fields import BaseTemporalField - -from graphene import ID, Boolean, Float, Int, List, String, UUID - -from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField -from .utils import import_single_dispatch - -singledispatch = import_single_dispatch() - -try: - UUIDField = forms.UUIDField -except AttributeError: - class UUIDField(object): - pass - - -@singledispatch -def convert_form_field(field): - raise Exception( - "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.CharField) -@convert_form_field.register(forms.EmailField) -@convert_form_field.register(forms.SlugField) -@convert_form_field.register(forms.URLField) -@convert_form_field.register(forms.ChoiceField) -@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) - - -@convert_form_field.register(UUIDField) -def convert_form_field_to_uuid(field): - return 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) - - -@convert_form_field.register(forms.BooleanField) -def convert_form_field_to_boolean(field): - return 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) - - -@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) - - -@convert_form_field.register(forms.ModelMultipleChoiceField) -@convert_form_field.register(GlobalIDMultipleChoiceField) -def convert_form_field_to_list(field): - return List(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) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 9d7b2428a..7e816d6eb 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -1,8 +1,17 @@ from django import forms from django.core.exceptions import ImproperlyConfigured -from graphene_django.utils import import_single_dispatch + import graphene +from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from .utils import import_single_dispatch + +try: + UUIDField = forms.UUIDField +except AttributeError: + class UUIDField(object): + pass + singledispatch = import_single_dispatch() @@ -23,68 +32,71 @@ def convert_form_to_input_type(form_class): @singledispatch -def get_graphene_type_from_form_field(field): +def convert_form_field(field): raise ImproperlyConfigured( - "Don't know how to convert the form field %s (%s) " - "to Graphene type" % (field, field.__class__) + "Don't know how to convert the Django 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 - """ +@convert_form_field.register(forms.BaseTemporalField) +@convert_form_field.register(forms.CharField) +@convert_form_field.register(forms.EmailField) +@convert_form_field.register(forms.SlugField) +@convert_form_field.register(forms.URLField) +@convert_form_field.register(forms.ChoiceField) +@convert_form_field.register(forms.RegexField) +@convert_form_field.register(forms.Field) +def convert_form_field_to_string(field): + return graphene.String(description=field.help_text, required=field.required) - graphql_type = get_graphene_type_from_form_field(field) - kwargs = { - 'description': field.help_text, - 'required': is_input and field.required, - } +@convert_form_field.register(UUIDField) +def convert_form_field_to_uuid(field): + return graphene.UUID(description=field.help_text, required=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) +@convert_form_field.register(forms.IntegerField) +@convert_form_field.register(forms.NumberInput) +def convert_form_field_to_int(field): + return graphene.Int(description=field.help_text, required=field.required) -@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 +@convert_form_field.register(forms.BooleanField) +def convert_form_field_to_boolean(field): + return graphene.Boolean(description=field.help_text, required=True) -@get_graphene_type_from_form_field.register(forms.IntegerField) -def convert_form_field_to_int(field): - return graphene.Int +@convert_form_field.register(forms.NullBooleanField) +def convert_form_field_to_nullboolean(field): + return graphene.Boolean(description=field.help_text) -@get_graphene_type_from_form_field.register(forms.BooleanField) -def convert_form_field_to_bool(field): - return graphene.Boolean +@convert_form_field.register(forms.DecimalField) +@convert_form_field.register(forms.FloatField) +def convert_form_field_to_float(field): + return graphene.Float(description=field.help_text, required=field.required) -@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 +@convert_form_field.register(forms.ModelMultipleChoiceField) +@convert_form_field.register(GlobalIDMultipleChoiceField) +def convert_form_field_to_list(field): + return graphene.List(graphene.ID, required=field.required) -@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 +@convert_form_field.register(forms.ModelChoiceField) +@convert_form_field.register(GlobalIDFormField) +def convert_form_field_to_id(field): + return graphene.ID(required=field.required) -@get_graphene_type_from_form_field.register(forms.TimeField) -def convert_form_field_to_time(field): - return graphene.types.datetime.Time +@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) -@get_graphene_type_from_form_field.register(forms.MultipleChoiceField) -def convert_form_field_to_list_of_string(field): - return (graphene.List, graphene.String) +@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) diff --git a/graphene_django/tests/test_form_converter.py b/graphene_django/forms/tests/test_converter.py similarity index 98% rename from graphene_django/tests/test_form_converter.py rename to graphene_django/forms/tests/test_converter.py index 5a13554b1..ec66f8fa6 100644 --- a/graphene_django/tests/test_form_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -4,7 +4,7 @@ import graphene from graphene import ID, List, NonNull -from ..form_converter import convert_form_field +from ..converter import convert_form_field from .models import Reporter From 463ce68b16b070c0a49637dc71755a33acdf9d49 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Mon, 2 Oct 2017 13:03:20 -0500 Subject: [PATCH 08/13] Change mutations to new 2.0 format --- graphene_django/forms/converter.py | 4 +- graphene_django/forms/mutation.py | 216 ++++++++---------- graphene_django/forms/tests/test_converter.py | 11 +- graphene_django/forms/tests/test_coverter.py | 98 -------- graphene_django/forms/tests/test_mutation.py | 26 ++- 5 files changed, 124 insertions(+), 231 deletions(-) delete mode 100644 graphene_django/forms/tests/test_coverter.py diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 7e816d6eb..220edc40a 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -4,7 +4,7 @@ import graphene from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField -from .utils import import_single_dispatch +from ..utils import import_single_dispatch try: UUIDField = forms.UUIDField @@ -40,7 +40,7 @@ def convert_form_field(field): ) -@convert_form_field.register(forms.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) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 43726d4bf..bbe5f8019 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -1,161 +1,141 @@ -from functools import partial +from collections import OrderedDict -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 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_to_input_type +from .converter import convert_form_field 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 +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 not options.form_class: - raise Exception('Missing form_class') + if is_not_in_only or is_excluded: + continue - 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) + fields[name] = convert_form_field(field) + return fields - 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): +class BaseFormMutation(ClientIDMutation): + class Meta: + abstract = True @classmethod - def mutate(cls, root, args, context, info): - form = cls.get_form(root, args, context, info) + def mutate_and_get_payload(cls, root, info, **input): + form = cls._meta.form_class(data=input) if form.is_valid(): - return cls.form_valid(form, info) + return cls.perform_mutate(form, info) else: - return cls.form_invalid(form, info) + errors = [ + ErrorType(field=key, messages=value) + for key, value in form.errors.items() + ] - @classmethod - def form_valid(cls, form, info): - form.save() - return cls(errors=[]) + return cls(errors=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) +class FormMutationOptions(MutationOptions): + form_class = None - @classmethod - def get_form_kwargs(cls, root, args, context, info): - return {} - -class FormMutation(six.with_metaclass(FormMutationMeta, BaseFormMutation)): +class FormMutation(BaseFormMutation): + class Meta: + abstract = True errors = graphene.List(ErrorType) + @classmethod + def __init_subclass_with_meta__(cls, form_class=None, + only_fields=(), exclude_fields=(), **options): -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 form_class: + raise Exception('form_class is required for FormMutation') - if not options.form_class: - raise Exception('Missing form_class') + form = form_class() + input_fields = fields_for_form(form, only_fields, exclude_fields) + output_fields = fields_for_form(form, only_fields, exclude_fields) - cls = ObjectTypeMeta.__new__( - cls, name, bases, dict(attrs, _meta=options) + _meta = FormMutationOptions(cls) + _meta.form_class = form_class + _meta.fields = yank_fields_from_attrs( + output_fields, + _as=Field, ) - options.fields = merge( - options.interface_fields, options.base_fields, options.local_fields, - {'errors': get_field_as(cls.errors, Field)} + input_fields = yank_fields_from_attrs( + input_fields, + _as=InputField, ) + super(FormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) - cls.Input = convert_form_to_input_type(options.form_class) + @classmethod + def perform_mutate(cls, form, info): + form.save() + return cls(errors=None) - 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 +class ModelFormMutationOptions(FormMutationOptions): + model = None + return_field_name = None - registry = get_global_registry() - model_type = registry.get_type_for_model(cls.model) - options.fields[cls.return_field_name] = graphene.Field(model_type) +class ModelFormMutation(BaseFormMutation): + class Meta: + abstract = True - return cls + 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): -class ModelFormMutation(six.with_metaclass(ModelFormMutationMeta, BaseFormMutation)): + if not form_class: + raise Exception('form_class is required for ModelFormMutation') - errors = graphene.List(ErrorType) + if not model: + model = form_class._meta.model + + if not model: + raise Exception('model is required for ModelFormMutation') + + form = form_class() + input_fields = fields_for_form(form, only_fields, exclude_fields) + + registry = get_global_registry() + model_type = registry.get_type_for_model(model) + return_field_name = return_field_name or model._meta.model_name + output_fields = OrderedDict() + output_fields[return_field_name] = graphene.Field(model_type) + + _meta = ModelFormMutationOptions(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(ModelFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) @classmethod - def form_valid(cls, form, info): + def perform_mutate(cls, form, info): obj = form.save() - kwargs = {cls.return_field_name: obj} - return cls(errors=[], **kwargs) + kwargs = {cls._meta.return_field_name: obj} + return cls(errors=None, **kwargs) diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py index ec66f8fa6..f0020315d 100644 --- a/graphene_django/forms/tests/test_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -5,7 +5,6 @@ from graphene import ID, List, NonNull from ..converter import convert_form_field -from .models import Reporter def assert_conversion(django_field, graphene_field, *args): @@ -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(): @@ -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) diff --git a/graphene_django/forms/tests/test_coverter.py b/graphene_django/forms/tests/test_coverter.py deleted file mode 100644 index e4a686b3c..000000000 --- a/graphene_django/forms/tests/test_coverter.py +++ /dev/null @@ -1,98 +0,0 @@ -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 diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 9874ae653..5f1ef4aea 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -2,7 +2,7 @@ from django.test import TestCase from py.test import raises -from graphene_django.tests.models import Pet +from graphene_django.tests.models import Pet, Film from ..mutation import FormMutation, ModelFormMutation @@ -22,10 +22,10 @@ def test_needs_form_class(): class MyMutation(FormMutation): pass - assert exc.value.args[0] == 'Missing form_class' + assert exc.value.args[0] == 'form_class is required for FormMutation' -def test_has_fields(): +def test_has_output_fields(): class MyMutation(FormMutation): class Meta: form_class = MyForm @@ -43,20 +43,32 @@ class Meta: class ModelFormMutationTests(TestCase): - def test_model_form_mutation(self): + def test_default_meta_fields(self): class PetMutation(ModelFormMutation): class Meta: form_class = PetForm - self.assertEqual(PetMutation.model, Pet) - self.assertEqual(PetMutation.return_field_name, 'pet') + self.assertEqual(PetMutation._meta.model, Pet) + self.assertEqual(PetMutation._meta.return_field_name, 'pet') + self.assertIn('pet', PetMutation._meta.fields) + + def test_custom_return_field_name(self): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + model = Film + return_field_name = 'animal' + + self.assertEqual(PetMutation._meta.model, Film) + self.assertEqual(PetMutation._meta.return_field_name, 'animal') + self.assertIn('animal', PetMutation._meta.fields) def test_model_form_mutation_mutate(self): class PetMutation(ModelFormMutation): class Meta: form_class = PetForm - PetMutation.mutate(None, {'input': {'name': 'Fluffy'}}, None, None) + PetMutation.mutate_and_get_payload(None, None, name='Fluffy') self.assertEqual(Pet.objects.count(), 1) pet = Pet.objects.get() From bf7ad7eeda4c493d2b5954ee69232ec052eeaea6 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Mon, 2 Oct 2017 13:15:29 -0500 Subject: [PATCH 09/13] Test invalid forms --- graphene_django/forms/converter.py | 15 --------------- graphene_django/forms/mutation.py | 4 ++-- graphene_django/forms/tests/test_mutation.py | 17 ++++++++++++++++- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 220edc40a..9d878110f 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -16,21 +16,6 @@ class UUIDField(object): 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 convert_form_field(field): raise ImproperlyConfigured( diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index bbe5f8019..49fabb87c 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -83,7 +83,7 @@ def __init_subclass_with_meta__(cls, form_class=None, @classmethod def perform_mutate(cls, form, info): form.save() - return cls(errors=None) + return cls(errors=[]) class ModelFormMutationOptions(FormMutationOptions): @@ -138,4 +138,4 @@ def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_n def perform_mutate(cls, form, info): obj = form.save() kwargs = {cls._meta.return_field_name: obj} - return cls(errors=None, **kwargs) + return cls(errors=[], **kwargs) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 5f1ef4aea..084b8b067 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -68,8 +68,23 @@ class PetMutation(ModelFormMutation): class Meta: form_class = PetForm - PetMutation.mutate_and_get_payload(None, None, name='Fluffy') + result = PetMutation.mutate_and_get_payload(None, None, name='Fluffy') self.assertEqual(Pet.objects.count(), 1) pet = Pet.objects.get() self.assertEqual(pet.name, 'Fluffy') + self.assertEqual(result.errors, []) + + def test_model_form_mutation_mutate_invalid_form(self): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + + result = PetMutation.mutate_and_get_payload(None, None) + + # A pet was not created + self.assertEqual(Pet.objects.count(), 0) + + self.assertEqual(len(result.errors), 1) + self.assertEqual(result.errors[0].field, 'name') + self.assertEqual(result.errors[0].messages, ['This field is required.']) From 40610c64a3be003719d88db439e442085ed18072 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 14:12:02 -0600 Subject: [PATCH 10/13] Support instance kwarg --- graphene_django/forms/mutation.py | 40 ++++++++++++++------ graphene_django/forms/tests/test_mutation.py | 34 ++++++++++++----- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 49fabb87c..876e76ae5 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -27,13 +27,13 @@ def fields_for_form(form, only_fields, exclude_fields): return fields -class BaseFormMutation(ClientIDMutation): +class BaseDjangoFormMutation(ClientIDMutation): class Meta: abstract = True @classmethod def mutate_and_get_payload(cls, root, info, **input): - form = cls._meta.form_class(data=input) + form = cls.get_form(root, info, **input) if form.is_valid(): return cls.perform_mutate(form, info) @@ -45,12 +45,28 @@ def mutate_and_get_payload(cls, root, info, **input): 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 FormMutationOptions(MutationOptions): +class DjangoFormMutationOptions(MutationOptions): form_class = None -class FormMutation(BaseFormMutation): +class DjangoFormMutation(BaseDjangoFormMutation): class Meta: abstract = True @@ -67,7 +83,7 @@ def __init_subclass_with_meta__(cls, form_class=None, input_fields = fields_for_form(form, only_fields, exclude_fields) output_fields = fields_for_form(form, only_fields, exclude_fields) - _meta = FormMutationOptions(cls) + _meta = DjangoFormMutationOptions(cls) _meta.form_class = form_class _meta.fields = yank_fields_from_attrs( output_fields, @@ -78,7 +94,7 @@ def __init_subclass_with_meta__(cls, form_class=None, input_fields, _as=InputField, ) - super(FormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) + super(DjangoFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) @classmethod def perform_mutate(cls, form, info): @@ -86,12 +102,12 @@ def perform_mutate(cls, form, info): return cls(errors=[]) -class ModelFormMutationOptions(FormMutationOptions): +class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions): model = None return_field_name = None -class ModelFormMutation(BaseFormMutation): +class DjangoModelFormMutation(BaseDjangoFormMutation): class Meta: abstract = True @@ -102,13 +118,13 @@ def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_n only_fields=(), exclude_fields=(), **options): if not form_class: - raise Exception('form_class is required for ModelFormMutation') + 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 ModelFormMutation') + raise Exception('model is required for DjangoModelFormMutation') form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) @@ -119,7 +135,7 @@ def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_n output_fields = OrderedDict() output_fields[return_field_name] = graphene.Field(model_type) - _meta = ModelFormMutationOptions(cls) + _meta = DjangoModelDjangoFormMutationOptions(cls) _meta.form_class = form_class _meta.model = model _meta.return_field_name = return_field_name @@ -132,7 +148,7 @@ def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_n input_fields, _as=InputField, ) - super(ModelFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) + super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) @classmethod def perform_mutate(cls, form, info): diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 084b8b067..58764051b 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -3,7 +3,7 @@ from py.test import raises from graphene_django.tests.models import Pet, Film -from ..mutation import FormMutation, ModelFormMutation +from ..mutation import DjangoFormMutation, DjangoModelFormMutation class MyForm(forms.Form): @@ -19,14 +19,14 @@ class Meta: def test_needs_form_class(): with raises(Exception) as exc: - class MyMutation(FormMutation): + class MyMutation(DjangoFormMutation): pass assert exc.value.args[0] == 'form_class is required for FormMutation' def test_has_output_fields(): - class MyMutation(FormMutation): + class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm @@ -34,7 +34,7 @@ class Meta: def test_has_input_fields(): - class MyMutation(FormMutation): + class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm @@ -44,7 +44,7 @@ class Meta: class ModelFormMutationTests(TestCase): def test_default_meta_fields(self): - class PetMutation(ModelFormMutation): + class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm @@ -53,7 +53,7 @@ class Meta: self.assertIn('pet', PetMutation._meta.fields) def test_custom_return_field_name(self): - class PetMutation(ModelFormMutation): + class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm model = Film @@ -64,19 +64,33 @@ class Meta: self.assertIn('animal', PetMutation._meta.fields) def test_model_form_mutation_mutate(self): - class PetMutation(ModelFormMutation): + class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm - result = PetMutation.mutate_and_get_payload(None, None, name='Fluffy') + pet = Pet.objects.create(name='Axel') + + result = PetMutation.mutate_and_get_payload(None, None, id=pet.pk, name='Mia') + + self.assertEqual(Pet.objects.count(), 1) + pet.refresh_from_db() + self.assertEqual(pet.name, 'Mia') + self.assertEqual(result.errors, []) + + def test_model_form_mutation_updates_existing_(self): + class PetMutation(DjangoModelFormMutation): + class Meta: + form_class = PetForm + + result = PetMutation.mutate_and_get_payload(None, None, name='Mia') self.assertEqual(Pet.objects.count(), 1) pet = Pet.objects.get() - self.assertEqual(pet.name, 'Fluffy') + self.assertEqual(pet.name, 'Mia') self.assertEqual(result.errors, []) def test_model_form_mutation_mutate_invalid_form(self): - class PetMutation(ModelFormMutation): + class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm From d6dbe2a4a81dd069d12bfb75f8e997f8105d7335 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 14:20:56 -0600 Subject: [PATCH 11/13] Default return_field_name is camcelcased --- graphene_django/forms/mutation.py | 6 +++++- graphene_django/forms/tests/test_mutation.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 876e76ae5..58e007581 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -131,7 +131,11 @@ def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_n registry = get_global_registry() model_type = registry.get_type_for_model(model) - return_field_name = return_field_name or model._meta.model_name + 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) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 58764051b..3bfb883f8 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -2,7 +2,7 @@ from django.test import TestCase from py.test import raises -from graphene_django.tests.models import Pet, Film +from graphene_django.tests.models import Pet, Film, FilmDetails from ..mutation import DjangoFormMutation, DjangoModelFormMutation @@ -52,6 +52,15 @@ class Meta: self.assertEqual(PetMutation._meta.return_field_name, 'pet') self.assertIn('pet', PetMutation._meta.fields) + def test_return_field_name_is_camelcased(self): + class PetMutation(DjangoModelFormMutation): + class Meta: + form_class = PetForm + model = FilmDetails + + self.assertEqual(PetMutation._meta.model, FilmDetails) + self.assertEqual(PetMutation._meta.return_field_name, 'filmDetails') + def test_custom_return_field_name(self): class PetMutation(DjangoModelFormMutation): class Meta: From 748dc4c50994c99301143aaff36035ac513309f6 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 15:15:16 -0600 Subject: [PATCH 12/13] Add id input field to model form mutation --- graphene_django/forms/mutation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 58e007581..e8e76a2d4 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -77,7 +77,7 @@ 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 FormMutation') + raise Exception('form_class is required for DjangoFormMutation') form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) @@ -128,6 +128,7 @@ def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_n form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) + input_fields['id'] = convert_form_field(model._meta.pk) registry = get_global_registry() model_type = registry.get_type_for_model(model) From 6d7a0d053dfff709dabef85d5509a648df594715 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 15:25:24 -0600 Subject: [PATCH 13/13] Make id field an ID type --- graphene_django/forms/mutation.py | 2 +- graphene_django/forms/tests/test_mutation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index e8e76a2d4..793aafefb 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -128,7 +128,7 @@ def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_n form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) - input_fields['id'] = convert_form_field(model._meta.pk) + input_fields['id'] = graphene.ID() registry = get_global_registry() model_type = registry.get_type_for_model(model) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 3bfb883f8..10a15ae0a 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -22,7 +22,7 @@ def test_needs_form_class(): class MyMutation(DjangoFormMutation): pass - assert exc.value.args[0] == 'form_class is required for FormMutation' + assert exc.value.args[0] == 'form_class is required for DjangoFormMutation' def test_has_output_fields():