Update form mutation errors

This commit is contained in:
Patrick Arminio 2018-10-28 20:25:26 +00:00
parent dafcc8514b
commit 19fcc3cdf2
5 changed files with 176 additions and 127 deletions

View File

@ -13,6 +13,7 @@ from graphene.types.mutation import MutationOptions
from graphene.types.utils import yank_fields_from_attrs from graphene.types.utils import yank_fields_from_attrs
from graphene_django.registry import get_global_registry from graphene_django.registry import get_global_registry
from ..utils import create_errors_type
from .converter import convert_form_field from .converter import convert_form_field
from .types import ErrorType from .types import ErrorType
@ -45,10 +46,8 @@ class BaseDjangoFormMutation(ClientIDMutation):
if form.is_valid(): if form.is_valid():
return cls.perform_mutate(form, info) return cls.perform_mutate(form, info)
else: else:
errors = [ # TODO: double check non field errors name
ErrorType(field=key, messages=value) errors = cls.Errors(**form.errors)
for key, value in form.errors.items()
]
return cls(errors=errors) return cls(errors=errors)
@ -99,8 +98,6 @@ class DjangoFormMutation(BaseDjangoFormMutation):
class Meta: class Meta:
abstract = True abstract = True
errors = graphene.List(ErrorType)
@classmethod @classmethod
def __init_subclass_with_meta__( def __init_subclass_with_meta__(
cls, form_class=None, only_fields=(), exclude_fields=(), **options cls, form_class=None, only_fields=(), exclude_fields=(), **options
@ -112,12 +109,21 @@ class DjangoFormMutation(BaseDjangoFormMutation):
form = form_class() form = form_class()
input_fields = fields_for_form(form, only_fields, exclude_fields) input_fields = fields_for_form(form, only_fields, exclude_fields)
output_fields = fields_for_form(form, only_fields, exclude_fields) output_fields = fields_for_form(form, only_fields, exclude_fields)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
base_name = cls.__name__
cls.Errors = create_errors_type(
"{}Errors".format(base_name),
input_fields
)
output_fields['errors'] = graphene.Field(cls.Errors, required=True)
_meta = DjangoFormMutationOptions(cls) _meta = DjangoFormMutationOptions(cls)
_meta.form_class = form_class _meta.form_class = form_class
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(DjangoFormMutation, cls).__init_subclass_with_meta__( super(DjangoFormMutation, cls).__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options _meta=_meta, input_fields=input_fields, **options
) )
@ -137,8 +143,6 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
class Meta: class Meta:
abstract = True abstract = True
errors = graphene.List(ErrorType)
@classmethod @classmethod
def __init_subclass_with_meta__( def __init_subclass_with_meta__(
cls, cls,
@ -173,6 +177,16 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
output_fields = OrderedDict() output_fields = OrderedDict()
output_fields[return_field_name] = graphene.Field(model_type) output_fields[return_field_name] = graphene.Field(model_type)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
base_name = cls.__name__
cls.Errors = create_errors_type(
"{}Errors".format(base_name),
input_fields
)
output_fields['errors'] = graphene.Field(cls.Errors, required=True)
_meta = DjangoModelDjangoFormMutationOptions(cls) _meta = DjangoModelDjangoFormMutationOptions(cls)
_meta.form_class = form_class _meta.form_class = form_class
@ -180,7 +194,6 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
_meta.return_field_name = return_field_name _meta.return_field_name = return_field_name
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(DjangoModelFormMutation, cls).__init_subclass_with_meta__( super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options _meta=_meta, input_fields=input_fields, **options
) )
@ -189,4 +202,4 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
def perform_mutate(cls, form, info): def perform_mutate(cls, form, info):
obj = form.save() obj = form.save()
kwargs = {cls._meta.return_field_name: obj} kwargs = {cls._meta.return_field_name: obj}
return cls(errors=[], **kwargs) return cls(errors={}, **kwargs)

View File

@ -1,6 +1,9 @@
import pytest
from django import forms from django import forms
from django.test import TestCase from django.test import TestCase
from py.test import raises
from graphene import NonNull, List
from graphene_django.tests.models import Pet, Film, FilmDetails from graphene_django.tests.models import Pet, Film, FilmDetails
from ..mutation import DjangoFormMutation, DjangoModelFormMutation from ..mutation import DjangoFormMutation, DjangoModelFormMutation
@ -17,8 +20,7 @@ class PetForm(forms.ModelForm):
def test_needs_form_class(): def test_needs_form_class():
with raises(Exception) as exc: with pytest.raises(Exception) as exc:
class MyMutation(DjangoFormMutation): class MyMutation(DjangoFormMutation):
pass pass
@ -41,101 +43,127 @@ def test_has_input_fields():
assert "text" in MyMutation.Input._meta.fields assert "text" in MyMutation.Input._meta.fields
class ModelFormMutationTests(TestCase): def test_default_meta_fields():
def test_default_meta_fields(self): class PetMutation(DjangoModelFormMutation):
class PetMutation(DjangoModelFormMutation): class Meta:
class Meta: form_class = PetForm
form_class = PetForm
self.assertEqual(PetMutation._meta.model, Pet) assert PetMutation._meta.model == Pet
self.assertEqual(PetMutation._meta.return_field_name, "pet") assert PetMutation._meta.return_field_name == "pet"
self.assertIn("pet", PetMutation._meta.fields)
def test_default_input_meta_fields(self): assert "pet" in PetMutation._meta.fields
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
self.assertEqual(PetMutation._meta.model, Pet)
self.assertEqual(PetMutation._meta.return_field_name, "pet")
self.assertIn("name", PetMutation.Input._meta.fields)
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
self.assertIn("id", PetMutation.Input._meta.fields)
def test_exclude_fields_input_meta_fields(self):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
exclude_fields = ['id']
self.assertEqual(PetMutation._meta.model, Pet)
self.assertEqual(PetMutation._meta.return_field_name, "pet")
self.assertIn("name", PetMutation.Input._meta.fields)
self.assertIn("age", PetMutation.Input._meta.fields)
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
self.assertNotIn("id", PetMutation.Input._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:
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(DjangoModelFormMutation):
class Meta:
form_class = PetForm
pet = Pet.objects.create(name="Axel", age=10)
result = PetMutation.mutate_and_get_payload(None, None, id=pet.pk, name="Mia", age=10)
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", age=10)
self.assertEqual(Pet.objects.count(), 1)
pet = Pet.objects.get()
self.assertEqual(pet.name, "Mia")
self.assertEqual(pet.age, 10)
self.assertEqual(result.errors, [])
def test_model_form_mutation_mutate_invalid_form(self):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
result = PetMutation.mutate_and_get_payload(None, None)
# A pet was not created
self.assertEqual(Pet.objects.count(), 0)
fields_w_error = [e.field for e in result.errors] def test_default_input_meta_fields():
self.assertEqual(len(result.errors), 2) class PetMutation(DjangoModelFormMutation):
self.assertIn("name", fields_w_error) class Meta:
self.assertEqual(result.errors[0].messages, ["This field is required."]) form_class = PetForm
self.assertIn("age", fields_w_error)
self.assertEqual(result.errors[1].messages, ["This field is required."]) assert PetMutation._meta.model == Pet
assert PetMutation._meta.return_field_name == "pet"
assert "name" in PetMutation.Input._meta.fields
assert "client_mutation_id" in PetMutation.Input._meta.fields
assert "id" in PetMutation.Input._meta.fields
def test_exclude_fields_input_meta_fields():
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
exclude_fields = ['id']
assert PetMutation._meta.model == Pet
assert PetMutation._meta.return_field_name == "pet"
assert "name" in PetMutation.Input._meta.fields
assert "age" in PetMutation.Input._meta.fields
assert "client_mutation_id" in PetMutation.Input._meta.fields
assert "id" not in PetMutation.Input._meta.fields
def test_return_field_name_is_camelcased():
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
model = FilmDetails
assert PetMutation._meta.model == FilmDetails
assert PetMutation._meta.return_field_name == "filmDetails"
def test_custom_return_field_name():
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
model = Film
return_field_name = "animal"
assert PetMutation._meta.model == Film
assert PetMutation._meta.return_field_name == "animal"
assert "animal" in PetMutation._meta.fields
@pytest.mark.django_db
def test_model_form_mutation_mutate():
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
pet = Pet.objects.create(name="Axel", age=10)
result = PetMutation.mutate_and_get_payload(None, None, id=pet.pk, name="Mia", age=10)
assert Pet.objects.count() == 1
pet.refresh_from_db()
assert pet.name == "Mia"
assert result.errors == {}
@pytest.mark.django_db
def test_model_form_mutation_updates_existing():
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
result = PetMutation.mutate_and_get_payload(None, None, name="Mia", age=10)
assert Pet.objects.count() == 1
pet = Pet.objects.get()
assert pet.name == "Mia"
assert pet.age == 10
assert result.errors == {}
@pytest.mark.django_db
def test_model_form_mutation_mutate_invalid_form():
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
result = PetMutation.mutate_and_get_payload(None, None)
# A pet was not created
assert Pet.objects.count() == 0
assert result.errors != {}
assert result.errors.name == ["This field is required."]
assert result.errors.age == ["This field is required."]
def test_errors_field():
class MyMutation(DjangoFormMutation):
class Meta:
form_class = MyForm
errors_field = MyMutation._meta.fields['errors']
assert MyMutation.Errors
assert type(errors_field.type) == NonNull
errors_field = errors_field.type.of_type
assert type(errors_field.text.type) == List
assert type(errors_field.text.type.of_type) == NonNull
# TODO: how to test that the nonnull type is a string?

View File

@ -8,6 +8,7 @@ from graphene.types.mutation import MutationOptions
from graphene.relay.mutation import ClientIDMutation from graphene.relay.mutation import ClientIDMutation
from graphene.types.objecttype import yank_fields_from_attrs from graphene.types.objecttype import yank_fields_from_attrs
from ..utils import create_errors_type
from .serializer_converter import convert_serializer_field from .serializer_converter import convert_serializer_field
from .types import ErrorType from .types import ErrorType
@ -73,22 +74,11 @@ class SerializerMutation(ClientIDMutation):
serializer, only_fields, exclude_fields, is_input=False serializer, only_fields, exclude_fields, is_input=False
) )
error_fields = {
key: graphene.List(graphene.NonNull(graphene.String))
for key in input_fields.keys()
}
# TODO: from settings
error_fields['non_field_errors'] = graphene.List(
graphene.NonNull(graphene.String)
)
base_name = cls.__name__ base_name = cls.__name__
cls.Errors = type( cls.Errors = create_errors_type(
"{}Errors".format(base_name), "{}Errors".format(base_name),
(graphene.ObjectType, ), input_fields
yank_fields_from_attrs(error_fields, _as=Field),
) )
output_fields['errors'] = graphene.Field(cls.Errors, required=True) output_fields['errors'] = graphene.Field(cls.Errors, required=True)

View File

@ -159,14 +159,14 @@ def test_mutate_and_get_payload_error():
# missing required fields # missing required fields
result = MyMutation.mutate_and_get_payload(None, mock_info(), **{}) result = MyMutation.mutate_and_get_payload(None, mock_info(), **{})
assert result.errors.text[0] == "This field is required." assert result.errors.text == ["This field is required."]
assert result.errors.model[0] == "This field is required." assert result.errors.model == ["This field is required."]
def test_model_mutate_and_get_payload_error(): def test_model_mutate_and_get_payload_error():
# missing required fields # missing required fields
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{}) result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
assert result.errors.cool_name[0] == "This field is required." assert result.errors.cool_name == ["This field is required."]
def test_invalid_serializer_operations(): def test_invalid_serializer_operations():

View File

@ -1,12 +1,12 @@
import inspect import inspect
import graphene
from graphene.types.objecttype import yank_fields_from_attrs
from django.db import models from django.db import models
from django.db.models.manager import Manager from django.db.models.manager import Manager
# from graphene.utils import LazyList
class LazyList(object): class LazyList(object):
pass pass
@ -81,3 +81,21 @@ def import_single_dispatch():
) )
return singledispatch return singledispatch
def create_errors_type(name, input_fields):
error_fields = {
key: graphene.List(graphene.NonNull(graphene.String))
for key in input_fields.keys()
}
# TODO: from settings
error_fields['non_field_errors'] = graphene.List(
graphene.NonNull(graphene.String)
)
return type(
name,
(graphene.ObjectType, ),
yank_fields_from_attrs(error_fields, _as=graphene.Field),
)