diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 0de6964..bccc46b 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -1,11 +1,17 @@ from collections import OrderedDict +from functools import partial, wraps + from django.db import models from django.utils.encoding import force_str from django.utils.module_loading import import_string +from graphql import assert_valid_name from graphene import ( ID, + UUID, Boolean, + Date, + DateTime, Dynamic, Enum, Field, @@ -14,24 +20,39 @@ from graphene import ( List, NonNull, String, - UUID, - DateTime, - Date, Time, ) +from graphene.types.resolver import get_default_resolver from graphene.types.json import JSONString from graphene.utils.str_converters import to_camel_case -from graphql import assert_valid_name -from .settings import graphene_settings from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField -from .fields import DjangoListField, DjangoConnectionField +from .fields import DjangoConnectionField, DjangoListField +from .settings import graphene_settings from .utils import import_single_dispatch from .utils.str_converters import to_const singledispatch = import_single_dispatch() +class BlankValueField(Field): + def get_resolver(self, parent_resolver): + resolver = self.resolver or parent_resolver + + # create custom resolver + def blank_field_wrapper(func): + @wraps(func) + def wrapped_resolver(*args, **kwargs): + return_value = func(*args, **kwargs) + if return_value == "": + return None + return return_value + + return wrapped_resolver + + return blank_field_wrapper(resolver) + + def convert_choice_name(name): name = to_const(force_str(name)) try: @@ -68,7 +89,8 @@ def convert_choices_to_named_enum_with_descriptions(name, choices): def description(self): return named_choices_descriptions[self.name] - return Enum(name, list(named_choices), type=EnumWithDescriptionsType) + return_type = Enum(name, list(named_choices), type=EnumWithDescriptionsType) + return return_type def generate_enum_name(django_model_meta, field): @@ -105,9 +127,12 @@ def convert_django_field_with_choices( return converted choices = getattr(field, "choices", None) if choices and convert_choices_to_enum: - enum = convert_choice_field_to_enum(field) + EnumCls = convert_choice_field_to_enum(field) required = not (field.blank or field.null) - converted = enum(description=field.help_text, required=required) + + converted = EnumCls(description=field.help_text, required=required).mount_as( + BlankValueField + ) else: converted = convert_django_field(field, registry) if registry is not None: diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 7d8e669..877befe 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -394,3 +394,43 @@ def test_generate_enum_name(graphene_settings): generate_enum_name(model_meta, field) == "SomeLongAppNameSomeObjectFizzBuzzChoices" ) + + +def test_choice_enum_blank_value(): + """Test that choice fields with blank values work""" + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + fields = ( + "first_name", + "a_choice", + ) + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + + def resolve_reporter(root, info): + return Reporter.objects.first() + + 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( + """ + query { + reporter { + firstName + aChoice + } + } + """ + ) + assert not result.errors + assert result.data == { + "reporter": {"firstName": "Bridget", "aChoice": None}, + }