Skip to content

Commit 4246dab

Browse files
committed
Update django form mutation to more granularly handle fields
Set up forms Update docs Nearly working tests
1 parent a987035 commit 4246dab

File tree

3 files changed

+140
-4
lines changed

3 files changed

+140
-4
lines changed

docs/mutations.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,15 @@ DjangoFormMutation
6363
class MyForm(forms.Form):
6464
name = forms.CharField()
6565
66+
def clean(self):
67+
self.cleaned_data["constructed_output"] = "an item"
68+
6669
class MyMutation(DjangoFormMutation):
6770
class Meta:
6871
form_class = MyForm
72+
input_fields = "__all__"
73+
74+
constructed_output = graphene.String()
6975
7076
``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string.
7177

graphene_django/forms/mutation.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# from django import forms
2+
import warnings
23
from collections import OrderedDict
34

45
import graphene
@@ -71,22 +72,69 @@ class DjangoFormMutationOptions(MutationOptions):
7172

7273

7374
class DjangoFormMutation(BaseDjangoFormMutation):
75+
"""Create a mutation based on a Django Form.
76+
77+
The form's fields will, by default, be set as inputs. Specify ``input_fields`` to limit to a
78+
subset.
79+
80+
You can use ``fields`` and ``exclude`` to limit output fields. Use ``fields = '__all__'`` to
81+
select all fields.
82+
83+
Fields are considered to be required based on the ``required`` attribute of the form.
84+
85+
Meta fields:
86+
form_class (class): the model to base form off of
87+
input_fields (List[str], optional): limit the input fields of the form to be used (by default uses all of them)
88+
fields (List[str], optional): only output the subset of fields as output (based on ``cleaned_data``), use
89+
``__all__`` to get all fields
90+
exclude (List[str], optional): remove specified fields from output (uses ``cleaned_data``)
91+
92+
The default output of the mutation will use ``form.cleaned_data`` as params.
93+
94+
Override ``perform_mutate(cls, form, info) -> DjangoFormMutation`` to customize this behavior.
95+
96+
NOTE: ``only_fields`` and ``exclude_fields`` are still supported for backwards compatibility
97+
but are deprecated and will be removed in a future version.
98+
"""
99+
74100
class Meta:
75101
abstract = True
76102

77103
errors = graphene.List(ErrorType)
78104

79105
@classmethod
80106
def __init_subclass_with_meta__(
81-
cls, form_class=None, only_fields=(), exclude_fields=(), **options
107+
cls, form_class=None, only_fields=(), exclude_fields=(),
108+
fields=None, exclude=(), input_fields=None,
109+
**options
82110
):
83111

84112
if not form_class:
85113
raise Exception("form_class is required for DjangoFormMutation")
86114

87115
form = form_class()
88-
input_fields = fields_for_form(form, only_fields, exclude_fields)
89-
output_fields = fields_for_form(form, only_fields, exclude_fields)
116+
if (any([fields, exclude, input_fields])
117+
and (only_fields or exclude_fields)):
118+
raise Exception("Cannot specify legacy `only_fields` or `exclude_fields` params with"
119+
" `only`, `exclude`, or `input_fields` params")
120+
if only_fields or exclude_fields:
121+
warnings.warn(
122+
"only_fields/exclude_fields have been deprecated, use "
123+
"input_fields or only/exclude (for output fields)"
124+
"instead",
125+
DeprecationWarning
126+
)
127+
if not fields or exclude:
128+
warnings.warn(
129+
"a future version of graphene-django will require fields or exclude."
130+
" Set fields='__all__' to allow all fields through.",
131+
DeprecationWarning
132+
)
133+
if not input_fields and input_fields is not None:
134+
input_fields = {}
135+
else:
136+
input_fields = fields_for_form(form, only_fields or input_fields, exclude_fields)
137+
output_fields = fields_for_form(form, only_fields or fields, exclude_fields or exclude)
90138

91139
_meta = DjangoFormMutationOptions(cls)
92140
_meta.form_class = form_class

graphene_django/forms/tests/test_mutation.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import pytest
22
from django import forms
3+
from django.test import TestCase
34
from django.core.exceptions import ValidationError
45
from py.test import raises
56

6-
from graphene import Field, ObjectType, Schema, String
7+
from graphene import Field, Int, ObjectType, Schema, String
78
from graphene_django import DjangoObjectType
89
from graphene_django.tests.models import Pet
910

@@ -22,13 +23,17 @@ class Meta:
2223

2324
class MyForm(forms.Form):
2425
text = forms.CharField()
26+
another = forms.CharField(required=False)
2527

2628
def clean_text(self):
2729
text = self.cleaned_data["text"]
2830
if text == "INVALID_INPUT":
2931
raise ValidationError("Invalid input")
3032
return text
3133

34+
def clean_another(self):
35+
self.cleaned_data["another"] = self.cleaned_data["another"] or "defaultvalue"
36+
3237
def save(self):
3338
pass
3439

@@ -68,6 +73,83 @@ class Meta:
6873
form_class = MyForm
6974

7075
assert "text" in MyMutation.Input._meta.fields
76+
assert "another" in MyMutation.Input._meta.fields
77+
78+
79+
def test_no_input_fields():
80+
class MyMutation(DjangoFormMutation):
81+
class Meta:
82+
form_class = MyForm
83+
input_fields = []
84+
assert set(MyMutation.Input._meta.fields.keys()) == set(["client_mutation_id"])
85+
86+
87+
def test_filtering_input_fields():
88+
class MyMutation(DjangoFormMutation):
89+
class Meta:
90+
form_class = MyForm
91+
input_fields = ["text"]
92+
93+
assert "text" in MyMutation.Input._meta.fields
94+
assert "another" not in MyMutation.Input._meta.fields
95+
96+
97+
def test_select_output_fields():
98+
class MyMutation(DjangoFormMutation):
99+
class Meta:
100+
form_class = MyForm
101+
fields = ["text"]
102+
assert "text" in MyMutation._meta.fields
103+
assert "another" not in MyMutation._meta.fields
104+
105+
106+
def test_filtering_output_fields_exclude():
107+
class FormWithWeirdOutput(MyForm):
108+
"""Weird form that has extra cleaned_data we want to expose"""
109+
text = forms.CharField()
110+
another = forms.CharField(required=False)
111+
def clean(self):
112+
super(FormWithWeirdOutput, self).clean()
113+
self.cleaned_data["some_integer"] = 5
114+
return self.cleaned_data
115+
116+
class MyMutation(DjangoFormMutation):
117+
class Meta:
118+
form_class = FormWithWeirdOutput
119+
exclude = ["text"]
120+
121+
some_integer = Int()
122+
123+
assert "text" in MyMutation.Input._meta.fields
124+
assert "another" in MyMutation.Input._meta.fields
125+
126+
assert "text" not in MyMutation._meta.fields
127+
assert "another" in MyMutation._meta.fields
128+
assert "some_integer" in MyMutation._meta.fields
129+
130+
class Mutation(ObjectType):
131+
my_mutation = MyMutation.Field()
132+
133+
schema = Schema(query=MockQuery, mutation=Mutation)
134+
135+
result = schema.execute(
136+
""" mutation MyMutation {
137+
myMutation(input: { text: "VALID_INPUT" }) {
138+
errors {
139+
field
140+
messages
141+
}
142+
another
143+
someInteger
144+
}
145+
}
146+
"""
147+
)
148+
149+
assert result.errors is None
150+
assert result.data["myMutation"]["errors"] == []
151+
assert result.data["myMutation"]["someInteger"] == 5
152+
assert result.data["myMutation"]["another"] == "defaultvalue"
71153

72154

73155
def test_mutation_error_camelcased(pet_type, graphene_settings):

0 commit comments

Comments
 (0)