mirror of
				https://github.com/graphql-python/graphene-django.git
				synced 2025-11-04 18:08:01 +03:00 
			
		
		
		
	Add options to override how Django Choice fields are converted t… (#860)
* Add new setting to create unique enum names * Add specific tests for name generation * Add schema test * Rename settings field * Rename setting * Add custom function setting * Add documentation * Use format instead of f strings * Update graphene_django/converter.py Co-Authored-By: Syrus Akbary <me@syrusakbary.com> * Fix tests * Update docs * Import function through import_string function Co-authored-by: Syrus Akbary <me@syrusakbary.com>
This commit is contained in:
		
							parent
							
								
									13352216a4
								
							
						
					
					
						commit
						b8e598d66d
					
				| 
						 | 
					@ -140,3 +140,33 @@ Default: ``False``
 | 
				
			||||||
   #         'messages': ['This field is required.'],
 | 
					   #         'messages': ['This field is required.'],
 | 
				
			||||||
   #     }
 | 
					   #     }
 | 
				
			||||||
   # ]
 | 
					   # ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING``
 | 
				
			||||||
 | 
					--------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Set to ``True`` to use the new naming format for the auto generated Enum types from Django choice fields. The new format looks like this: ``{app_label}{object_name}{field_name}Choices``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Default: ``False``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
 | 
				
			||||||
 | 
					--------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If set to a function then the ``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING`` setting is ignored.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Default: ``None``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. code:: python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   # myapp.utils
 | 
				
			||||||
 | 
					   def enum_naming(field):
 | 
				
			||||||
 | 
					      if isinstance(field.model, User):
 | 
				
			||||||
 | 
					         return f"CustomUserEnum{field.name.title()}"
 | 
				
			||||||
 | 
					      return f"CustomEnum{field.name.title()}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   GRAPHENE = {
 | 
				
			||||||
 | 
					      'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
from collections import OrderedDict
 | 
					from collections import OrderedDict
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.utils.encoding import force_str
 | 
					from django.utils.encoding import force_str
 | 
				
			||||||
 | 
					from django.utils.module_loading import import_string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from graphene import (
 | 
					from graphene import (
 | 
				
			||||||
    ID,
 | 
					    ID,
 | 
				
			||||||
| 
						 | 
					@ -22,6 +23,7 @@ from graphene.types.json import JSONString
 | 
				
			||||||
from graphene.utils.str_converters import to_camel_case, to_const
 | 
					from graphene.utils.str_converters import to_camel_case, to_const
 | 
				
			||||||
from graphql import assert_valid_name
 | 
					from graphql import assert_valid_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .settings import graphene_settings
 | 
				
			||||||
from .compat import ArrayField, HStoreField, JSONField, RangeField
 | 
					from .compat import ArrayField, HStoreField, JSONField, RangeField
 | 
				
			||||||
from .fields import DjangoListField, DjangoConnectionField
 | 
					from .fields import DjangoListField, DjangoConnectionField
 | 
				
			||||||
from .utils import import_single_dispatch
 | 
					from .utils import import_single_dispatch
 | 
				
			||||||
| 
						 | 
					@ -68,6 +70,31 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
 | 
				
			||||||
    return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
 | 
					    return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def generate_enum_name(django_model_meta, field):
 | 
				
			||||||
 | 
					    if graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME:
 | 
				
			||||||
 | 
					        # Try and import custom function
 | 
				
			||||||
 | 
					        custom_func = import_string(
 | 
				
			||||||
 | 
					            graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        name = custom_func(field)
 | 
				
			||||||
 | 
					    elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING is True:
 | 
				
			||||||
 | 
					        name = "{app_label}{object_name}{field_name}Choices".format(
 | 
				
			||||||
 | 
					            app_label=to_camel_case(django_model_meta.app_label.title()),
 | 
				
			||||||
 | 
					            object_name=django_model_meta.object_name,
 | 
				
			||||||
 | 
					            field_name=to_camel_case(field.name.title()),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name))
 | 
				
			||||||
 | 
					    return name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def convert_choice_field_to_enum(field, name=None):
 | 
				
			||||||
 | 
					    if name is None:
 | 
				
			||||||
 | 
					        name = generate_enum_name(field.model._meta, field)
 | 
				
			||||||
 | 
					    choices = field.choices
 | 
				
			||||||
 | 
					    return convert_choices_to_named_enum_with_descriptions(name, choices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def convert_django_field_with_choices(
 | 
					def convert_django_field_with_choices(
 | 
				
			||||||
    field, registry=None, convert_choices_to_enum=True
 | 
					    field, registry=None, convert_choices_to_enum=True
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
| 
						 | 
					@ -77,9 +104,7 @@ def convert_django_field_with_choices(
 | 
				
			||||||
            return converted
 | 
					            return converted
 | 
				
			||||||
    choices = getattr(field, "choices", None)
 | 
					    choices = getattr(field, "choices", None)
 | 
				
			||||||
    if choices and convert_choices_to_enum:
 | 
					    if choices and convert_choices_to_enum:
 | 
				
			||||||
        meta = field.model._meta
 | 
					        enum = convert_choice_field_to_enum(field)
 | 
				
			||||||
        name = to_camel_case("{}_{}".format(meta.object_name, field.name))
 | 
					 | 
				
			||||||
        enum = convert_choices_to_named_enum_with_descriptions(name, choices)
 | 
					 | 
				
			||||||
        required = not (field.blank or field.null)
 | 
					        required = not (field.blank or field.null)
 | 
				
			||||||
        converted = enum(description=field.help_text, required=required)
 | 
					        converted = enum(description=field.help_text, required=required)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,9 @@ DEFAULTS = {
 | 
				
			||||||
    # Max items returned in ConnectionFields / FilterConnectionFields
 | 
					    # Max items returned in ConnectionFields / FilterConnectionFields
 | 
				
			||||||
    "RELAY_CONNECTION_MAX_LIMIT": 100,
 | 
					    "RELAY_CONNECTION_MAX_LIMIT": 100,
 | 
				
			||||||
    "CAMELCASE_ERRORS": False,
 | 
					    "CAMELCASE_ERRORS": False,
 | 
				
			||||||
 | 
					    # Set to True to enable v3 naming convention for choice field Enum's
 | 
				
			||||||
 | 
					    "DJANGO_CHOICE_FIELD_ENUM_V3_NAMING": False,
 | 
				
			||||||
 | 
					    "DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if settings.DEBUG:
 | 
					if settings.DEBUG:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					from collections import namedtuple
 | 
				
			||||||
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 _
 | 
				
			||||||
from graphene import NonNull
 | 
					from graphene import NonNull
 | 
				
			||||||
| 
						 | 
					@ -10,9 +11,14 @@ from graphene.types.datetime import DateTime, Date, Time
 | 
				
			||||||
from graphene.types.json import JSONString
 | 
					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,
 | 
				
			||||||
 | 
					    generate_enum_name,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from ..registry import Registry
 | 
					from ..registry import Registry
 | 
				
			||||||
from ..types import DjangoObjectType
 | 
					from ..types import DjangoObjectType
 | 
				
			||||||
 | 
					from ..settings import graphene_settings
 | 
				
			||||||
from .models import Article, Film, FilmDetails, Reporter
 | 
					from .models import Article, Film, FilmDetails, Reporter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -325,3 +331,25 @@ def test_should_postgres_range_convert_list():
 | 
				
			||||||
    assert isinstance(field.type, graphene.NonNull)
 | 
					    assert isinstance(field.type, graphene.NonNull)
 | 
				
			||||||
    assert isinstance(field.type.of_type, graphene.List)
 | 
					    assert isinstance(field.type.of_type, graphene.List)
 | 
				
			||||||
    assert field.type.of_type.of_type == graphene.Int
 | 
					    assert field.type.of_type.of_type == graphene.Int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_generate_enum_name():
 | 
				
			||||||
 | 
					    MockDjangoModelMeta = namedtuple("DjangoMeta", ["app_label", "object_name"])
 | 
				
			||||||
 | 
					    graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Simple case
 | 
				
			||||||
 | 
					    field = graphene.Field(graphene.String, name="type")
 | 
				
			||||||
 | 
					    model_meta = MockDjangoModelMeta(app_label="users", object_name="User")
 | 
				
			||||||
 | 
					    assert generate_enum_name(model_meta, field) == "UsersUserTypeChoices"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # More complicated multiple work case
 | 
				
			||||||
 | 
					    field = graphene.Field(graphene.String, name="fizz_buzz")
 | 
				
			||||||
 | 
					    model_meta = MockDjangoModelMeta(
 | 
				
			||||||
 | 
					        app_label="some_long_app_name", object_name="SomeObject"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert (
 | 
				
			||||||
 | 
					        generate_enum_name(model_meta, field)
 | 
				
			||||||
 | 
					        == "SomeLongAppNameSomeObjectFizzBuzzChoices"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,9 @@ from graphene import Connection, Field, Interface, ObjectType, Schema, String
 | 
				
			||||||
from graphene.relay import Node
 | 
					from graphene.relay import Node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .. import registry
 | 
					from .. import registry
 | 
				
			||||||
 | 
					from ..settings import graphene_settings
 | 
				
			||||||
from ..types import DjangoObjectType, DjangoObjectTypeOptions
 | 
					from ..types import DjangoObjectType, DjangoObjectTypeOptions
 | 
				
			||||||
 | 
					from ..converter import convert_choice_field_to_enum
 | 
				
			||||||
from .models import Article as ArticleModel
 | 
					from .models import Article as ArticleModel
 | 
				
			||||||
from .models import Reporter as ReporterModel
 | 
					from .models import Reporter as ReporterModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -386,6 +388,10 @@ def test_django_objecttype_exclude_fields_exist_on_model():
 | 
				
			||||||
    assert len(record) == 0
 | 
					    assert len(record) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def custom_enum_name(field):
 | 
				
			||||||
 | 
					    return "CustomEnum{}".format(field.name.title())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestDjangoObjectType:
 | 
					class TestDjangoObjectType:
 | 
				
			||||||
    @pytest.fixture
 | 
					    @pytest.fixture
 | 
				
			||||||
    def PetModel(self):
 | 
					    def PetModel(self):
 | 
				
			||||||
| 
						 | 
					@ -492,3 +498,78 @@ class TestDjangoObjectType:
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_django_objecttype_convert_choices_enum_naming_collisions(self, PetModel):
 | 
				
			||||||
 | 
					        graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class PetModelKind(DjangoObjectType):
 | 
				
			||||||
 | 
					            class Meta:
 | 
				
			||||||
 | 
					                model = PetModel
 | 
				
			||||||
 | 
					                fields = ["id", "kind"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class Query(ObjectType):
 | 
				
			||||||
 | 
					            pet = Field(PetModelKind)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        schema = Schema(query=Query)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert str(schema) == dedent(
 | 
				
			||||||
 | 
					            """\
 | 
				
			||||||
 | 
					        schema {
 | 
				
			||||||
 | 
					          query: Query
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        type PetModelKind {
 | 
				
			||||||
 | 
					          id: ID!
 | 
				
			||||||
 | 
					          kind: TestsPetModelKindChoices!
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        type Query {
 | 
				
			||||||
 | 
					          pet: PetModelKind
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        enum TestsPetModelKindChoices {
 | 
				
			||||||
 | 
					          CAT
 | 
				
			||||||
 | 
					          DOG
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_django_objecttype_choices_custom_enum_name(self, PetModel):
 | 
				
			||||||
 | 
					        graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = (
 | 
				
			||||||
 | 
					            "graphene_django.tests.test_types.custom_enum_name"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class PetModelKind(DjangoObjectType):
 | 
				
			||||||
 | 
					            class Meta:
 | 
				
			||||||
 | 
					                model = PetModel
 | 
				
			||||||
 | 
					                fields = ["id", "kind"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class Query(ObjectType):
 | 
				
			||||||
 | 
					            pet = Field(PetModelKind)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        schema = Schema(query=Query)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert str(schema) == dedent(
 | 
				
			||||||
 | 
					            """\
 | 
				
			||||||
 | 
					        schema {
 | 
				
			||||||
 | 
					          query: Query
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        enum CustomEnumKind {
 | 
				
			||||||
 | 
					          CAT
 | 
				
			||||||
 | 
					          DOG
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        type PetModelKind {
 | 
				
			||||||
 | 
					          id: ID!
 | 
				
			||||||
 | 
					          kind: CustomEnumKind!
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        type Query {
 | 
				
			||||||
 | 
					          pet: PetModelKind
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = None
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user