Skip to content

Commit 612ba5a

Browse files
authored
Add convert_choices_to_enum option on DjangoObjectType Meta class (#674)
* Add convert_choices_to_enum meta option * Add tests * Run black * Update documentation * Add link to Django choices documentation * Add test and documentation note That setting to an empty list is the same as setting the value as False * Fix Django warning in tests * rst is not markdown
1 parent 894b105 commit 612ba5a

File tree

5 files changed

+220
-6
lines changed

5 files changed

+220
-6
lines changed

docs/queries.rst

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
9292
return 'hello!'
9393
9494
95+
Choices to Enum conversion
96+
~~~~~~~~~~~~~~~~~~~~~~~~~~
97+
98+
By default Graphene-Django will convert any Django fields that have `choices`_
99+
defined into a GraphQL enum type.
100+
101+
.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
102+
103+
For example the following ``Model`` and ``DjangoObjectType``:
104+
105+
.. code:: python
106+
107+
class PetModel(models.Model):
108+
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
109+
110+
class Pet(DjangoObjectType):
111+
class Meta:
112+
model = PetModel
113+
114+
Results in the following GraphQL schema definition:
115+
116+
.. code::
117+
118+
type Pet {
119+
id: ID!
120+
kind: PetModelKind!
121+
}
122+
123+
enum PetModelKind {
124+
CAT
125+
DOG
126+
}
127+
128+
You can disable this automatic conversion by setting
129+
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
130+
``Meta`` class.
131+
132+
.. code:: python
133+
134+
class Pet(DjangoObjectType):
135+
class Meta:
136+
model = PetModel
137+
convert_choices_to_enum = False
138+
139+
.. code::
140+
141+
type Pet {
142+
id: ID!
143+
kind: String!
144+
}
145+
146+
You can also set ``convert_choices_to_enum`` to a list of fields that should be
147+
automatically converted into enums:
148+
149+
.. code:: python
150+
151+
class Pet(DjangoObjectType):
152+
class Meta:
153+
model = PetModel
154+
convert_choices_to_enum = ['kind']
155+
156+
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
157+
``False``.
158+
159+
95160
Related models
96161
--------------
97162

graphene_django/converter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,15 @@ def get_choices(choices):
5252
yield name, value, description
5353

5454

55-
def convert_django_field_with_choices(field, registry=None):
55+
def convert_django_field_with_choices(
56+
field, registry=None, convert_choices_to_enum=True
57+
):
5658
if registry is not None:
5759
converted = registry.get_converted_field(field)
5860
if converted:
5961
return converted
6062
choices = getattr(field, "choices", None)
61-
if choices:
63+
if choices and convert_choices_to_enum:
6264
meta = field.model._meta
6365
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
6466
choices = list(get_choices(choices))

graphene_django/tests/test_converter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,23 @@ class Meta:
196196
convert_django_field_with_choices(field)
197197

198198

199+
def test_field_with_choices_convert_enum_false():
200+
field = models.CharField(
201+
help_text="Language", choices=(("es", "Spanish"), ("en", "English"))
202+
)
203+
204+
class TranslatedModel(models.Model):
205+
language = field
206+
207+
class Meta:
208+
app_label = "test"
209+
210+
graphene_type = convert_django_field_with_choices(
211+
field, convert_choices_to_enum=False
212+
)
213+
assert isinstance(graphene_type, graphene.String)
214+
215+
199216
def test_should_float_convert_float():
200217
assert_conversion(models.FloatField, graphene.Float)
201218

graphene_django/tests/test_types.py

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
from collections import OrderedDict, defaultdict
2+
from textwrap import dedent
3+
4+
import pytest
5+
from django.db import models
16
from mock import patch
27

3-
from graphene import Interface, ObjectType, Schema, Connection, String
8+
from graphene import Connection, Field, Interface, ObjectType, Schema, String
49
from graphene.relay import Node
510

611
from .. import registry
@@ -224,3 +229,111 @@ class Meta:
224229

225230
fields = list(Reporter._meta.fields.keys())
226231
assert "email" not in fields
232+
233+
234+
class TestDjangoObjectType:
235+
@pytest.fixture
236+
def PetModel(self):
237+
class PetModel(models.Model):
238+
kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog")))
239+
cuteness = models.IntegerField(
240+
choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!"))
241+
)
242+
243+
yield PetModel
244+
245+
# Clear Django model cache so we don't get warnings when creating the
246+
# model multiple times
247+
PetModel._meta.apps.all_models = defaultdict(OrderedDict)
248+
249+
def test_django_objecttype_convert_choices_enum_false(self, PetModel):
250+
class Pet(DjangoObjectType):
251+
class Meta:
252+
model = PetModel
253+
convert_choices_to_enum = False
254+
255+
class Query(ObjectType):
256+
pet = Field(Pet)
257+
258+
schema = Schema(query=Query)
259+
260+
assert str(schema) == dedent(
261+
"""\
262+
schema {
263+
query: Query
264+
}
265+
266+
type Pet {
267+
id: ID!
268+
kind: String!
269+
cuteness: Int!
270+
}
271+
272+
type Query {
273+
pet: Pet
274+
}
275+
"""
276+
)
277+
278+
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
279+
class Pet(DjangoObjectType):
280+
class Meta:
281+
model = PetModel
282+
convert_choices_to_enum = ["kind"]
283+
284+
class Query(ObjectType):
285+
pet = Field(Pet)
286+
287+
schema = Schema(query=Query)
288+
289+
assert str(schema) == dedent(
290+
"""\
291+
schema {
292+
query: Query
293+
}
294+
295+
type Pet {
296+
id: ID!
297+
kind: PetModelKind!
298+
cuteness: Int!
299+
}
300+
301+
enum PetModelKind {
302+
CAT
303+
DOG
304+
}
305+
306+
type Query {
307+
pet: Pet
308+
}
309+
"""
310+
)
311+
312+
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
313+
class Pet(DjangoObjectType):
314+
class Meta:
315+
model = PetModel
316+
convert_choices_to_enum = []
317+
318+
class Query(ObjectType):
319+
pet = Field(Pet)
320+
321+
schema = Schema(query=Query)
322+
323+
assert str(schema) == dedent(
324+
"""\
325+
schema {
326+
query: Query
327+
}
328+
329+
type Pet {
330+
id: ID!
331+
kind: String!
332+
cuteness: Int!
333+
}
334+
335+
type Query {
336+
pet: Pet
337+
}
338+
"""
339+
)

graphene_django/types.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
from typing import Type
1919

2020

21-
def construct_fields(model, registry, only_fields, exclude_fields):
21+
def construct_fields(
22+
model, registry, only_fields, exclude_fields, convert_choices_to_enum
23+
):
2224
_model_fields = get_model_fields(model)
2325

2426
fields = OrderedDict()
@@ -33,7 +35,18 @@ def construct_fields(model, registry, only_fields, exclude_fields):
3335
# in there. Or when we exclude this field in exclude_fields.
3436
# Or when there is no back reference.
3537
continue
36-
converted = convert_django_field_with_choices(field, registry)
38+
39+
_convert_choices_to_enum = convert_choices_to_enum
40+
if not isinstance(_convert_choices_to_enum, bool):
41+
# then `convert_choices_to_enum` is a list of field names to convert
42+
if name in _convert_choices_to_enum:
43+
_convert_choices_to_enum = True
44+
else:
45+
_convert_choices_to_enum = False
46+
47+
converted = convert_django_field_with_choices(
48+
field, registry, convert_choices_to_enum=_convert_choices_to_enum
49+
)
3750
fields[name] = converted
3851

3952
return fields
@@ -63,6 +76,7 @@ def __init_subclass_with_meta__(
6376
connection_class=None,
6477
use_connection=None,
6578
interfaces=(),
79+
convert_choices_to_enum=True,
6680
_meta=None,
6781
**options
6882
):
@@ -90,7 +104,10 @@ def __init_subclass_with_meta__(
90104
)
91105

92106
django_fields = yank_fields_from_attrs(
93-
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
107+
construct_fields(
108+
model, registry, only_fields, exclude_fields, convert_choices_to_enum
109+
),
110+
_as=Field,
94111
)
95112

96113
if use_connection is None and interfaces:

0 commit comments

Comments
 (0)