Skip to content

Update django form mutation to more granularly handle input/output fields #934

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/mutations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
64 changes: 61 additions & 3 deletions graphene_django/forms/mutation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# from django import forms
import warnings
from collections import OrderedDict

import graphene
Expand Down Expand Up @@ -71,22 +72,79 @@ 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

errors = graphene.List(ErrorType)

@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
Expand Down
88 changes: 87 additions & 1 deletion graphene_django/forms/tests/test_mutation.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -22,13 +23,17 @@ class Meta:

class MyForm(forms.Form):
text = forms.CharField()
another = forms.CharField(required=False)

def clean_text(self):
text = self.cleaned_data["text"]
if text == "INVALID_INPUT":
raise ValidationError("Invalid input")
return text

def clean_another(self):
self.cleaned_data["another"] = self.cleaned_data["another"] or "defaultvalue"

def save(self):
pass

Expand Down Expand Up @@ -68,6 +73,87 @@ class Meta:
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" }) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkimbo - I am super confused why this test errors with text not a valid keyword argument when the assertion statements above are true. Any thoughts?

>       assert result.errors is None
E       assert [GraphQLLocatedError("'text' is an invalid keyword argument for MyMutation")] is None
E        +  where [GraphQLLocatedError("'text' is an invalid keyword argument for MyMutation")] = <graphql.execution.base.ExecutionResult object at 0x103d25af8>.errors```

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):
Expand Down