Repository: graphql-python/graphene-django

Issue:
2.5.0 to 2.6.0 error:  TypeError: issubclass() arg 1 must be a class
Hello,

Thanks for all the work on the new release.

I am however getting this error when upgrading to 2.6.0 from 2.5.0 and I just wanted to make sure I understand the change.

```py
 Traceback (most recent call last):
   File "src/manage.py", line 22, in <module>
     execute_from_command_line(sys.argv)
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
     utility.execute()
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/core/management/__init__.py", line 375, in execute
     self.fetch_command(subcommand).run_from_argv(self.argv)
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/core/management/base.py", line 323, in run_from_argv
     self.execute(*args, **cmd_options)
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/core/management/base.py", line 361, in execute
     self.check()
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/core/management/base.py", line 390, in check
     include_deployment_checks=include_deployment_checks,
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/core/management/commands/migrate.py", line 65, in _run_checks
     issues.extend(super()._run_checks(**kwargs))
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/core/management/base.py", line 377, in _run_checks
     return checks.run_checks(**kwargs)
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/core/checks/registry.py", line 72, in run_checks
     new_errors = check(app_configs=app_configs)
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/contrib/admin/checks.py", line 111, in check_dependencies
     if not _contains_subclass('django.contrib.auth.middleware.AuthenticationMiddleware', settings.MIDDLEWARE):
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/contrib/admin/checks.py", line 43, in _contains_subclass
     candidate_cls = import_string(path)
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/django/utils/module_loading.py", line 17, in import_string
     module = import_module(module_path)
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/importlib/__init__.py", line 127, in import_module
     return _bootstrap._gcd_import(name[level:], package, level)
   File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
   File "<frozen importlib._bootstrap>", line 983, in _find_and_load
   File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
   File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
   File "<frozen importlib._bootstrap_external>", line 728, in exec_module
   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
   File "/app/src/ems_api/middleware.py", line 5, in <module>
     from ems.schema import loaders as ems_loaders, nodes
   File "/app/src/ems/schema/nodes.py", line 44, in <module>
     class BenefitSetNode(graphene_django.DjangoObjectType):
   File "/app/src/ems/schema/nodes.py", line 48, in BenefitSetNode
     benefits = fields.DjangoListField("ems.schema.nodes.BenefitNode")
   File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.7/site-packages/graphene_django/fields.py", line 23, in __init__
     _type, DjangoObjectType
 TypeError: issubclass() arg 1 must be a class
```

Does this mean I can no longer pass strings into `DjangoListField`?

e.g. `benefits = fields.DjangoListField("ems.schema.nodes.BenefitNode")`

Discussion:

Patch:
diff --git a/docs/authorization.rst b/docs/authorization.rst
index ebc9795..63123b0 100644
--- a/docs/authorization.rst
+++ b/docs/authorization.rst
@@ -166,7 +166,7 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR
 
 After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
 
-For Django 1.9 and below:
+For Django 1.11:
 
 .. code:: python
 
diff --git a/docs/installation.rst b/docs/installation.rst
index 52f2520..048a994 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -8,7 +8,7 @@ Requirements
 
 Graphene-Django currently supports the following versions of Django:
 
-* Django 2.X
+* >= Django 1.11
 
 Installation
 ------------
@@ -32,6 +32,20 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of
 
 We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
 
+For Django 1.11:
+
+.. code:: python
+
+    from django.conf.urls import url
+    from graphene_django.views import GraphQLView
+
+    urlpatterns = [
+        # ...
+        url(r"graphql", GraphQLView.as_view(graphiql=True)),
+    ]
+
+For Django 2.0 and above:
+
 .. code:: python
 
     from django.urls import path
diff --git a/docs/introspection.rst b/docs/introspection.rst
index dea55bd..2097c30 100644
--- a/docs/introspection.rst
+++ b/docs/introspection.rst
@@ -29,6 +29,20 @@ you're ready to use Relay with Graphene GraphQL implementation.
 
 The schema file is sorted to create a reproducible canonical representation.
 
+GraphQL SDL Representation
+--------------------------
+
+The schema can also be exported as a GraphQL SDL file by changing the file
+extension :
+
+.. code:: bash
+
+    ./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.graphql
+
+When exporting the schema as a ``.graphql`` file the ``--indent`` option is
+ignored.
+
+
 Advanced Usage
 --------------
 
diff --git a/docs/settings.rst b/docs/settings.rst
index 4776ce0..5a7e4c9 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -140,3 +140,33 @@ Default: ``False``
    #         '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"
+   }
diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst
index c3ee269..e80f9ab 100644
--- a/docs/tutorial-plain.rst
+++ b/docs/tutorial-plain.rst
@@ -286,7 +286,7 @@ from the command line.
     $ python ./manage.py runserver
 
     Performing system checks...
-    Django version 1.9, using settings 'cookbook.settings'
+    Django version 1.11, using settings 'cookbook.settings'
     Starting development server at http://127.0.0.1:8000/
     Quit the server with CONTROL-C.
 
diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst
index 5f8bd64..e900ea1 100644
--- a/docs/tutorial-relay.rst
+++ b/docs/tutorial-relay.rst
@@ -277,7 +277,7 @@ from the command line.
     $ python ./manage.py runserver
 
     Performing system checks...
-    Django version 1.9, using settings 'cookbook.settings'
+    Django version 1.11, using settings 'cookbook.settings'
     Starting development server at http://127.0.0.1:8000/
     Quit the server with CONTROL-C.
 
diff --git a/graphene_django/converter.py b/graphene_django/converter.py
index 8b93d17..bd8f79d 100644
--- a/graphene_django/converter.py
+++ b/graphene_django/converter.py
@@ -1,6 +1,7 @@
 from collections import OrderedDict
 from django.db import models
 from django.utils.encoding import force_str
+from django.utils.module_loading import import_string
 
 from graphene import (
     ID,
@@ -22,6 +23,7 @@ from graphene.types.json import JSONString
 from graphene.utils.str_converters import to_camel_case, to_const
 from graphql import assert_valid_name
 
+from .settings import graphene_settings
 from .compat import ArrayField, HStoreField, JSONField, RangeField
 from .fields import DjangoListField, DjangoConnectionField
 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)
 
 
+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(
     field, registry=None, convert_choices_to_enum=True
 ):
@@ -77,9 +104,7 @@ def convert_django_field_with_choices(
             return converted
     choices = getattr(field, "choices", None)
     if choices and convert_choices_to_enum:
-        meta = field.model._meta
-        name = to_camel_case("{}_{}".format(meta.object_name, field.name))
-        enum = convert_choices_to_named_enum_with_descriptions(name, choices)
+        enum = convert_choice_field_to_enum(field)
         required = not (field.blank or field.null)
         converted = enum(description=field.help_text, required=required)
     else:
diff --git a/graphene_django/fields.py b/graphene_django/fields.py
index 47b44f6..fb6b98a 100644
--- a/graphene_django/fields.py
+++ b/graphene_django/fields.py
@@ -1,5 +1,6 @@
 from functools import partial
 
+import six
 from django.db.models.query import QuerySet
 from graphql_relay.connection.arrayconnection import connection_from_list_slice
 from promise import Promise
@@ -19,19 +20,23 @@ class DjangoListField(Field):
         if isinstance(_type, NonNull):
             _type = _type.of_type
 
+        # Django would never return a Set of None  vvvvvvv
+        super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
+
         assert issubclass(
-            _type, DjangoObjectType
+            self._underlying_type, DjangoObjectType
         ), "DjangoListField only accepts DjangoObjectType types"
 
-        # Django would never return a Set of None  vvvvvvv
-        super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
+    @property
+    def _underlying_type(self):
+        _type = self._type
+        while hasattr(_type, "of_type"):
+            _type = _type.of_type
+        return _type
 
     @property
     def model(self):
-        _type = self.type.of_type
-        if isinstance(_type, NonNull):
-            _type = _type.of_type
-        return _type._meta.model
+        return self._underlying_type._meta.model
 
     @staticmethod
     def list_resolver(django_object_type, resolver, root, info, **args):
diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py
index 1eeeb97..b7bf049 100644
--- a/graphene_django/forms/mutation.py
+++ b/graphene_django/forms/mutation.py
@@ -47,7 +47,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
         else:
             errors = ErrorType.from_errors(form.errors)
 
-            return cls(errors=errors)
+            return cls(errors=errors, **form.data)
 
     @classmethod
     def get_form(cls, root, info, **input):
@@ -100,7 +100,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
     @classmethod
     def perform_mutate(cls, form, info):
         form.save()
-        return cls(errors=[])
+        return cls(errors=[], **form.cleaned_data)
 
 
 class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions):
diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py
index 1e8baf6..751a385 100644
--- a/graphene_django/management/commands/graphql_schema.py
+++ b/graphene_django/management/commands/graphql_schema.py
@@ -1,3 +1,4 @@
+import os
 import importlib
 import json
 import functools
@@ -5,6 +6,7 @@ import functools
 from django.core.management.base import BaseCommand, CommandError
 from django.utils import autoreload
 
+from graphql import print_schema
 from graphene_django.settings import graphene_settings
 
 
@@ -44,24 +46,40 @@ class CommandArguments(BaseCommand):
 
 
 class Command(CommandArguments):
-    help = "Dump Graphene schema JSON to file"
+    help = "Dump Graphene schema as a JSON or GraphQL file"
     can_import_settings = True
 
-    def save_file(self, out, schema_dict, indent):
+    def save_json_file(self, out, schema_dict, indent):
         with open(out, "w") as outfile:
             json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
 
+    def save_graphql_file(self, out, schema):
+        with open(out, "w") as outfile:
+            outfile.write(print_schema(schema))
+
     def get_schema(self, schema, out, indent):
         schema_dict = {"data": schema.introspect()}
         if out == "-":
             self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
         else:
-            self.save_file(out, schema_dict, indent)
+            # Determine format
+            _, file_extension = os.path.splitext(out)
+
+            if file_extension == ".graphql":
+                self.save_graphql_file(out, schema)
+            elif file_extension == ".json":
+                self.save_json_file(out, schema_dict, indent)
+            else:
+                raise CommandError(
+                    'Unrecognised file format "{}"'.format(file_extension)
+                )
 
             style = getattr(self, "style", None)
             success = getattr(style, "SUCCESS", lambda x: x)
 
-            self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
+            self.stdout.write(
+                success("Successfully dumped GraphQL schema to {}".format(out))
+            )
 
     def handle(self, *args, **options):
         options_schema = options.get("schema")
diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py
index 1365b5c..592f8b3 100644
--- a/graphene_django/rest_framework/mutation.py
+++ b/graphene_django/rest_framework/mutation.py
@@ -30,12 +30,13 @@ def fields_for_serializer(
     fields = OrderedDict()
     for name, field in serializer.fields.items():
         is_not_in_only = only_fields and name not in only_fields
-        is_excluded = (
-            name
-            in exclude_fields  # or
-            # name in already_created_fields
-        ) or (
-            field.write_only and not is_input  # don't show write_only fields in Query
+        is_excluded = any(
+            [
+                name in exclude_fields,
+                field.write_only
+                and not is_input,  # don't show write_only fields in Query
+                field.read_only and is_input,  # don't show read_only fields in Input
+            ]
         )
 
         if is_not_in_only or is_excluded:
diff --git a/graphene_django/settings.py b/graphene_django/settings.py
index 9a5e8a9..666ad8a 100644
--- a/graphene_django/settings.py
+++ b/graphene_django/settings.py
@@ -36,6 +36,9 @@ DEFAULTS = {
     # Max items returned in ConnectionFields / FilterConnectionFields
     "RELAY_CONNECTION_MAX_LIMIT": 100,
     "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:
diff --git a/graphene_django/types.py b/graphene_django/types.py
index 922790c..0c0cb1c 100644
--- a/graphene_django/types.py
+++ b/graphene_django/types.py
@@ -102,7 +102,7 @@ def validate_fields(type_, model, fields, only_fields, exclude_fields):
             # Field is a custom field
             warnings.warn(
                 (
-                    'Excluding the custom field "{field_name} on DjangoObjectType "{type_}" has no effect. '
+                    'Excluding the custom field "{field_name}" on DjangoObjectType "{type_}" has no effect. '
                     'Either remove the custom field or remove the field from the "exclude" list.'
                 ).format(
                     field_name=name,
diff --git a/graphene_django/views.py b/graphene_django/views.py
index d2c8324..4c58839 100644
--- a/graphene_django/views.py
+++ b/graphene_django/views.py
@@ -274,10 +274,10 @@ class GraphQLView(View):
                 extra_options["executor"] = self.executor
 
             return document.execute(
-                root=self.get_root_value(request),
-                variables=variables,
+                root_value=self.get_root_value(request),
+                variable_values=variables,
                 operation_name=operation_name,
-                context=self.get_context(request),
+                context_value=self.get_context(request),
                 middleware=self.get_middleware(request),
                 **extra_options
             )


Test patch:
diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py
index de366ba..a0f7d96 100644
--- a/graphene_django/filter/tests/test_fields.py
+++ b/graphene_django/filter/tests/test_fields.py
@@ -180,7 +180,7 @@ def test_filter_shortcut_filterset_context():
     }
     """
     schema = Schema(query=Query)
-    result = schema.execute(query, context=context())
+    result = schema.execute(query, context_value=context())
     assert not result.errors
 
     assert len(result.data["contextArticles"]["edges"]) == 1
diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py
index 494c77c..f648330 100644
--- a/graphene_django/forms/tests/test_mutation.py
+++ b/graphene_django/forms/tests/test_mutation.py
@@ -1,5 +1,6 @@
 from django import forms
 from django.test import TestCase
+from django.core.exceptions import ValidationError
 from py.test import raises
 
 from graphene import ObjectType, Schema, String, Field
@@ -13,6 +14,15 @@ from ..mutation import DjangoFormMutation, DjangoModelFormMutation
 class MyForm(forms.Form):
     text = forms.CharField()
 
+    def clean_text(self):
+        text = self.cleaned_data["text"]
+        if text == "INVALID_INPUT":
+            raise ValidationError("Invalid input")
+        return text
+
+    def save(self):
+        pass
+
 
 class PetForm(forms.ModelForm):
     class Meta:
@@ -83,6 +93,64 @@ class MockQuery(ObjectType):
     a = String()
 
 
+class FormMutationTests(TestCase):
+    def test_form_invalid_form(self):
+        class MyMutation(DjangoFormMutation):
+            class Meta:
+                form_class = MyForm
+
+        class Mutation(ObjectType):
+            my_mutation = MyMutation.Field()
+
+        schema = Schema(query=MockQuery, mutation=Mutation)
+
+        result = schema.execute(
+            """ mutation MyMutation {
+                myMutation(input: { text: "INVALID_INPUT" }) {
+                    errors {
+                        field
+                        messages
+                    }
+                    text
+                }
+            }
+            """
+        )
+
+        self.assertIs(result.errors, None)
+        self.assertEqual(
+            result.data["myMutation"]["errors"],
+            [{"field": "text", "messages": ["Invalid input"]}],
+        )
+
+    def test_form_valid_input(self):
+        class MyMutation(DjangoFormMutation):
+            class Meta:
+                form_class = MyForm
+
+        class Mutation(ObjectType):
+            my_mutation = MyMutation.Field()
+
+        schema = Schema(query=MockQuery, mutation=Mutation)
+
+        result = schema.execute(
+            """ mutation MyMutation {
+                myMutation(input: { text: "VALID_INPUT" }) {
+                    errors {
+                        field
+                        messages
+                    }
+                    text
+                }
+            }
+            """
+        )
+
+        self.assertIs(result.errors, None)
+        self.assertEqual(result.data["myMutation"]["errors"], [])
+        self.assertEqual(result.data["myMutation"]["text"], "VALID_INPUT")
+
+
 class ModelFormMutationTests(TestCase):
     def test_default_meta_fields(self):
         class PetMutation(DjangoModelFormMutation):
@@ -161,7 +229,7 @@ class ModelFormMutationTests(TestCase):
                 }
             }
             """,
-            variables={"pk": pet.pk},
+            variable_values={"pk": pet.pk},
         )
 
         self.assertIs(result.errors, None)
diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py
index 32f1e28..5bf3bc1 100644
--- a/graphene_django/rest_framework/tests/test_mutation.py
+++ b/graphene_django/rest_framework/tests/test_mutation.py
@@ -144,6 +144,25 @@ def test_write_only_field_using_extra_kwargs():
     ), "'password' is write_only field and shouldn't be visible"
 
 
+@mark.django_db
+def test_read_only_fields():
+    class ReadOnlyFieldModelSerializer(serializers.ModelSerializer):
+        cool_name = serializers.CharField(read_only=True)
+
+        class Meta:
+            model = MyFakeModelWithPassword
+            fields = ["cool_name", "password"]
+
+    class MyMutation(SerializerMutation):
+        class Meta:
+            serializer_class = ReadOnlyFieldModelSerializer
+
+    assert "password" in MyMutation.Input._meta.fields
+    assert (
+        "cool_name" not in MyMutation.Input._meta.fields
+    ), "'cool_name' is read_only field and shouldn't be on arguments"
+
+
 def test_nested_model():
     class MyFakeModelGrapheneType(DjangoObjectType):
         class Meta:
diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py
index dbabafa..8b0a8e6 100644
--- a/graphene_django/tests/test_command.py
+++ b/graphene_django/tests/test_command.py
@@ -1,17 +1,21 @@
+from textwrap import dedent
+
 from django.core import management
-from mock import patch, mock_open
+from mock import mock_open, patch
 from six import StringIO
 
+from graphene import ObjectType, Schema, String
+
 
-@patch("graphene_django.management.commands.graphql_schema.Command.save_file")
-def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
+@patch("graphene_django.management.commands.graphql_schema.Command.save_json_file")
+def test_generate_json_file_on_call_graphql_schema(savefile_mock, settings):
     out = StringIO()
     management.call_command("graphql_schema", schema="", stdout=out)
     assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
 
 
 @patch("json.dump")
-def test_files_are_canonical(dump_mock):
+def test_json_files_are_canonical(dump_mock):
     open_mock = mock_open()
     with patch("graphene_django.management.commands.graphql_schema.open", open_mock):
         management.call_command("graphql_schema", schema="")
@@ -25,3 +29,34 @@ def test_files_are_canonical(dump_mock):
     assert (
         dump_mock.call_args[1]["indent"] > 0
     ), "output should be pretty-printed by default"
+
+
+def test_generate_graphql_file_on_call_graphql_schema():
+    class Query(ObjectType):
+        hi = String()
+
+    mock_schema = Schema(query=Query)
+
+    open_mock = mock_open()
+    with patch("graphene_django.management.commands.graphql_schema.open", open_mock):
+        management.call_command(
+            "graphql_schema", schema=mock_schema, out="schema.graphql"
+        )
+
+    open_mock.assert_called_once()
+
+    handle = open_mock()
+    assert handle.write.called_once()
+
+    schema_output = handle.write.call_args[0][0]
+    assert schema_output == dedent(
+        """\
+        schema {
+          query: Query
+        }
+
+        type Query {
+          hi: String
+        }
+    """
+    )
diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py
index 3790c4a..7f84de3 100644
--- a/graphene_django/tests/test_converter.py
+++ b/graphene_django/tests/test_converter.py
@@ -1,4 +1,5 @@
 import pytest
+from collections import namedtuple
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 from graphene import NonNull
@@ -10,9 +11,14 @@ from graphene.types.datetime import DateTime, Date, Time
 from graphene.types.json import JSONString
 
 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 ..types import DjangoObjectType
+from ..settings import graphene_settings
 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.of_type, graphene.List)
     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
diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py
index f6abf00..8ea1901 100644
--- a/graphene_django/tests/test_fields.py
+++ b/graphene_django/tests/test_fields.py
@@ -19,6 +19,12 @@ class TestDjangoListField:
         with pytest.raises(AssertionError):
             list_field = DjangoListField(TestType)
 
+    def test_only_import_paths(self):
+        list_field = DjangoListField("graphene_django.tests.schema.Human")
+        from .schema import Human
+
+        assert list_field._type.of_type.of_type is Human
+
     def test_non_null_type(self):
         class Reporter(DjangoObjectType):
             class Meta:
diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py
index c32f46c..888521f 100644
--- a/graphene_django/tests/test_types.py
+++ b/graphene_django/tests/test_types.py
@@ -9,7 +9,9 @@ from graphene import Connection, Field, Interface, ObjectType, Schema, String
 from graphene.relay import Node
 
 from .. import registry
+from ..settings import graphene_settings
 from ..types import DjangoObjectType, DjangoObjectTypeOptions
+from ..converter import convert_choice_field_to_enum
 from .models import Article as ArticleModel
 from .models import Reporter as ReporterModel
 
@@ -386,6 +388,10 @@ def test_django_objecttype_exclude_fields_exist_on_model():
     assert len(record) == 0
 
 
+def custom_enum_name(field):
+    return "CustomEnum{}".format(field.name.title())
+
+
 class TestDjangoObjectType:
     @pytest.fixture
     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