mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-02-10 08:30:35 +03:00
fix typed choices, make working with different Django 5x choices options
This commit is contained in:
parent
269225085d
commit
7e41825c6d
|
@ -1,10 +1,11 @@
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
|
|
||||||
# For backwards compatibility, we import JSONField to have it available for import via
|
# For backwards compatibility, we import JSONField to have it available for import via
|
||||||
# this compat module (https://github.com/graphql-python/graphene-django/issues/1428).
|
# this compat module (https://github.com/graphql-python/graphene-django/issues/1428).
|
||||||
# Django's JSONField is available in Django 3.2+ (the minimum version we support)
|
# Django's JSONField is available in Django 3.2+ (the minimum version we support)
|
||||||
from django.db.models import JSONField
|
from django.db.models import Choices, JSONField
|
||||||
|
|
||||||
|
|
||||||
class MissingType:
|
class MissingType:
|
||||||
|
@ -42,3 +43,23 @@ except ImportError:
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ArrayField = MissingType
|
ArrayField = MissingType
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.utils.choices import normalize_choices
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def normalize_choices(choices):
|
||||||
|
if isinstance(choices, type) and issubclass(choices, Choices):
|
||||||
|
choices = choices.choices
|
||||||
|
|
||||||
|
if isinstance(choices, Callable):
|
||||||
|
choices = choices()
|
||||||
|
|
||||||
|
# In restframework==3.15.0, choices are not passed
|
||||||
|
# as OrderedDict anymore, so it's safer to check
|
||||||
|
# for a dict
|
||||||
|
if isinstance(choices, dict):
|
||||||
|
choices = choices.items()
|
||||||
|
|
||||||
|
return choices
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import inspect
|
import inspect
|
||||||
from collections.abc import Callable
|
|
||||||
from functools import partial, singledispatch, wraps
|
from functools import partial, singledispatch, wraps
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -37,7 +36,7 @@ except ImportError:
|
||||||
from graphql import assert_valid_name as assert_name
|
from graphql import assert_valid_name as assert_name
|
||||||
from graphql.pyutils import register_description
|
from graphql.pyutils import register_description
|
||||||
|
|
||||||
from .compat import ArrayField, HStoreField, RangeField
|
from .compat import ArrayField, HStoreField, RangeField, normalize_choices
|
||||||
from .fields import DjangoConnectionField, DjangoListField
|
from .fields import DjangoConnectionField, DjangoListField
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
from .utils.str_converters import to_const
|
from .utils.str_converters import to_const
|
||||||
|
@ -61,6 +60,24 @@ class BlankValueField(Field):
|
||||||
return blank_field_wrapper(resolver)
|
return blank_field_wrapper(resolver)
|
||||||
|
|
||||||
|
|
||||||
|
class EnumValueField(BlankValueField):
|
||||||
|
def wrap_resolve(self, parent_resolver):
|
||||||
|
resolver = super().wrap_resolve(parent_resolver)
|
||||||
|
|
||||||
|
# create custom resolver
|
||||||
|
def enum_field_wrapper(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped_resolver(*args, **kwargs):
|
||||||
|
return_value = func(*args, **kwargs)
|
||||||
|
if isinstance(return_value, models.Choices):
|
||||||
|
return_value = return_value.value
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
return wrapped_resolver
|
||||||
|
|
||||||
|
return enum_field_wrapper(resolver)
|
||||||
|
|
||||||
|
|
||||||
def convert_choice_name(name):
|
def convert_choice_name(name):
|
||||||
name = to_const(force_str(name))
|
name = to_const(force_str(name))
|
||||||
try:
|
try:
|
||||||
|
@ -72,15 +89,7 @@ def convert_choice_name(name):
|
||||||
|
|
||||||
def get_choices(choices):
|
def get_choices(choices):
|
||||||
converted_names = []
|
converted_names = []
|
||||||
if isinstance(choices, Callable):
|
choices = normalize_choices(choices)
|
||||||
choices = choices()
|
|
||||||
|
|
||||||
# In restframework==3.15.0, choices are not passed
|
|
||||||
# as OrderedDict anymore, so it's safer to check
|
|
||||||
# for a dict
|
|
||||||
if isinstance(choices, dict):
|
|
||||||
choices = choices.items()
|
|
||||||
|
|
||||||
for value, help_text in choices:
|
for value, help_text in choices:
|
||||||
if isinstance(help_text, (tuple, list)):
|
if isinstance(help_text, (tuple, list)):
|
||||||
yield from get_choices(help_text)
|
yield from get_choices(help_text)
|
||||||
|
@ -157,7 +166,7 @@ def convert_django_field_with_choices(
|
||||||
|
|
||||||
converted = EnumCls(
|
converted = EnumCls(
|
||||||
description=get_django_field_description(field), required=required
|
description=get_django_field_description(field), required=required
|
||||||
).mount_as(BlankValueField)
|
).mount_as(EnumValueField)
|
||||||
else:
|
else:
|
||||||
converted = convert_django_field(field, registry)
|
converted = convert_django_field(field, registry)
|
||||||
if registry is not None:
|
if registry is not None:
|
||||||
|
|
|
@ -3,7 +3,7 @@ from graphene import ID
|
||||||
from graphene.types.inputobjecttype import InputObjectType
|
from graphene.types.inputobjecttype import InputObjectType
|
||||||
from graphene.utils.str_converters import to_camel_case
|
from graphene.utils.str_converters import to_camel_case
|
||||||
|
|
||||||
from ..converter import BlankValueField
|
from ..converter import EnumValueField
|
||||||
from ..types import ErrorType # noqa Import ErrorType for backwards compatibility
|
from ..types import ErrorType # noqa Import ErrorType for backwards compatibility
|
||||||
from .mutation import fields_for_form
|
from .mutation import fields_for_form
|
||||||
|
|
||||||
|
@ -57,11 +57,10 @@ class DjangoFormInputObjectType(InputObjectType):
|
||||||
if (
|
if (
|
||||||
object_type
|
object_type
|
||||||
and name in object_type._meta.fields
|
and name in object_type._meta.fields
|
||||||
and isinstance(object_type._meta.fields[name], BlankValueField)
|
and isinstance(object_type._meta.fields[name], EnumValueField)
|
||||||
):
|
):
|
||||||
# Field type BlankValueField here means that field
|
# Field type EnumValueField here means that field
|
||||||
# with choices have been converted to enum
|
# with choices have been converted to enum
|
||||||
# (BlankValueField is using only for that task ?)
|
|
||||||
setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type))
|
setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type))
|
||||||
elif (
|
elif (
|
||||||
object_type
|
object_type
|
||||||
|
|
|
@ -1,9 +1,38 @@
|
||||||
|
import django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
CHOICES = ((1, "this"), (2, _("that")))
|
CHOICES = ((1, "this"), (2, _("that")))
|
||||||
|
|
||||||
|
|
||||||
|
def get_choices_as_class(choices_class):
|
||||||
|
if django.VERSION >= (5, 0):
|
||||||
|
return choices_class
|
||||||
|
else:
|
||||||
|
return choices_class.choices
|
||||||
|
|
||||||
|
|
||||||
|
def get_choices_as_callable(choices_class):
|
||||||
|
if django.VERSION >= (5, 0):
|
||||||
|
|
||||||
|
def choices():
|
||||||
|
return choices_class.choices
|
||||||
|
|
||||||
|
return choices
|
||||||
|
else:
|
||||||
|
return choices_class.choices
|
||||||
|
|
||||||
|
|
||||||
|
class TypedIntChoice(models.IntegerChoices):
|
||||||
|
CHOICE_THIS = 1
|
||||||
|
CHOICE_THAT = 2
|
||||||
|
|
||||||
|
|
||||||
|
class TypedStrChoice(models.TextChoices):
|
||||||
|
CHOICE_THIS = "this"
|
||||||
|
CHOICE_THAT = "that"
|
||||||
|
|
||||||
|
|
||||||
class Person(models.Model):
|
class Person(models.Model):
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
|
@ -51,6 +80,21 @@ class Reporter(models.Model):
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
pets = models.ManyToManyField("self")
|
pets = models.ManyToManyField("self")
|
||||||
a_choice = models.IntegerField(choices=CHOICES, null=True, blank=True)
|
a_choice = models.IntegerField(choices=CHOICES, null=True, blank=True)
|
||||||
|
typed_choice = models.IntegerField(
|
||||||
|
choices=TypedIntChoice.choices,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
class_choice = models.IntegerField(
|
||||||
|
choices=get_choices_as_class(TypedIntChoice),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
callable_choice = models.IntegerField(
|
||||||
|
choices=get_choices_as_callable(TypedStrChoice),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
doe_objects = DoeReporterManager()
|
doe_objects = DoeReporterManager()
|
||||||
fans = models.ManyToManyField(Person)
|
fans = models.ManyToManyField(Person)
|
||||||
|
|
|
@ -25,7 +25,7 @@ from ..converter import (
|
||||||
)
|
)
|
||||||
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, TypedIntChoice, TypedStrChoice
|
||||||
|
|
||||||
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
|
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
|
||||||
|
|
||||||
|
@ -443,35 +443,102 @@ def test_choice_enum_blank_value():
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Reporter
|
model = Reporter
|
||||||
fields = (
|
fields = ("callable_choice",)
|
||||||
"first_name",
|
|
||||||
"a_choice",
|
|
||||||
)
|
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
reporter = graphene.Field(ReporterType)
|
reporter = graphene.Field(ReporterType)
|
||||||
|
|
||||||
def resolve_reporter(root, info):
|
def resolve_reporter(root, info):
|
||||||
return Reporter.objects.first()
|
# return a model instance with blank choice field value
|
||||||
|
return Reporter(callable_choice="")
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
# Create model with empty choice option
|
|
||||||
Reporter.objects.create(
|
|
||||||
first_name="Bridget", last_name="Jones", email="bridget@example.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
"""
|
"""
|
||||||
query {
|
query {
|
||||||
reporter {
|
reporter {
|
||||||
firstName
|
callableChoice
|
||||||
aChoice
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {
|
assert result.data == {
|
||||||
"reporter": {"firstName": "Bridget", "aChoice": None},
|
"reporter": {"callableChoice": None},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_typed_choice_value():
|
||||||
|
"""Test that typed choices fields are resolved correctly to the enum values"""
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
fields = ("typed_choice", "class_choice", "callable_choice")
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
reporter = graphene.Field(ReporterType)
|
||||||
|
|
||||||
|
def resolve_reporter(root, info):
|
||||||
|
# assign choice values to the fields instead of their str or int values
|
||||||
|
return Reporter(
|
||||||
|
typed_choice=TypedIntChoice.CHOICE_THIS,
|
||||||
|
class_choice=TypedIntChoice.CHOICE_THAT,
|
||||||
|
callable_choice=TypedStrChoice.CHOICE_THIS,
|
||||||
|
)
|
||||||
|
|
||||||
|
class CreateReporter(graphene.Mutation):
|
||||||
|
reporter = graphene.Field(ReporterType)
|
||||||
|
|
||||||
|
def mutate(root, info, **kwargs):
|
||||||
|
return CreateReporter(
|
||||||
|
reporter=Reporter(
|
||||||
|
typed_choice=TypedIntChoice.CHOICE_THIS,
|
||||||
|
class_choice=TypedIntChoice.CHOICE_THAT,
|
||||||
|
callable_choice=TypedStrChoice.CHOICE_THIS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
create_reporter = CreateReporter.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query, mutation=Mutation)
|
||||||
|
|
||||||
|
reporter_fragment = """
|
||||||
|
fragment reporter on ReporterType {
|
||||||
|
typedChoice
|
||||||
|
classChoice
|
||||||
|
callableChoice
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_reporter = {
|
||||||
|
"typedChoice": "A_1",
|
||||||
|
"classChoice": "A_2",
|
||||||
|
"callableChoice": "THIS",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
reporter_fragment
|
||||||
|
+ """
|
||||||
|
query {
|
||||||
|
reporter { ...reporter }
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["reporter"] == expected_reporter
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
reporter_fragment
|
||||||
|
+ """
|
||||||
|
mutation {
|
||||||
|
createReporter {
|
||||||
|
reporter { ...reporter }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["createReporter"]["reporter"] == expected_reporter
|
||||||
|
|
|
@ -40,6 +40,9 @@ def test_should_map_fields_correctly():
|
||||||
"email",
|
"email",
|
||||||
"pets",
|
"pets",
|
||||||
"a_choice",
|
"a_choice",
|
||||||
|
"typed_choice",
|
||||||
|
"class_choice",
|
||||||
|
"callable_choice",
|
||||||
"fans",
|
"fans",
|
||||||
"reporter_type",
|
"reporter_type",
|
||||||
]
|
]
|
||||||
|
|
|
@ -77,6 +77,9 @@ def test_django_objecttype_map_correct_fields():
|
||||||
"email",
|
"email",
|
||||||
"pets",
|
"pets",
|
||||||
"a_choice",
|
"a_choice",
|
||||||
|
"typed_choice",
|
||||||
|
"class_choice",
|
||||||
|
"callable_choice",
|
||||||
"fans",
|
"fans",
|
||||||
"reporter_type",
|
"reporter_type",
|
||||||
]
|
]
|
||||||
|
@ -186,6 +189,9 @@ def test_schema_representation():
|
||||||
email: String!
|
email: String!
|
||||||
pets: [Reporter!]!
|
pets: [Reporter!]!
|
||||||
aChoice: TestsReporterAChoiceChoices
|
aChoice: TestsReporterAChoiceChoices
|
||||||
|
typedChoice: TestsReporterTypedChoiceChoices
|
||||||
|
classChoice: TestsReporterClassChoiceChoices
|
||||||
|
callableChoice: TestsReporterCallableChoiceChoices
|
||||||
reporterType: TestsReporterReporterTypeChoices
|
reporterType: TestsReporterReporterTypeChoices
|
||||||
articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
|
articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
|
||||||
}
|
}
|
||||||
|
@ -199,6 +205,33 @@ def test_schema_representation():
|
||||||
A_2
|
A_2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum TestsReporterTypedChoiceChoices {
|
||||||
|
\"""Choice This\"""
|
||||||
|
A_1
|
||||||
|
|
||||||
|
\"""Choice That\"""
|
||||||
|
A_2
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum TestsReporterClassChoiceChoices {
|
||||||
|
\"""Choice This\"""
|
||||||
|
A_1
|
||||||
|
|
||||||
|
\"""Choice That\"""
|
||||||
|
A_2
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum TestsReporterCallableChoiceChoices {
|
||||||
|
\"""Choice This\"""
|
||||||
|
THIS
|
||||||
|
|
||||||
|
\"""Choice That\"""
|
||||||
|
THAT
|
||||||
|
}
|
||||||
|
|
||||||
\"""An enumeration.\"""
|
\"""An enumeration.\"""
|
||||||
enum TestsReporterReporterTypeChoices {
|
enum TestsReporterReporterTypeChoices {
|
||||||
\"""Regular\"""
|
\"""Regular\"""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user