Introduce DjangoChoicesEnum

This commit is contained in:
Paul Hallett 2019-05-08 17:29:51 +01:00
parent 223d0b1d28
commit c827267cb9
No known key found for this signature in database
GPG Key ID: 529C11F0C93CDF11
10 changed files with 254 additions and 22 deletions

View File

@ -2,7 +2,7 @@ dev-setup:
pip install -e ".[dev]" pip install -e ".[dev]"
tests: tests:
py.test graphene_django --cov=graphene_django -vv py.test graphene_django --cov=graphene_django -vv -x
format: format:
black graphene_django black graphene_django

View File

@ -26,6 +26,7 @@ For more advanced use, check out the Relay tutorial.
schema schema
queries queries
mutations mutations
types
filtering filtering
authorization authorization
debug debug

View File

@ -30,7 +30,7 @@ Default: ``None``
``SCHEMA_OUTPUT`` ``SCHEMA_OUTPUT``
---------- -----------------
The name of the file where the GraphQL schema output will go. The name of the file where the GraphQL schema output will go.
@ -44,7 +44,7 @@ Default: ``schema.json``
``SCHEMA_INDENT`` ``SCHEMA_INDENT``
---------- -----------------
The indentation level of the schema output. The indentation level of the schema output.
@ -58,7 +58,7 @@ Default: ``2``
``MIDDLEWARE`` ``MIDDLEWARE``
---------- --------------
A tuple of middleware that will be executed for each GraphQL query. A tuple of middleware that will be executed for each GraphQL query.
@ -76,7 +76,7 @@ Default: ``()``
``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST`` ``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
---------- ------------------------------------------
Enforces relay queries to have the ``first`` or ``last`` argument. Enforces relay queries to have the ``first`` or ``last`` argument.
@ -90,7 +90,7 @@ Default: ``False``
``RELAY_CONNECTION_MAX_LIMIT`` ``RELAY_CONNECTION_MAX_LIMIT``
---------- ------------------------------
The maximum size of objects that can be requested through a relay connection. The maximum size of objects that can be requested through a relay connection.
@ -100,4 +100,4 @@ Default: ``100``
GRAPHENE = { GRAPHENE = {
'RELAY_CONNECTION_MAX_LIMIT': 100, 'RELAY_CONNECTION_MAX_LIMIT': 100,
} }

84
docs/types.rst Normal file
View File

@ -0,0 +1,84 @@
Types
=====
This page documents specific features of Types related to Graphene-Django.
DjangoChoicesEnum
-----------------
``DjangoChoicesEnum`` is a helper class that lets you keep Graphene style enums
and ``models.Field.choices`` in sync. Some Django fields accept a ``choices`` list like this:
.. code:: python
choices = [
('FOO', 'foo'),
('BAR', 'bar'),
]
class MyModel(models.Model):
options = models.CharField(max_length='3', choices=choices)
With Graphene-Django it is useful to represent these choices as an enum:
.. code::
query getEnumType {
__type(name: "MyModelOptions" ) {
name
enumValues {
name
description
}
}
}
Which will return a data structure like this:
.. code::
{
"data": {
"__type": {
"name": "MyModelOptions",
"enumValues": [
{
"name": "FOO",
"description": "foo"
},
{
"name": "BAR",
"description": "bar"
}
]
}
}
}
We can use ``DjangoChoicesEnum`` to support both of these for us:
.. code:: python
from graphene_django import DjangoObjectType, DjangoChoicesEnum
from django.db import models
# Declare your DjangoChoicesEnum
class MyModelChoices(DjangoChoicesEnum):
FOO = 'foo'
BAR = 'bar'
# Your model should use the .choices method
class MyModel(models.Model):
options = models.CharField(
max_length='3',
choices=DjangoChoicesEnum.choices(),
default=DjangoChoicesEnum.choices()[0][0],
)
# And your ObjectType should explicitly declare the type:
class MyModelType(DjangoObjectType):
class Meta:
model = MyModel
fields = ('options',)
options = MyModelChoices.as_enum()

View File

@ -1,6 +1,11 @@
from .types import DjangoObjectType from .types import DjangoObjectType, DjangoChoicesEnum
from .fields import DjangoConnectionField from .fields import DjangoConnectionField
__version__ = "2.2.0" __version__ = "2.2.0"
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"] __all__ = [
"__version__",
"DjangoObjectType",
"DjangoConnectionField",
"DjangoChoicesEnum",
]

View File

@ -1,5 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from ..types import DjangoChoicesEnum
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -103,3 +105,17 @@ class Article(models.Model):
class Meta: class Meta:
ordering = ("headline",) ordering = ("headline",)
class MyCustomChoices(DjangoChoicesEnum):
DO = "Documentary"
OT = "Other"
class FilmWithChoices(models.Model):
genre = models.CharField(
max_length=2,
help_text="Genre",
choices=MyCustomChoices.choices(),
default=MyCustomChoices.choices()[0][0],
)

View File

@ -10,11 +10,11 @@ from graphene.types.json import JSONString
from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
from ..converter import convert_django_field, convert_django_field_with_choices from ..converter import convert_django_field, convert_django_field_with_choices
from ..settings import graphene_settings
from ..registry import Registry from ..registry import Registry
from ..types import DjangoObjectType from ..types import DjangoObjectType
from .models import Article, Film, FilmDetails, Reporter from .models import Article, Film, FilmDetails, Reporter
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString # from graphene.core.types.custom_scalars import DateTime, Time, JSONString
@ -128,10 +128,12 @@ def test_should_nullboolean_convert_boolean():
assert_conversion(models.NullBooleanField, graphene.Boolean) assert_conversion(models.NullBooleanField, graphene.Boolean)
def test_field_with_choices_convert_enum(): @pytest.mark.parametrize(
field = models.CharField( "choices",
help_text="Language", choices=(("es", "Spanish"), ("en", "English")) ((("es", "Spanish"), ("en", "English")), [("es", "Spanish"), ("en", "English")]),
) )
def test_field_with_choices_convert_enum(choices):
field = models.CharField(help_text="Language", choices=choices)
class TranslatedModel(models.Model): class TranslatedModel(models.Model):
language = field language = field

View File

@ -14,8 +14,17 @@ from ..utils import DJANGO_FILTER_INSTALLED
from ..compat import MissingType, JSONField from ..compat import MissingType, JSONField
from ..fields import DjangoConnectionField from ..fields import DjangoConnectionField
from ..types import DjangoObjectType from ..types import DjangoObjectType
from ..registry import reset_global_registry
from ..settings import graphene_settings from ..settings import graphene_settings
from .models import Article, CNNReporter, Reporter, Film, FilmDetails from .models import (
Article,
CNNReporter,
Reporter,
Film,
FilmWithChoices,
MyCustomChoices,
FilmDetails,
)
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -145,9 +154,6 @@ def test_should_query_postgres_fields():
def test_should_node(): def test_should_node():
# reset_global_registry()
# Node._meta.registry = get_global_registry()
class ReporterNode(DjangoObjectType): class ReporterNode(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
@ -413,9 +419,6 @@ def test_should_query_node_filtering_with_distinct_queryset():
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
films = DjangoConnectionField(FilmType) films = DjangoConnectionField(FilmType)
# def resolve_all_reporters_with_berlin_films(self, args, context, info):
# return Reporter.objects.filter(Q(films__film__location__contains="Berlin") | Q(a_choice=1))
def resolve_films(self, info, **args): def resolve_films(self, info, **args):
return Film.objects.filter( return Film.objects.filter(
Q(details__location__contains="Berlin") | Q(genre__in=["ot"]) Q(details__location__contains="Berlin") | Q(genre__in=["ot"])
@ -1051,3 +1054,57 @@ def test_should_resolve_get_queryset_connectionfields():
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
assert result.data == expected assert result.data == expected
def test_using_django_choices_enum():
reset_global_registry()
class FilmWithChoicesType(DjangoObjectType):
class Meta:
model = FilmWithChoices
interfaces = (Node,)
genre = MyCustomChoices.as_enum()
class Query(graphene.ObjectType):
films_with_choices = DjangoConnectionField(FilmWithChoicesType)
def resolve_films(self, info, **args):
return Film.objects.all()
f = FilmWithChoices.objects.create()
query = """
query NodeFilteringQuery {
filmsWithChoices {
edges {
node {
genre
}
}
}
}
"""
schema = graphene.Schema(query=Query)
result = schema.execute(query)
assert not result.errors
assert result.data["filmsWithChoices"]["edges"][0]["node"]["genre"] == "DO"
query = """
query getEnumType {
__type(name: "FilmWithChoicesGenre" ) {
name
enumValues {
name
description
}
}
}
"""
result = schema.execute(query)
assert not result.errors
enum_values = result.data["__type"]["enumValues"]
assert enum_values == [
{"name": "DO", "description": "Documentary"},
{"name": "OT", "description": "Other"},
]

View File

@ -2,9 +2,10 @@ from mock import patch
from graphene import Interface, ObjectType, Schema, Connection, String from graphene import Interface, ObjectType, Schema, Connection, String
from graphene.relay import Node from graphene.relay import Node
from graphene.types.enum import Enum, EnumMeta, EnumOptions
from .. import registry from .. import registry
from ..types import DjangoObjectType, DjangoObjectTypeOptions from ..types import DjangoObjectType, DjangoObjectTypeOptions, DjangoChoicesEnum
from .models import Article as ArticleModel from .models import Article as ArticleModel
from .models import Reporter as ReporterModel from .models import Reporter as ReporterModel
@ -224,3 +225,21 @@ def test_django_objecttype_exclude_fields():
fields = list(Reporter._meta.fields.keys()) fields = list(Reporter._meta.fields.keys())
assert "email" not in fields assert "email" not in fields
def test_custom_django_choices_enum():
class MyChoicesEnum(DjangoChoicesEnum):
FOO = "foo"
BAR = "bar"
# As a Graphene enum
graphene_enum = MyChoicesEnum.as_enum()
assert isinstance(graphene_enum, EnumMeta)
assert isinstance(graphene_enum._meta, EnumOptions)
assert graphene_enum.FOO.value == "foo"
assert graphene_enum.FOO.name == "FOO"
assert graphene_enum._meta.name == "MyChoicesEnum"
assert graphene_enum._meta.description(graphene_enum.FOO) == "foo"
# As a Django choices option
assert MyChoicesEnum.choices() == [("FOO", "foo"), ("BAR", "bar")]

View File

@ -1,6 +1,8 @@
import six import six
from collections import OrderedDict from collections import OrderedDict
from enum import Enum
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
@ -150,3 +152,49 @@ 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)
def get_attr_fields_name_from_class(enum):
"""
Used to get field names from a class like below:
class ExampleEnum:
FOO = 'FOO VALUE'
BAR = 'BAR VALUE'
it will return ['FOO', 'BAR']
"""
return [x for x in enum.__dict__ if not x.startswith("__")]
class DjangoChoicesEnum(object):
@classmethod
def choices(cls):
"""
Used to convert the this class into django CharField choices.
Example:
# enums.py:
ChoicesType:
TEST = 'test'
# models.py:
class Model(models.model)
some_field = CharField(choices=ChoicesType.choices())
"""
return [(k, v) for k, v in cls.__dict__.items() if not k.startswith("__")]
@classmethod
def as_enum(cls):
"""
Gets a Python class that looks like enum e.g.
class TestEnum:
FOO = 'BAR'
FOO2 = 'BAR2'
and returns a Graphene enum representing the Python enum
"""
props = get_attr_fields_name_from_class(cls)
new_enum = Enum(cls.__name__, ((prop, getattr(cls, prop)) for prop in props))
def get_description(value):
return getattr(cls, value.name, None) if value else None
return graphene.Enum.from_enum(new_enum, description=get_description)