8000 Add DjangoFormInputObjectType to forms/types by ndpu · Pull Request #1325 · graphql-python/graphene-django · GitHub
[go: up one dir, main page]

Skip to content

Add DjangoFormInputObjectType to forms/types #1325

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

Merged
Merged
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,8 @@ Session.vim
tags
.tox/
.pytest_cache/

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
49 changes: 49 additions & 0 deletions docs/mutations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,55 @@ 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.

DjangoFormInputObjectType
~~~~~~~~~~~~~~~~~~~~~~~~~

``DjangoFormInputObjectType`` is used in mutations to create input fields by **using django form** to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation.

.. code:: python

from graphene_django.forms.types import DjangoFormInputObjectType


class PetFormInput(DjangoFormInputObjectType):
# any other fields can be placed here as well as
# other djangoforminputobjects and intputobjects
class Meta:
form_class = PetForm
object_type = PetType

class QuestionFormInput(DjangoFormInputObjectType)
class Meta:
form_class = QuestionForm
object_type = QuestionType

class SeveralFormsInputData(graphene.InputObjectType):
pet = PetFormInput(required=True)
question = QuestionFormInput(required=True)

class SomeSophisticatedMutation(graphene.Mutation):
class Arguments:
data = SeveralFormsInputData(required=True)

@staticmethod
def mutate(_root, _info, data):
pet_form_inst = PetForm(data=data.pet)
question_form_inst = QuestionForm(data=data.question)

if pet_form_inst.is_valid():
pet_model_instance = pet_form_inst.save(commit=False)

if question_form_inst.is_valid():
question_model_instance = question_form_inst.save(commit=False)

# ...

Additional to **InputObjectType** ``Meta`` class attributes:

* ``form_class`` is required and should be equal to django form class.
* ``object_type`` is not required and used to enable convertion of enum values back to original if model object type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data.
* ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``)
* ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``)

Django REST Framework
---------------------
Expand Down
333 changes: 333 additions & 0 deletions graphene_django/forms/tests/test_djangoinputobject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
import graphene

from django import forms
from pytest import raises

from graphene_django import DjangoObjectType
from ..types import DjangoFormInputObjectType
from ...tests.models import Reporter, Film, CHOICES

# Reporter a_choice CHOICES = ((1, "this"), (2, _("that")))
THIS = CHOICES[0][0]
THIS_ON_CLIENT_CONVERTED = "A_1"

# Film genre choices=[("do", "Documentary"), ("ac", "Action"), ("ot", "Other")],
DOCUMENTARY = "do"
DOCUMENTARY_ON_CLIENT_CONVERTED = "DO"


class FilmForm(forms.ModelForm):
class Meta:
model = Film
exclude = ()


class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
fields = "__all__"


class ReporterForm(forms.ModelForm):
class Meta:
model = Reporter
exclude = ("pets", "email")


class MyForm(forms.Form):
text_field = forms.CharField()
int_field = forms.IntegerField()


def test_needs_form_class():
with raises(Exception) as exc:

class MyInputType(DjangoFormInputObjectType):
pass

assert exc.value.args[0] == "form_class is required for DjangoFormInputObjectType"


def test_type_from_modelform_has_input_fields():
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
only_fields = ("first_name", "last_name", "a_choice")

fields = ["first_name", "last_name", "a_choice", "id"]
assert all(f in ReporterInputType._meta.fields for f in fields)


def test_type_from_form_has_input_fields():
class MyFormInputType(DjangoFormInputObjectType):
class Meta:
form_class = MyForm

fields = ["text_field", "int_field", "id"]
assert all(f in MyFormInputType._meta.fields for f in fields)


def test_type_custom_id_field():
class MyFormInputType(DjangoFormInputObjectType):
class Meta:
form_class = MyForm
add_id_field_name = "pk"

fields = ["text_field", "int_field", "pk"]
assert all(f in MyFormInputType._meta.fields for f in fields)
assert MyFormInputType._meta.fields["pk"].type is graphene.ID


def test_type_custom_id_field_type():
class MyFormInputType(DjangoFormInputObjectType):
class Meta:
form_class = MyForm
add_id_field_name = "pk"
add_id_field_type = graphene.String(required=False)

fields = ["text_field", "int_field", "pk"]
assert all(f in MyFormInputType._meta.fields for f in fields)
assert MyFormInputType._meta.fields["pk"].type is graphene.String


class MockQuery(graphene.ObjectType):
a = graphene.String()


def test_mutation_with_form_djangoforminputtype():
class MyFormInputType(DjangoFormInputObjectType):
class Meta:
form_class = MyForm

class MyFormMutation(graphene.Mutation):
class Arguments:
form_data = MyFormInputType(required=True)

result = graphene.Boolean()

def mutate(_root, _info, form_data):
form = MyForm(data=form_data)
if form.is_valid():
result = form.cleaned_data == {
"text_field": "text",
"int_field": 777,
}
return MyFormMutation(result=result)
return MyFormMutation(result=False)

class Mutation(graphene.ObjectType):
myForm_mutation = MyFormMutation.Field()

schema = graphene.Schema(query=MockQuery, mutation=Mutation)

result = schema.execute(
""" mutation MyFormMutation($formData: MyFormInputType!) {
myFormMutation(formData: $formData) {
result
}
}
""",
variable_values={"formData": {"textField": "text", "intField": 777}},
)
assert result.errors is None
assert result.data == {"myFormMutation": {"result": True}}


def test_mutation_with_modelform_djangoforminputtype():
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
object_type = ReporterType
only_fields = ("first_name", "last_name", "a_choice")

class ReporterMutation(graphene.Mutation):
class Arguments:
reporter_data = ReporterInputType(required=True)

result = graphene.Field(ReporterType)

def mutate(_root, _info, reporter_data):
reporter = Reporter.objects.get(pk=reporter_data.id)
form = ReporterForm(data=reporter_data, instance=reporter)
if form.is_valid():
reporter = form.save()
return ReporterMutation(result=reporter)

return ReporterMutation(result=None)

class Mutation(graphene.ObjectType):
report_mutation = ReporterMutation.Field()

schema = graphene.Schema(query=MockQuery, mutation=Mutation)

reporter = Reporter.objects.create(
first_name="Bob", last_name="Roberts", a_choice=THIS
)

result = schema.execute(
""" mutation ReportMutation($reporterData: ReporterInputType!) {
reportMutation(reporterData: $reporterData) {
result {
id,
firstName,
lastName,
aChoice
}
}
}
""",
variable_values={
"reporterData": {
"id": reporter.pk,
"firstName": "Dave",
"lastName": "Smith",
"aChoice": THIS_ON_CLIENT_CONVERTED,
}
},
)
assert result.errors is None
assert result.data["reportMutation"]["result"] == {
"id": "1",
"firstName": "Dave",
"lastName": "Smith",
"aChoice": THIS_ON_CLIENT_CONVERTED,
}
assert Reporter.objects.count() == 1
reporter.refresh_from_db()
assert reporter.first_name == "Dave"


def reporter_enum_convert_mutation_result(
ReporterInputType, choice_val_on_client=THIS_ON_CLIENT_CONVERTED
):
class ReporterMutation(graphene.Mutation):
class Arguments:
reporter = ReporterInputType(required=True)

result_str = graphene.String()
result_int = graphene.Int()

def mutate(_root, _info, reporter):
if isinstance(reporter.a_choice, int) or reporter.a_choice.isdigit():
return ReporterMutation(result_int=reporter.a_choice, result_str=None)
return ReporterMutation(result_int=None, result_str=reporter.a_choice)

class Mutation(graphene.ObjectType):
report_mutation = A3D4 ReporterMutation.Field()

schema = graphene.Schema(query=MockQuery, mutation=Mutation)

return schema.execute(
""" mutation ReportMutation($reporter: ReporterInputType!) {
reportMutation(reporter: $reporter) {
resultStr,
resultInt
}
}
""",
variable_values={"reporter": {"aChoice": choice_val_on_client}},
)


def test_enum_not_converted():
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
only_fields = ("a_choice",)

result = reporter_enum_convert_mutation_result(ReporterInputType)
assert result.errors is None
assert result.data["reportMutation"]["resultStr"] == THIS_ON_CLIENT_CONVERTED
assert result.data["reportMutation"]["resultInt"] is None
assert ReporterInputType._meta.fields["a_choice"].type is graphene.String


def test_enum_is_converted_to_original():
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
object_type = ReporterType
only_fields = ("a_choice",)

result = reporter_enum_convert_mutation_result(ReporterInputType)
assert result.errors is None
assert result.data["reportMutation"]["resultInt"] == THIS
assert result.data["reportMutation"]["resultStr"] is None
assert (
ReporterInputType._meta.fields["a_choice"].type.__name__
== "AChoiceEnumBackConvString"
)


def test_convert_choices_to_enum_is_false_and_field_type_as_in_model():
class ReporterTypeNotConvertChoices(DjangoObjectType):
class Meta:
model = Reporter
convert_choices_to_enum = False
fields = "__all__"

class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
object_type = ReporterTypeNotConvertChoices
only_fields = ("a_choice",)

result = reporter_enum_convert_mutation_result(ReporterInputType, THIS)
assert result.errors is None
assert result.data["reportMutation"]["resultInt"] == THIS
assert result.data["reportMutation"]["resultStr"] is None
assert ReporterInputType._meta.fields["a_choice"].type is graphene.Int


def enum_convert_mutation_result_film(FilmInputType):
class FilmMutation(graphene.Mutation):
class Arguments:
film = FilmInputType(required=True)

result = graphene.String()

def mutate(_root, _info, film):
return FilmMutation(result=film.genre)

class Mutation(graphene.ObjectType):
film_mutation = FilmMutation.Field()

schema = graphene.Schema(query=MockQuery, mutation=Mutation)

return schema.execute(
""" mutation FilmMutation($film: FilmInputType!) {
filmMutation(film: $film) {
result
}
}
""",
variable_values={"film": {"genre": DOCUMENTARY_ON_CLIENT_CONVERTED}},
)


def test_enum_not_converted_required_non_number():
class FilmInputType(DjangoFormInputObjectType):
class Meta:
form_class = FilmForm
only_fields = ("genre",)

result = enum_convert_mutation_result_film(FilmInputType)
assert result.errors is None
assert result.data["filmMutation"]["result"] == DOCUMENTARY_ON_CLIENT_CONVERTED


def test_enum_is_converted_to_original_required_non_number():
class FilmType(DjangoObjectType):
class Meta:
model = Film
fields = "__all__"

class FilmInputType(DjangoFormInputObjectType):
class Meta:
form_class = FilmForm
object_type = FilmType
only_fields = ("genre",)

result = enum_convert_mutation_result_film(FilmInputType)
assert result.errors is None
assert result.data["filmMutation"]["result"] == DOCUMENTARY
Loading
0