diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 270b24e..90085c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 4 matrix: - django: ["2.2", "3.0"] + django: ["2.2", "3.0", "3.1"] python-version: ["3.6", "3.7", "3.8"] steps: diff --git a/docs/settings.rst b/docs/settings.rst index 8ad2d3f..ff1c05e 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -186,3 +186,24 @@ Default: ``None`` GRAPHENE = { 'SUBSCRIPTION_PATH': "/ws/graphql" } + + +``GRAPHIQL_HEADER_EDITOR_ENABLED`` +--------------------- + +GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables. + +Set to ``False`` if you want to disable GraphiQL headers editor tab for some reason. + +This setting is passed to ``headerEditorEnabled`` GraphiQL options, for details refer to GraphiQLDocs_. + +.. _GraphiQLDocs: https://github.com/graphql/graphiql/tree/main/packages/graphiql#options + + +Default: ``True`` + +.. code:: python + + GRAPHENE = { + 'GRAPHIQL_HEADER_EDITOR_ENABLED': True, + } diff --git a/graphene_django/compat.py b/graphene_django/compat.py index 59fab30..8a2b933 100644 --- a/graphene_django/compat.py +++ b/graphene_django/compat.py @@ -8,8 +8,14 @@ try: from django.contrib.postgres.fields import ( ArrayField, HStoreField, - JSONField, + JSONField as PGJSONField, RangeField, ) except ImportError: - ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4 + ArrayField, HStoreField, PGJSONField, RangeField = (MissingType,) * 4 + +try: + # JSONField is only available from Django 3.1 + from django.db.models import JSONField +except ImportError: + JSONField = MissingType diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 860887f..330660c 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -26,9 +26,10 @@ from graphene.utils.str_converters import to_camel_case from graphql import GraphQLError, assert_valid_name from graphql.pyutils import register_description -from .compat import ArrayField, HStoreField, JSONField, RangeField -from .fields import DjangoConnectionField, DjangoListField +from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField +from .fields import DjangoListField, DjangoConnectionField from .settings import graphene_settings +from .utils import import_single_dispatch from .utils.str_converters import to_const @@ -296,8 +297,9 @@ def convert_postgres_array_to_list(field, registry=None): @convert_django_field.register(HStoreField) +@convert_django_field.register(PGJSONField) @convert_django_field.register(JSONField) -def convert_postgres_field_to_string(field, registry=None): +def convert_pg_and_json_field_to_string(field, registry=None): return JSONString( description=get_django_field_description(field), required=not field.null ) diff --git a/graphene_django/settings.py b/graphene_django/settings.py index df29ab4..296879d 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -40,6 +40,10 @@ DEFAULTS = { "DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None, # Use a separate path for handling subscriptions. "SUBSCRIPTION_PATH": None, + # By default GraphiQL headers editor tab is enabled, set to False to hide it + # This sets headerEditorEnabled GraphiQL option, for details go to + # https://github.com/graphql/graphiql/tree/main/packages/graphiql#options + "GRAPHIQL_HEADER_EDITOR_ENABLED": True, } if settings.DEBUG: diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index 45f8ad7..8c3b5ce 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -61,13 +61,15 @@ var fetchURL = locationQuery(otherParams); // Defines a GraphQL fetcher using the fetch API. - function httpClient(graphQLParams) { - var headers = { - Accept: "application/json", - "Content-Type": "application/json", - }; + function httpClient(graphQLParams, opts) { + if (typeof opts === 'undefined') { + opts = {}; + } + var headers = opts.headers || {}; + headers['Accept'] = headers['Accept'] || 'application/json'; + headers['Content-Type'] = headers['Content-Type'] || 'application/json'; if (csrftoken) { - headers["X-CSRFToken"] = csrftoken; + headers['X-CSRFToken'] = csrftoken } return fetch(fetchURL, { method: "post", @@ -108,7 +110,7 @@ var activeSubscription = null; // Define a GraphQL fetcher that can intelligently route queries based on the operation type. - function graphQLFetcher(graphQLParams) { + function graphQLFetcher(graphQLParams, opts) { var operationType = getOperationType(graphQLParams); // If we're about to execute a new operation, and we have an active subscription, @@ -126,7 +128,7 @@ }, }; } else { - return httpClient(graphQLParams); + return httpClient(graphQLParams, opts); } } @@ -173,6 +175,7 @@ onEditQuery: onEditQuery, onEditVariables: onEditVariables, onEditOperationName: onEditOperationName, + headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, query: parameters.query, }; if (parameters.variables) { diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index abc4b52..cec4893 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -45,6 +45,7 @@ add "&raw" to the end of the URL within a browser. {% if subscription_path %} subscriptionPath: "{{subscription_path}}", {% endif %} + graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }}, }; diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 501a4f8..8c61167 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -11,7 +11,14 @@ from graphene.relay import ConnectionField, Node from graphene.types.datetime import Date, DateTime, Time from graphene.types.json import JSONString -from ..compat import ArrayField, HStoreField, JSONField, MissingType, RangeField +from ..compat import ( + ArrayField, + HStoreField, + JSONField, + PGJSONField, + MissingType, + RangeField, +) from ..converter import ( convert_django_field, convert_django_field_with_choices, @@ -352,8 +359,13 @@ def test_should_postgres_hstore_convert_string(): assert_conversion(HStoreField, JSONString) -@pytest.mark.skipif(JSONField is MissingType, reason="JSONField should exist") +@pytest.mark.skipif(PGJSONField is MissingType, reason="PGJSONField should exist") def test_should_postgres_json_convert_string(): + assert_conversion(PGJSONField, JSONString) + + +@pytest.mark.skipif(JSONField is MissingType, reason="JSONField should exist") +def test_should_json_convert_string(): assert_conversion(JSONField, JSONString) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 51dabbd..e970588 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -9,6 +9,7 @@ from graphene import Connection, Field, Interface, ObjectType, Schema, String from graphene.relay import Node from .. import registry +from ..filter import DjangoFilterConnectionField from ..types import DjangoObjectType, DjangoObjectTypeOptions from .models import Article as ArticleModel from .models import Reporter as ReporterModel @@ -662,3 +663,28 @@ class TestDjangoObjectType: } """ ) + + +@with_local_registry +def test_django_objecttype_name_connection_propagation(): + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + name = "CustomReporterName" + filter_fields = ["email"] + interfaces = (Node,) + + class Query(ObjectType): + reporter = Node.Field(Reporter) + reporters = DjangoFilterConnectionField(Reporter) + + assert Reporter._meta.name == "CustomReporterName" + schema = str(Schema(query=Query)) + + assert "type CustomReporterName implements Node {" in schema + assert "type CustomReporterNameConnection {" in schema + assert "type CustomReporterNameEdge {" in schema + + assert "type Reporter implements Node {" not in schema + assert "type ReporterConnection {" not in schema + assert "type ReporterEdge {" not in schema diff --git a/graphene_django/types.py b/graphene_django/types.py index 7ad6f1f..53c4d23 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -246,7 +246,7 @@ class DjangoObjectType(ObjectType): connection_class = Connection connection = connection_class.create_type( - "{}Connection".format(cls.__name__), node=cls + "{}Connection".format(options.get("name") or cls.__name__), node=cls ) if connection is not None: diff --git a/graphene_django/views.py b/graphene_django/views.py index 48db1ec..1316214 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -154,6 +154,8 @@ class GraphQLView(View): subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri, # The SUBSCRIPTION_PATH setting. subscription_path=self.subscription_path, + # GraphiQL headers tab, + graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED, ) if self.batch: diff --git a/tox.ini b/tox.ini index 9402fff..3592042 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{36,37,38}-django{22,30,master}, + py{36,37,38}-django{22,30,31,master}, black,flake8 [gh-actions] @@ -13,6 +13,7 @@ python = DJANGO = 2.2: django22 3.0: django30 + 3.1: django31 master: djangomaster [testenv] @@ -28,6 +29,7 @@ deps = django21: Django>=2.1,<2.2 django22: Django>=2.2,<3.0 django30: Django>=3.0a1,<3.1 + django31: Django>=3.1,<3.2 djangomaster: https://github.com/django/django/archive/master.zip commands = {posargs:py.test --cov=graphene_django graphene_django examples}