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_django.registry import get_global_registry
from .converter import convert_form_field
from ..types import ErrorType
from .converter import convert_form_field
def fields_for_form(form, only_fields, exclude_fields):
@ -45,10 +45,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
if form.is_valid():
return cls.perform_mutate(form, info)
else:
errors = [
ErrorType(field=key, messages=value)
for key, value in form.errors.items()
]
errors = ErrorType.from_errors(form.errors)
return cls(errors=errors)

View File

@ -2,7 +2,9 @@ from django import forms
from django.test import TestCase
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
@ -41,6 +43,22 @@ def test_has_input_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):
def test_default_meta_fields(self):
class PetMutation(DjangoModelFormMutation):

View File

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

View File

@ -1,11 +1,12 @@
import datetime
from py.test import mark, raises
from rest_framework import serializers
from graphene import Field, ResolveInfo
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 ..models import MyFakeModel, MyFakeModelWithPassword
from ..mutation import SerializerMutation
@ -213,6 +214,13 @@ def test_model_mutate_and_get_payload_error():
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():
with raises(Exception) as exc:

View File

@ -35,6 +35,7 @@ DEFAULTS = {
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
# Max items returned in ConnectionFields / FilterConnectionFields
"RELAY_CONNECTION_MAX_LIMIT": 100,
"DJANGO_GRAPHENE_CAMELCASE_ERRORS": False,
}
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
@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication():
film_fields = get_model_fields(Film)
film_name_set = set([field[0] for field in film_fields])
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
import six
from django.db.models import Model
from django.utils.functional import SimpleLazyObject
import graphene
from graphene import Field
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 .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:
from typing import Type
@ -182,3 +188,12 @@ class DjangoObjectType(ObjectType):
class ErrorType(ObjectType):
field = 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 (
DJANGO_FILTER_INSTALLED,
get_reverse_fields,
maybe_queryset,
camelize,
get_model_fields,
is_valid_django_model,
get_reverse_fields,
import_single_dispatch,
is_valid_django_model,
maybe_queryset,
)
from .testing import GraphQLTestCase
__all__ = [
"DJANGO_FILTER_INSTALLED",
"get_reverse_fields",
"maybe_queryset",
"get_model_fields",
"camelize",
"is_valid_django_model",
"import_single_dispatch",
"GraphQLTestCase",

View File

@ -2,7 +2,11 @@ import inspect
from django.db import models
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:
import django_filters # noqa
@ -12,6 +16,28 @@ except ImportError:
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):
for name, attr in model.__dict__.items():
# Don't duplicate any local fields