diff --git a/docs/mutations.rst b/docs/mutations.rst index aef32eb..921130a 100644 --- a/docs/mutations.rst +++ b/docs/mutations.rst @@ -63,9 +63,15 @@ DjangoFormMutation class MyForm(forms.Form): name = forms.CharField() + def clean(self): + self.cleaned_data["constructed_output"] = "an item" + class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm + input_fields = "__all__" + + constructed_output = graphene.String() ``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string. diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 692f8d5..7b233b8 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -1,4 +1,5 @@ # from django import forms +import warnings from collections import OrderedDict import graphene @@ -71,6 +72,31 @@ class DjangoFormMutationOptions(MutationOptions): class DjangoFormMutation(BaseDjangoFormMutation): + """Create a mutation based on a Django Form. + + The form's fields will, by default, be set as inputs. Specify ``input_fields`` to limit to a + subset. + + You can use ``fields`` and ``exclude`` to limit output fields. Use ``fields = '__all__'`` to + select all fields. + + Fields are considered to be required based on the ``required`` attribute of the form. + + Meta fields: + form_class (class): the model to base form off of + input_fields (List[str], optional): limit the input fields of the form to be used (by default uses all of them) + fields (List[str], optional): only output the subset of fields as output (based on ``cleaned_data``), use + ``__all__`` to get all fields + exclude (List[str], optional): remove specified fields from output (uses ``cleaned_data``) + + The default output of the mutation will use ``form.cleaned_data`` as params. + + Override ``perform_mutate(cls, form, info) -> DjangoFormMutation`` to customize this behavior. + + NOTE: ``only_fields`` and ``exclude_fields`` are still supported for backwards compatibility + but are deprecated and will be removed in a future version. + """ + class Meta: abstract = True @@ -78,15 +104,37 @@ class DjangoFormMutation(BaseDjangoFormMutation): @classmethod def __init_subclass_with_meta__( - cls, form_class=None, only_fields=(), exclude_fields=(), **options + cls, form_class=None, only_fields=(), exclude_fields=(), + fields=None, exclude=(), input_fields=None, + **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) + if (any([fields, exclude, input_fields]) + and (only_fields or exclude_fields)): + raise Exception("Cannot specify legacy `only_fields` or `exclude_fields` params with" + " `only`, `exclude`, or `input_fields` params") + if only_fields or exclude_fields: + warnings.warn( + "only_fields/exclude_fields have been deprecated, use " + "input_fields or only/exclude (for output fields)" + "instead", + DeprecationWarning + ) + if not fields or exclude: + warnings.warn( + "a future version of graphene-django will require fields or exclude." + " Set fields='__all__' to allow all fields through.", + DeprecationWarning + ) + if not input_fields and input_fields is not None: + input_fields = {} + else: + input_fields = fields_for_form(form, only_fields or input_fields, exclude_fields) + output_fields = fields_for_form(form, only_fields or fields, exclude_fields or exclude) _meta = DjangoFormMutationOptions(cls) _meta.form_class = form_class diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index a455a0a..904f733 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -1,9 +1,10 @@ import pytest from django import forms +from django.test import TestCase from django.core.exceptions import ValidationError from py.test import raises -from graphene import Field, ObjectType, Schema, String +from graphene import Field, Int, ObjectType, Schema, String from graphene_django import DjangoObjectType from graphene_django.tests.models import Pet @@ -22,6 +23,7 @@ def pet_type(): class MyForm(forms.Form): text = forms.CharField() + another = forms.CharField(required=False) def clean_text(self): text = self.cleaned_data["text"] @@ -29,6 +31,9 @@ class MyForm(forms.Form): raise ValidationError("Invalid input") return text + def clean_another(self): + self.cleaned_data["another"] = self.cleaned_data["another"] or "defaultvalue" + def save(self): pass @@ -68,6 +73,83 @@ def test_has_input_fields(): form_class = MyForm assert "text" in MyMutation.Input._meta.fields + assert "another" in MyMutation.Input._meta.fields + + +def test_no_input_fields(): + class MyMutation(DjangoFormMutation): + class Meta: + form_class = MyForm + input_fields = [] + assert set(MyMutation.Input._meta.fields.keys()) == set(["client_mutation_id"]) + + +def test_filtering_input_fields(): + class MyMutation(DjangoFormMutation): + class Meta: + form_class = MyForm + input_fields = ["text"] + + assert "text" in MyMutation.Input._meta.fields + assert "another" not in MyMutation.Input._meta.fields + + +def test_select_output_fields(): + class MyMutation(DjangoFormMutation): + class Meta: + form_class = MyForm + fields = ["text"] + assert "text" in MyMutation._meta.fields + assert "another" not in MyMutation._meta.fields + + +def test_filtering_output_fields_exclude(): + class FormWithWeirdOutput(MyForm): + """Weird form that has extra cleaned_data we want to expose""" + text = forms.CharField() + another = forms.CharField(required=False) + def clean(self): + super(FormWithWeirdOutput, self).clean() + self.cleaned_data["some_integer"] = 5 + return self.cleaned_data + + class MyMutation(DjangoFormMutation): + class Meta: + form_class = FormWithWeirdOutput + exclude = ["text"] + + some_integer = Int() + + assert "text" in MyMutation.Input._meta.fields + assert "another" in MyMutation.Input._meta.fields + + assert "text" not in MyMutation._meta.fields + assert "another" in MyMutation._meta.fields + assert "some_integer" in MyMutation._meta.fields + + class Mutation(ObjectType): + my_mutation = MyMutation.Field() + + schema = Schema(query=MockQuery, mutation=Mutation) + + result = schema.execute( + """ mutation MyMutation { + myMutation(input: { text: "VALID_INPUT" }) { + errors { + field + messages + } + another + someInteger + } + } + """ + ) + + assert result.errors is None + assert result.data["myMutation"]["errors"] == [] + assert result.data["myMutation"]["someInteger"] == 5 + assert result.data["myMutation"]["another"] == "defaultvalue" def test_mutation_error_camelcased(pet_type, graphene_settings):