Skip to content

Add convert_choices_to_enum option on DjangoObjectType Meta class #674

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/queries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
return 'hello!'


Choices to Enum conversion
~~~~~~~~~~~~~~~~~~~~~~~~~~

By default Graphene-Django will convert any Django fields that have `choices`_
defined into a GraphQL enum type.

.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices

For example the following ``Model`` and ``DjangoObjectType``:

.. code:: python

class PetModel(models.Model):
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))

class Pet(DjangoObjectType):
class Meta:
model = PetModel

Results in the following GraphQL schema definition:

.. code::

type Pet {
id: ID!
kind: PetModelKind!
}

enum PetModelKind {
CAT
DOG
}

You can disable this automatic conversion by setting
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
``Meta`` class.

.. code:: python

class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = False

.. code::

type Pet {
id: ID!
kind: String!
}

You can also set ``convert_choices_to_enum`` to a list of fields that should be
automatically converted into enums:

.. code:: python

class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = ['kind']

**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
``False``.


Related models
--------------

Expand Down
6 changes: 4 additions & 2 deletions graphene_django/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ def get_choices(choices):
yield name, value, description


def convert_django_field_with_choices(field, registry=None):
def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=True
):
if registry is not None:
converted = registry.get_converted_field(field)
if converted:
return converted
choices = getattr(field, "choices", None)
if choices:
if choices and convert_choices_to_enum:
meta = field.model._meta
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
choices = list(get_choices(choices))
Expand Down
17 changes: 17 additions & 0 deletions graphene_django/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ class Meta:
convert_django_field_with_choices(field)


def test_field_with_choices_convert_enum_false():
field = models.CharField(
help_text="Language", choices=(("es", "Spanish"), ("en", "English"))
)

class TranslatedModel(models.Model):
language = field

class Meta:
app_label = "test"

graphene_type = convert_django_field_with_choices(
field, convert_choices_to_enum=False
)
assert isinstance(graphene_type, graphene.String)


def test_should_float_convert_float():
assert_conversion(models.FloatField, graphene.Float)

Expand Down
115 changes: 114 additions & 1 deletion graphene_django/tests/test_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from collections import OrderedDict, defaultdict
from textwrap import dedent

import pytest
from django.db import models
from mock import patch

from graphene import Interface, ObjectType, Schema, Connection, String
from graphene import Connection, Field, Interface, ObjectType, Schema, String
from graphene.relay import Node

from .. import registry
Expand Down Expand Up @@ -224,3 +229,111 @@ class Meta:

fields = list(Reporter._meta.fields.keys())
assert "email" not in fields


class TestDjangoObjectType:
@pytest.fixture
def PetModel(self):
class PetModel(models.Model):
kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog")))
cuteness = models.IntegerField(
choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!"))
)

yield PetModel

# Clear Django model cache so we don't get warnings when creating the
# model multiple times
PetModel._meta.apps.all_models = defaultdict(OrderedDict)

def test_django_objecttype_convert_choices_enum_false(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = False

class Query(ObjectType):
pet = Field(Pet)

schema = Schema(query=Query)

assert str(schema) == dedent(
"""\
schema {
query: Query
}

type Pet {
id: ID!
kind: String!
cuteness: Int!
}

type Query {
pet: Pet
}
"""
)

def test_django_objecttype_convert_choices_enum_list(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = ["kind"]

class Query(ObjectType):
pet = Field(Pet)

schema = Schema(query=Query)

assert str(schema) == dedent(
"""\
schema {
query: Query
}

type Pet {
id: ID!
kind: PetModelKind!
cuteness: Int!
}

enum PetModelKind {
CAT
DOG
}

type Query {
pet: Pet
}
"""
)

def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = []

class Query(ObjectType):
pet = Field(Pet)

schema = Schema(query=Query)

assert str(schema) == dedent(
"""\
schema {
query: Query
}

type Pet {
id: ID!
kind: String!
cuteness: Int!
}

type Query {
pet: Pet
}
"""
)
23 changes: 20 additions & 3 deletions graphene_django/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
from typing import Type


def construct_fields(model, registry, only_fields, exclude_fields):
def construct_fields(
model, registry, only_fields, exclude_fields, convert_choices_to_enum
):
_model_fields = get_model_fields(model)

fields = OrderedDict()
Expand All @@ -33,7 +35,18 @@ def construct_fields(model, registry, only_fields, exclude_fields):
# in there. Or when we exclude this field in exclude_fields.
# Or when there is no back reference.
continue
converted = convert_django_field_with_choices(field, registry)

_convert_choices_to_enum = convert_choices_to_enum
if not isinstance(_convert_choices_to_enum, bool):
# then `convert_choices_to_enum` is a list of field names to convert
if name in _convert_choices_to_enum:
_convert_choices_to_enum = True
else:
_convert_choices_to_enum = False

converted = convert_django_field_with_choices(
field, registry, convert_choices_to_enum=_convert_choices_to_enum
)
fields[name] = converted

return fields
Expand Down Expand Up @@ -63,6 +76,7 @@ def __init_subclass_with_meta__(
connection_class=None,
use_connection=None,
interfaces=(),
convert_choices_to_enum=True,
_meta=None,
**options
):
Expand Down Expand Up @@ -90,7 +104,10 @@ def __init_subclass_with_meta__(
)

django_fields = yank_fields_from_attrs(
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
construct_fields(
model, registry, only_fields, exclude_fields, convert_choices_to_enum
),
_as=Field,
)

if use_connection is None and interfaces:
Expand Down