Apply camel case converter to field names in DRF errors (#514)

* Apply camel case converter to field names in DRF errors

* Implement recursive error camelize, add setting.
This commit is contained in:
Konstantin Alekseev 2019-06-25 11:40:29 +03:00 committed by Jonathan Kim
parent 692540cc78
commit e2e496f505
9 changed files with 107 additions and 23 deletions

View File

@ -13,8 +13,8 @@ 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 .converter import convert_form_field
from ..types import ErrorType from ..types import ErrorType
from .converter import convert_form_field
def fields_for_form(form, only_fields, exclude_fields): def fields_for_form(form, only_fields, exclude_fields):
@ -45,10 +45,7 @@ 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 = [ errors = ErrorType.from_errors(form.errors)
ErrorType(field=key, messages=value)
for key, value in form.errors.items()
]
return cls(errors=errors) return cls(errors=errors)

View File

@ -2,7 +2,9 @@ from django import forms
from django.test import TestCase from django.test import TestCase
from py.test import raises from py.test import raises
from graphene_django.tests.models import Pet, Film, FilmDetails from graphene_django.tests.models import Film, FilmDetails, Pet
from ...settings import graphene_settings
from ..mutation import DjangoFormMutation, DjangoModelFormMutation from ..mutation import DjangoFormMutation, DjangoModelFormMutation
@ -41,6 +43,22 @@ def test_has_input_fields():
assert "text" in MyMutation.Input._meta.fields assert "text" in MyMutation.Input._meta.fields
def test_mutation_error_camelcased():
class ExtraPetForm(PetForm):
test_field = forms.CharField(required=True)
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = ExtraPetForm
result = PetMutation.mutate_and_get_payload(None, None)
assert {f.field for f in result.errors} == {"name", "age", "test_field"}
graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = True
result = PetMutation.mutate_and_get_payload(None, None)
assert {f.field for f in result.errors} == {"name", "age", "testField"}
graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = False
class ModelFormMutationTests(TestCase): class ModelFormMutationTests(TestCase):
def test_default_meta_fields(self): def test_default_meta_fields(self):
class PetMutation(DjangoModelFormMutation): class PetMutation(DjangoModelFormMutation):

View File

@ -3,13 +3,13 @@ from collections import OrderedDict
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
import graphene import graphene
from graphene.relay.mutation import ClientIDMutation
from graphene.types import Field, InputField from graphene.types import Field, InputField
from graphene.types.mutation import MutationOptions from graphene.types.mutation import MutationOptions
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 .serializer_converter import convert_serializer_field
from ..types import ErrorType from ..types import ErrorType
from .serializer_converter import convert_serializer_field
class SerializerMutationOptions(MutationOptions): class SerializerMutationOptions(MutationOptions):
@ -127,10 +127,7 @@ class SerializerMutation(ClientIDMutation):
if serializer.is_valid(): if serializer.is_valid():
return cls.perform_mutate(serializer, info) return cls.perform_mutate(serializer, info)
else: else:
errors = [ errors = ErrorType.from_errors(serializer.errors)
ErrorType(field=key, messages=value)
for key, value in serializer.errors.items()
]
return cls(errors=errors) return cls(errors=errors)

View File

@ -1,11 +1,12 @@
import datetime import datetime
from py.test import mark, raises
from rest_framework import serializers
from graphene import Field, ResolveInfo from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
from rest_framework import serializers
from ...settings import graphene_settings
from ...types import DjangoObjectType from ...types import DjangoObjectType
from ..models import MyFakeModel, MyFakeModelWithPassword from ..models import MyFakeModel, MyFakeModelWithPassword
from ..mutation import SerializerMutation from ..mutation import SerializerMutation
@ -213,6 +214,13 @@ def test_model_mutate_and_get_payload_error():
assert len(result.errors) > 0 assert len(result.errors) > 0
def test_mutation_error_camelcased():
graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = True
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
assert result.errors[0].field == "coolName"
graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = False
def test_invalid_serializer_operations(): def test_invalid_serializer_operations():
with raises(Exception) as exc: with raises(Exception) as exc:

View File

@ -35,6 +35,7 @@ DEFAULTS = {
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False, "RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
# Max items returned in ConnectionFields / FilterConnectionFields # Max items returned in ConnectionFields / FilterConnectionFields
"RELAY_CONNECTION_MAX_LIMIT": 100, "RELAY_CONNECTION_MAX_LIMIT": 100,
"DJANGO_GRAPHENE_CAMELCASE_ERRORS": False,
} }
if settings.DEBUG: if settings.DEBUG:

View File

@ -1,4 +1,6 @@
from ..utils import get_model_fields from django.utils.translation import gettext_lazy
from ..utils import camelize, get_model_fields
from .models import Film, Reporter from .models import Film, Reporter
@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication():
film_fields = get_model_fields(Film) film_fields = get_model_fields(Film)
film_name_set = set([field[0] for field in film_fields]) film_name_set = set([field[0] for field in film_fields])
assert len(film_fields) == len(film_name_set) assert len(film_fields) == len(film_name_set)
def test_camelize():
assert camelize({}) == {}
assert camelize("value_a") == "value_a"
assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"}
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == {
"nestedField": {"valueA": ["error"], "valueB": ["error"]}
}
assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"}
assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]}
assert camelize(gettext_lazy("value_a")) == "value_a"
assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == {
"valueA": "value_b"
}
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}

View File

@ -1,8 +1,9 @@
import six
from collections import OrderedDict from collections import OrderedDict
import six
from django.db.models import Model from django.db.models import Model
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
import graphene import graphene
from graphene import Field from graphene import Field
from graphene.relay import Connection, Node from graphene.relay import Connection, Node
@ -11,8 +12,13 @@ from graphene.types.utils import yank_fields_from_attrs
from .converter import convert_django_field_with_choices from .converter import convert_django_field_with_choices
from .registry import Registry, get_global_registry from .registry import Registry, get_global_registry
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model from .settings import graphene_settings
from .utils import (
DJANGO_FILTER_INSTALLED,
camelize,
get_model_fields,
is_valid_django_model,
)
if six.PY3: if six.PY3:
from typing import Type from typing import Type
@ -182,3 +188,12 @@ class DjangoObjectType(ObjectType):
class ErrorType(ObjectType): class ErrorType(ObjectType):
field = graphene.String(required=True) field = graphene.String(required=True)
messages = graphene.List(graphene.NonNull(graphene.String), required=True) messages = graphene.List(graphene.NonNull(graphene.String), required=True)
@classmethod
def from_errors(cls, errors):
data = (
camelize(errors)
if graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS
else errors
)
return [ErrorType(field=key, messages=value) for key, value in data.items()]

View File

@ -1,18 +1,20 @@
from .testing import GraphQLTestCase
from .utils import ( from .utils import (
DJANGO_FILTER_INSTALLED, DJANGO_FILTER_INSTALLED,
get_reverse_fields, camelize,
maybe_queryset,
get_model_fields, get_model_fields,
is_valid_django_model, get_reverse_fields,
import_single_dispatch, import_single_dispatch,
is_valid_django_model,
maybe_queryset,
) )
from .testing import GraphQLTestCase
__all__ = [ __all__ = [
"DJANGO_FILTER_INSTALLED", "DJANGO_FILTER_INSTALLED",
"get_reverse_fields", "get_reverse_fields",
"maybe_queryset", "maybe_queryset",
"get_model_fields", "get_model_fields",
"camelize",
"is_valid_django_model", "is_valid_django_model",
"import_single_dispatch", "import_single_dispatch",
"GraphQLTestCase", "GraphQLTestCase",

View File

@ -2,7 +2,11 @@ import inspect
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 django.utils import six
from django.utils.encoding import force_text
from django.utils.functional import Promise
from graphene.utils.str_converters import to_camel_case
try: try:
import django_filters # noqa import django_filters # noqa
@ -12,6 +16,28 @@ except ImportError:
DJANGO_FILTER_INSTALLED = False DJANGO_FILTER_INSTALLED = False
def isiterable(value):
try:
iter(value)
except TypeError:
return False
return True
def _camelize_django_str(s):
if isinstance(s, Promise):
s = force_text(s)
return to_camel_case(s) if isinstance(s, six.string_types) else s
def camelize(data):
if isinstance(data, dict):
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
if isiterable(data) and not isinstance(data, (six.string_types, Promise)):
return [camelize(d) for d in data]
return data
def get_reverse_fields(model, local_field_names): def get_reverse_fields(model, local_field_names):
for name, attr in model.__dict__.items(): for name, attr in model.__dict__.items():
# Don't duplicate any local fields # Don't duplicate any local fields