Skip to content

Commit d2b4a02

Browse files
committed
Implement recursive error camelize, add setting.
1 parent 6e77c35 commit d2b4a02

File tree

9 files changed

+107
-25
lines changed

9 files changed

+107
-25
lines changed

graphene_django/forms/mutation.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from graphene.types.utils import yank_fields_from_attrs
1414
from graphene_django.registry import get_global_registry
1515

16-
from .converter import convert_form_field
1716
from ..types import ErrorType
17+
from .converter import convert_form_field
1818

1919

2020
def fields_for_form(form, only_fields, exclude_fields):
@@ -45,10 +45,7 @@ def mutate_and_get_payload(cls, root, info, **input):
4545
if form.is_valid():
4646
return cls.perform_mutate(form, info)
4747
else:
48-
errors = [
49-
ErrorType(field=key, messages=value)
50-
for key, value in form.errors.items()
51-
]
48+
errors = ErrorType.from_errors(form.errors)
5249

5350
return cls(errors=errors)
5451

graphene_django/forms/tests/test_mutation.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from django.test import TestCase
33
from py.test import raises
44

5-
from graphene_django.tests.models import Pet, Film, FilmDetails
5+
from graphene_django.tests.models import Film, FilmDetails, Pet
6+
7+
from ...settings import graphene_settings
68
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
79

810

@@ -41,6 +43,22 @@ class Meta:
4143
assert "text" in MyMutation.Input._meta.fields
4244

4345

46+
def test_mutation_error_camelcased():
47+
class ExtraPetForm(PetForm):
48+
test_field = forms.CharField(required=True)
49+
50+
class PetMutation(DjangoModelFormMutation):
51+
class Meta:
52+
form_class = ExtraPetForm
53+
54+
result = PetMutation.mutate_and_get_payload(None, None)
55+
assert {f.field for f in result.errors} == {"name", "age", "test_field"}
56+
graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = True
57+
result = PetMutation.mutate_and_get_payload(None, None)
58+
assert {f.field for f in result.errors} == {"name", "age", "testField"}
59+
graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = False
60+
61+
4462
class ModelFormMutationTests(TestCase):
4563
def test_default_meta_fields(self):
4664
class PetMutation(DjangoModelFormMutation):

graphene_django/rest_framework/mutation.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
from django.shortcuts import get_object_or_404
44

55
import graphene
6+
from graphene.relay.mutation import ClientIDMutation
67
from graphene.types import Field, InputField
78
from graphene.types.mutation import MutationOptions
8-
from graphene.relay.mutation import ClientIDMutation
99
from graphene.types.objecttype import yank_fields_from_attrs
10-
from graphene.utils.str_converters import to_camel_case
1110

12-
from .serializer_converter import convert_serializer_field
1311
from ..types import ErrorType
12+
from .serializer_converter import convert_serializer_field
1413

1514

1615
class SerializerMutationOptions(MutationOptions):
@@ -128,10 +127,7 @@ def mutate_and_get_payload(cls, root, info, **input):
128127
if serializer.is_valid():
129128
return cls.perform_mutate(serializer, info)
130129
else:
131-
errors = [
132-
ErrorType(field=to_camel_case(key), messages=value)
133-
for key, value in serializer.errors.items()
134-
]
130+
errors = ErrorType.from_errors(serializer.errors)
135131

136132
return cls(errors=errors)
137133

graphene_django/rest_framework/tests/test_mutation.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import datetime
22

3+
from py.test import mark, raises
4+
from rest_framework import serializers
5+
36
from graphene import Field, ResolveInfo
47
from graphene.types.inputobjecttype import InputObjectType
5-
from py.test import raises
6-
from py.test import mark
7-
from rest_framework import serializers
88

9+
from ...settings import graphene_settings
910
from ...types import DjangoObjectType
1011
from ..models import MyFakeModel, MyFakeModelWithPassword
1112
from ..mutation import SerializerMutation
@@ -211,7 +212,13 @@ def test_model_mutate_and_get_payload_error():
211212
# missing required fields
212213
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
213214
assert len(result.errors) > 0
214-
assert result.errors[0].field == 'coolName'
215+
216+
217+
def test_mutation_error_camelcased():
218+
graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = True
219+
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
220+
assert result.errors[0].field == "coolName"
221+
graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS = False
215222

216223

217224
def test_invalid_serializer_operations():

graphene_django/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
3636
# Max items returned in ConnectionFields / FilterConnectionFields
3737
"RELAY_CONNECTION_MAX_LIMIT": 100,
38+
"DJANGO_GRAPHENE_CAMELCASE_ERRORS": False,
3839
}
3940

4041
if settings.DEBUG:

graphene_django/tests/test_utils.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from ..utils import get_model_fields
1+
from django.utils.translation import gettext_lazy
2+
3+
from ..utils import camelize, get_model_fields
24
from .models import Film, Reporter
35

46

@@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication():
1012
film_fields = get_model_fields(Film)
1113
film_name_set = set([field[0] for field in film_fields])
1214
assert len(film_fields) == len(film_name_set)
15+
16+
17+
def test_camelize():
18+
assert camelize({}) == {}
19+
assert camelize("value_a") == "value_a"
20+
assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"}
21+
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
22+
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
23+
assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == {
24+
"nestedField": {"valueA": ["error"], "valueB": ["error"]}
25+
}
26+
assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"}
27+
assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]}
28+
assert camelize(gettext_lazy("value_a")) == "value_a"
29+
assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == {
30+
"valueA": "value_b"
31+
}
32+
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}

graphene_django/types.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import six
21
from collections import OrderedDict
32

3+
import six
44
from django.db.models import Model
55
from django.utils.functional import SimpleLazyObject
6+
67
import graphene
78
from graphene import Field
89
from graphene.relay import Connection, Node
@@ -11,8 +12,13 @@
1112

1213
from .converter import convert_django_field_with_choices
1314
from .registry import Registry, get_global_registry
14-
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model
15-
15+
from .settings import graphene_settings
16+
from .utils import (
17+
DJANGO_FILTER_INSTALLED,
18+
camelize,
19+
get_model_fields,
20+
is_valid_django_model,
21+
)
1622

1723
if six.PY3:
1824
from typing import Type
@@ -165,3 +171,12 @@ def get_node(cls, info, id):
165171
class ErrorType(ObjectType):
166172
field = graphene.String(required=True)
167173
messages = graphene.List(graphene.NonNull(graphene.String), required=True)
174+
175+
@classmethod
176+
def from_errors(cls, errors):
177+
data = (
178+
camelize(errors)
179+
if graphene_settings.DJANGO_GRAPHENE_CAMELCASE_ERRORS
180+
else errors
181+
)
182+
return [ErrorType(field=key, messages=value) for key, value in data.items()]

graphene_django/utils/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
from .testing import GraphQLTestCase
12
from .utils import (
23
DJANGO_FILTER_INSTALLED,
3-
get_reverse_fields,
4-
maybe_queryset,
4+
camelize,
55
get_model_fields,
6-
is_valid_django_model,
6+
get_reverse_fields,
77
import_single_dispatch,
8+
is_valid_django_model,
9+
maybe_queryset,
810
)
9-
from .testing import GraphQLTestCase
1011

1112
__all__ = [
1213
"DJANGO_FILTER_INSTALLED",
1314
"get_reverse_fields",
1415
"maybe_queryset",
1516
"get_model_fields",
17+
"camelize",
1618
"is_valid_django_model",
1719
"import_single_dispatch",
1820
"GraphQLTestCase",

graphene_django/utils/utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
from django.db import models
44
from django.db.models.manager import Manager
5+
from django.utils import six
6+
from django.utils.encoding import force_text
7+
from django.utils.functional import Promise
58

9+
from graphene.utils.str_converters import to_camel_case
610

711
try:
812
import django_filters # noqa
@@ -12,6 +16,28 @@
1216
DJANGO_FILTER_INSTALLED = False
1317

1418

19+
def isiterable(value):
20+
try:
21+
iter(value)
22+
except TypeError:
23+
return False
24+
return True
25+
26+
27+
def _camelize_django_str(s):
28+
if isinstance(s, Promise):
29+
s = force_text(s)
30+
return to_camel_case(s) if isinstance(s, six.string_types) else s
31+
32+
33+
def camelize(data):
34+
if isinstance(data, dict):
35+
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
36+
if isiterable(data) and not isinstance(data, (six.string_types, Promise)):
37+
return [camelize(d) for d in data]
38+
return data
39+
40+
1541
def get_reverse_fields(model, local_field_names):
1642
for name, attr in model.__dict__.items():
1743
# Don't duplicate any local fields

0 commit comments

Comments
 (0)