mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-13 17:52:19 +03:00
Introduce DjangoChoicesEnum
This commit is contained in:
parent
223d0b1d28
commit
c827267cb9
2
Makefile
2
Makefile
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
84
docs/types.rst
Normal 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()
|
|
@ -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",
|
||||||
|
]
|
||||||
|
|
|
@ -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],
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"},
|
||||||
|
]
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user