Repository: graphql-python/graphene-django

Issue:
Different types with the same name in the schema - Django fields with choices
Hi,

When you try to create a model with choices field and then create a second model which inherit from the previous one, you will receive error while schema generating.

Here you have a simple [project](https://github.com/KaRi94/graphene-enum-bug-example) with models and basic configuration. Try to generate schema and you will get following error:
```
AssertionError: Found different types with the same name in the schema: PageType, PageType.
```

It seems that it is unable to inherit models with choices fields.

Discussion:
jkimbo: @zbyte64 good point about using module loading rather than direct references. I'll update the PR.

Also I don't understand the rest of your comment? Are you suggesting changes to the documentation or just re-iterating what the purpose of the change is?
zbyte64: Was just re-iterating / thinking out loud. Updating the settings to allow a string specified import was my only recommend action.
jkimbo: @zbyte64 PR is ready for another review

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/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/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/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_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_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