From 1d814c54c486575ac3f2674dcbac5b7dfdea4862 Mon Sep 17 00:00:00 2001 From: Bendik Eger Date: Wed, 30 Nov 2022 13:34:09 +0100 Subject: [PATCH 01/15] Fix schema print with `-.graphql` --- graphene_django/management/commands/graphql_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index bedddba..42c41c1 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -63,7 +63,7 @@ class Command(CommandArguments): if out == "-" or out == "-.json": self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) elif out == "-.graphql": - self.stdout.write(print_schema(schema)) + self.stdout.write(print_schema(schema.graphql_schema)) else: # Determine format _, file_extension = os.path.splitext(out) From 3283d0b1be37c32e4592991e76293987c5ac7bcd Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Fri, 14 Apr 2023 22:34:17 +0800 Subject: [PATCH 02/15] Update GraphiQL to 2.4.1 --- .../static/graphene_django/graphiql.js | 107 ++++-------------- .../templates/graphene/graphiql.html | 2 +- graphene_django/views.py | 10 +- 3 files changed, 29 insertions(+), 90 deletions(-) diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index f6be32c..106b470 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -5,7 +5,7 @@ GraphiQL, React, ReactDOM, - SubscriptionsTransportWs, + graphqlWs, fetch, history, location, @@ -52,8 +52,24 @@ var fetchURL = locationQuery(otherParams); - // Defines a GraphQL fetcher using the fetch API. - function httpClient(graphQLParams, opts) { + // Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise + // assumes the current window location with an appropriate websocket protocol. + var subscribeURL = + location.origin.replace(/^http/, "ws") + + (GRAPHENE_SETTINGS.subscriptionPath || location.pathname); + + function trueLambda() { return true; }; + + var fetcher = GraphiQL.createFetcher({ + url: fetchURL, + wsClient: graphqlWs.createClient({ + url: subscribeURL, + shouldRetry: trueLambda, + lazy: true, + }) + }) + + function graphQLFetcher(graphQLParams, opts) { if (typeof opts === 'undefined') { opts = {}; } @@ -73,86 +89,9 @@ headers['X-CSRFToken'] = csrftoken } - return fetch(fetchURL, { - method: "post", - headers: headers, - body: JSON.stringify(graphQLParams), - credentials: "include", - }) - .then(function (response) { - return response.text(); - }) - .then(function (responseBody) { - try { - return JSON.parse(responseBody); - } catch (error) { - return responseBody; - } - }); - } + opts.headers = headers - // Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise - // assumes the current window location with an appropriate websocket protocol. - var subscribeURL = - location.origin.replace(/^http/, "ws") + - (GRAPHENE_SETTINGS.subscriptionPath || location.pathname); - - // Create a subscription client. - var subscriptionClient = new SubscriptionsTransportWs.SubscriptionClient( - subscribeURL, - { - // Reconnect after any interruptions. - reconnect: true, - // Delay socket initialization until the first subscription is started. - lazy: true, - }, - ); - - // Keep a reference to the currently-active subscription, if available. - var activeSubscription = null; - - // Define a GraphQL fetcher that can intelligently route queries based on the operation type. - function graphQLFetcher(graphQLParams, opts) { - var operationType = getOperationType(graphQLParams); - - // If we're about to execute a new operation, and we have an active subscription, - // unsubscribe before continuing. - if (activeSubscription) { - activeSubscription.unsubscribe(); - activeSubscription = null; - } - - if (operationType === "subscription") { - return { - subscribe: function (observer) { - activeSubscription = subscriptionClient; - return subscriptionClient.request(graphQLParams, opts).subscribe(observer); - }, - }; - } else { - return httpClient(graphQLParams, opts); - } - } - - // Determine the type of operation being executed for a given set of GraphQL parameters. - function getOperationType(graphQLParams) { - // Run a regex against the query to determine the operation type (query, mutation, subscription). - var operationRegex = new RegExp( - // Look for lines that start with an operation keyword, ignoring whitespace. - "^\\s*(query|mutation|subscription)\\s*" + - // The operation keyword should be followed by whitespace and the operationName in the GraphQL parameters (if available). - (graphQLParams.operationName ? ("\\s+" + graphQLParams.operationName) : "") + - // The line should eventually encounter an opening curly brace. - "[^\\{]*\\{", - // Enable multiline matching. - "m", - ); - var match = operationRegex.exec(graphQLParams.query); - if (!match) { - return "query"; - } - - return match[1]; + return fetcher(graphQLParams, opts) } // When the query and variables string is edited, update the URL bar so @@ -177,7 +116,7 @@ onEditQuery: onEditQuery, onEditVariables: onEditVariables, onEditOperationName: onEditOperationName, - headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, + isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, query: parameters.query, }; @@ -199,7 +138,7 @@ window.GraphiQL, window.React, window.ReactDOM, - window.SubscriptionsTransportWs, + window.graphqlWs, window.fetch, window.history, window.location, diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 3685692..8fb00c4 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -33,7 +33,7 @@ add "&raw" to the end of the URL within a browser. - diff --git a/graphene_django/views.py b/graphene_django/views.py index 4d68b13..b29aeed 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -66,14 +66,14 @@ class GraphQLView(View): react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=" # The GraphiQL React app. - graphiql_version = "1.4.7" # "1.0.3" - graphiql_sri = "sha256-cpZ8w9D/i6XdEbY/Eu7yAXeYzReVw0mxYd7OU3gUcsc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk=" - graphiql_css_sri = "sha256-HADQowUuFum02+Ckkv5Yu5ygRoLllHZqg0TFZXY7NHI=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E=" + graphiql_version = "2.4.1" # "1.0.3" + graphiql_sri = "sha256-s+f7CFAPSUIygFnRC2nfoiEKd3liCUy+snSdYFAoLUc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk=" + graphiql_css_sri = "sha256-88yn8FJMyGboGs4Bj+Pbb3kWOWXo7jmb+XCRHE+282k=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E=" # The websocket transport library for subscriptions. - subscriptions_transport_ws_version = "0.9.18" + subscriptions_transport_ws_version = "5.12.1" subscriptions_transport_ws_sri = ( - "sha256-i0hAXd4PdJ/cHX3/8tIy/Q/qKiWr5WSTxMFuL9tACkw=" + "sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw=" ) schema = None From 7e1a1d1fb8f34da68e8d0111ec3230521735b936 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Fri, 21 Apr 2023 21:40:51 +0300 Subject: [PATCH 03/15] Update django-filter url --- docs/filtering.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index fb686a1..d850b69 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -2,7 +2,7 @@ Filtering ========= Graphene integrates with -`django-filter `__ to provide filtering of results. +`django-filter `__ to provide filtering of results. See the `usage documentation `__ for details on the format for ``filter_fields``. From df3c0bf75b3130825e9644bfd2325059d1191cf6 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Fri, 21 Apr 2023 21:42:52 +0300 Subject: [PATCH 04/15] Update filtering.rst --- docs/filtering.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index d850b69..95576a0 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -3,7 +3,7 @@ Filtering Graphene integrates with `django-filter `__ to provide filtering of results. -See the `usage documentation `__ +See the `usage documentation `__ for details on the format for ``filter_fields``. This filtering is automatically available when implementing a ``relay.Node``. From a335042dbe5c9def356eee9e20cb1dad41da109c Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Sat, 29 Apr 2023 20:26:05 +0300 Subject: [PATCH 05/15] =?UTF-8?q?=E2=98=82=EF=B8=8F=20v3.0.1=20=E2=98=82?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphene_django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index 7a413fc..755ed87 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,7 +1,7 @@ from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType -__version__ = "3.0.0" +__version__ = "3.1.0" __all__ = [ "__version__", From 34cc86063b4d01212d30c6244ae58cde78b6a139 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Sat, 29 Apr 2023 20:26:39 +0300 Subject: [PATCH 06/15] =?UTF-8?q?=E2=98=82=EF=B8=8F=20v3.0.1=20=E2=98=82?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphene_django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index 755ed87..82c4fb3 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,7 +1,7 @@ from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType -__version__ = "3.1.0" +__version__ = "3.0.1" __all__ = [ "__version__", From f67c5dbc8cfa6783247ed021b78dbda182614c00 Mon Sep 17 00:00:00 2001 From: Steven DeMartini Date: Sat, 29 Apr 2023 12:01:55 -0700 Subject: [PATCH 07/15] Revert field resolver logic to fix poor query performance This reverts the change to `convert_field_to_djangomodel` introduced in https://github.com/graphql-python/graphene-django/pull/1315 for the reasons discussed here https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857. As mentioned there, without reverting this code, "queries are forced every time an object is resolved, making an exponential number of queries when nesting without any possibility of optimizing". That regression prevented `graphene-django-optimizer` from working with `graphene-django` v3.0.0b9+ (where this change first was published), as discussed in https://github.com/graphql-python/graphene-django/issues/1356#issuecomment-1284718187, https://github.com/tfoxy/graphene-django-optimizer/issues/86, and https://github.com/tfoxy/graphene-django-optimizer/pull/83#issuecomment-1451987397. For now, this marks the two tests that depended on this problematic code as "expected to fail", and perhaps they can be reintroduced if there's a way to support this logic in a way that does not prevent `select_related` and `prefetch_related` query-optimization and introduce nested N+1s. As mentioned here https://github.com/graphql-python/graphene-django/pull/1315#issuecomment-1468594361, this is blocking upgrade to graphene-django v3 for many users, and fixing this would allow many to begin upgrading and contributing to keep graphene-django going. --- graphene_django/converter.py | 21 +-------------------- graphene_django/tests/test_get_queryset.py | 8 ++++++++ 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 386103a..9ad6c9d 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -315,26 +315,7 @@ def convert_field_to_djangomodel(field, registry=None): if not _type: return - class CustomField(Field): - def wrap_resolve(self, parent_resolver): - """ - Implements a custom resolver which go through the `get_node` method to ensure that - it goes through the `get_queryset` method of the DjangoObjectType. - """ - resolver = super().wrap_resolve(parent_resolver) - - def custom_resolver(root, info, **args): - fk_obj = resolver(root, info, **args) - if not isinstance(fk_obj, model): - # In case the resolver is a custom one that overwrites - # the default Django resolver - # This happens, for example, when using custom awaitable resolvers. - return fk_obj - return _type.get_node(info, fk_obj.pk) - - return custom_resolver - - return CustomField( + return Field( _type, description=get_django_field_description(field), required=not field.null, diff --git a/graphene_django/tests/test_get_queryset.py b/graphene_django/tests/test_get_queryset.py index 91bdc70..63027b9 100644 --- a/graphene_django/tests/test_get_queryset.py +++ b/graphene_django/tests/test_get_queryset.py @@ -121,6 +121,10 @@ class TestShouldCallGetQuerySetOnForeignKey: assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} + # TODO: This test is currently expected to fail because the logic it depended on has been + # removed, due to poor SQL performance and preventing query-optimization (see + # https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857) + @pytest.mark.xfail def test_get_queryset_called_on_foreignkey(self): # If a user tries to access a reporter through an article they should get our authorization error query = """ @@ -291,6 +295,10 @@ class TestShouldCallGetQuerySetOnForeignKeyNode: assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} + # TODO: This test is currently expected to fail because the logic it depended on has been + # removed, due to poor SQL performance and preventing query-optimization (see + # https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857) + @pytest.mark.xfail def test_get_queryset_called_on_foreignkey(self): # If a user tries to access a reporter through an article they should get our authorization error query = """ From 9796e93fc7d9cd1d440408f59857ebcca2f0f344 Mon Sep 17 00:00:00 2001 From: Steven DeMartini Date: Mon, 1 May 2023 09:00:30 -0700 Subject: [PATCH 08/15] Remove obsolete tests and add note about rationale --- graphene_django/tests/test_get_queryset.py | 146 +-------------------- 1 file changed, 6 insertions(+), 140 deletions(-) diff --git a/graphene_django/tests/test_get_queryset.py b/graphene_django/tests/test_get_queryset.py index 63027b9..7cbaa54 100644 --- a/graphene_django/tests/test_get_queryset.py +++ b/graphene_django/tests/test_get_queryset.py @@ -16,6 +16,12 @@ class TestShouldCallGetQuerySetOnForeignKey: Check that the get_queryset method is called in both forward and reversed direction of a foreignkey on types. (see issue #1111) + + NOTE: For now, we do not expect this get_queryset method to be called for nested + objects, as the original attempt to do so prevented SQL query-optimization with + `select_related`/`prefetch_related` and caused N+1 queries. See discussions here + https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857 + and here https://github.com/graphql-python/graphene-django/pull/1401. """ @pytest.fixture(autouse=True) @@ -121,73 +127,6 @@ class TestShouldCallGetQuerySetOnForeignKey: assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} - # TODO: This test is currently expected to fail because the logic it depended on has been - # removed, due to poor SQL performance and preventing query-optimization (see - # https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857) - @pytest.mark.xfail - def test_get_queryset_called_on_foreignkey(self): - # If a user tries to access a reporter through an article they should get our authorization error - query = """ - query getArticle($id: ID!) { - article(id: $id) { - headline - reporter { - firstName - } - } - } - """ - - result = self.schema.execute(query, variables={"id": self.articles[0].id}) - assert len(result.errors) == 1 - assert result.errors[0].message == "Not authorized to access reporters." - - # An admin user should be able to get reporters through an article - query = """ - query getArticle($id: ID!) { - article(id: $id) { - headline - reporter { - firstName - } - } - } - """ - - result = self.schema.execute( - query, - variables={"id": self.articles[0].id}, - context_value={"admin": True}, - ) - assert not result.errors - assert result.data["article"] == { - "headline": "A fantastic article", - "reporter": {"firstName": "Jane"}, - } - - # An admin user should not be able to access draft article through a reporter - query = """ - query getReporter($id: ID!) { - reporter(id: $id) { - firstName - articles { - headline - } - } - } - """ - - result = self.schema.execute( - query, - variables={"id": self.reporter.id}, - context_value={"admin": True}, - ) - assert not result.errors - assert result.data["reporter"] == { - "firstName": "Jane", - "articles": [{"headline": "A fantastic article"}], - } - class TestShouldCallGetQuerySetOnForeignKeyNode: """ @@ -294,76 +233,3 @@ class TestShouldCallGetQuerySetOnForeignKeyNode: ) assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} - - # TODO: This test is currently expected to fail because the logic it depended on has been - # removed, due to poor SQL performance and preventing query-optimization (see - # https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857) - @pytest.mark.xfail - def test_get_queryset_called_on_foreignkey(self): - # If a user tries to access a reporter through an article they should get our authorization error - query = """ - query getArticle($id: ID!) { - article(id: $id) { - headline - reporter { - firstName - } - } - } - """ - - result = self.schema.execute( - query, variables={"id": to_global_id("ArticleType", self.articles[0].id)} - ) - assert len(result.errors) == 1 - assert result.errors[0].message == "Not authorized to access reporters." - - # An admin user should be able to get reporters through an article - query = """ - query getArticle($id: ID!) { - article(id: $id) { - headline - reporter { - firstName - } - } - } - """ - - result = self.schema.execute( - query, - variables={"id": to_global_id("ArticleType", self.articles[0].id)}, - context_value={"admin": True}, - ) - assert not result.errors - assert result.data["article"] == { - "headline": "A fantastic article", - "reporter": {"firstName": "Jane"}, - } - - # An admin user should not be able to access draft article through a reporter - query = """ - query getReporter($id: ID!) { - reporter(id: $id) { - firstName - articles { - edges { - node { - headline - } - } - } - } - } - """ - - result = self.schema.execute( - query, - variables={"id": to_global_id("ReporterType", self.reporter.id)}, - context_value={"admin": True}, - ) - assert not result.errors - assert result.data["reporter"] == { - "firstName": "Jane", - "articles": {"edges": [{"node": {"headline": "A fantastic article"}}]}, - } From 20a6cecc4ca77b3866073ef66bb5ef63b4beab05 Mon Sep 17 00:00:00 2001 From: Steven DeMartini Date: Tue, 2 May 2023 09:00:22 -0700 Subject: [PATCH 09/15] Add test validating query performance with select_related + prefetch_related This test passes after reverting the `CustomField` resolver change introduced in https://github.com/graphql-python/graphene-django/pull/1315, but fails with that resolver code present. For instance, adding back the resolver code gives a test failure showing: ``` Failed: Expected to perform 2 queries but 11 were done ``` This should ensure there aren't regressions that prevent query-optimization in the future. --- graphene_django/tests/test_fields.py | 153 ++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 835de78..8c7b78d 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,5 +1,6 @@ import datetime -from django.db.models import Count +import re +from django.db.models import Count, Prefetch import pytest @@ -7,8 +8,12 @@ from graphene import List, NonNull, ObjectType, Schema, String from ..fields import DjangoListField from ..types import DjangoObjectType -from .models import Article as ArticleModel -from .models import Reporter as ReporterModel +from .models import ( + Article as ArticleModel, + Film as FilmModel, + FilmDetails as FilmDetailsModel, + Reporter as ReporterModel, +) class TestDjangoListField: @@ -500,3 +505,145 @@ class TestDjangoListField: assert not result.errors assert result.data == {"reporters": [{"firstName": "Tara"}]} + + def test_select_related_and_prefetch_related_are_respected( + self, django_assert_num_queries + ): + class Article(DjangoObjectType): + class Meta: + model = ArticleModel + fields = ("headline", "editor", "reporter") + + class Film(DjangoObjectType): + class Meta: + model = FilmModel + fields = ("genre", "details") + + class FilmDetail(DjangoObjectType): + class Meta: + model = FilmDetailsModel + fields = ("location",) + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + fields = ("first_name", "articles", "films") + + class Query(ObjectType): + articles = DjangoListField(Article) + + @staticmethod + def resolve_articles(root, info): + # Optimize for querying associated editors and reporters, and the films and film + # details of those reporters. This is similar to what would happen using a library + # like https://github.com/tfoxy/graphene-django-optimizer for a query like the one + # below (albeit simplified and hardcoded here). + return ArticleModel.objects.select_related( + "editor", "reporter" + ).prefetch_related( + Prefetch( + "reporter__films", + queryset=FilmModel.objects.select_related("details"), + ), + ) + + schema = Schema(query=Query) + + query = """ + query { + articles { + headline + + editor { + firstName + } + + reporter { + firstName + + films { + genre + + details { + location + } + } + } + } + } + """ + + r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") + r2 = ReporterModel.objects.create(first_name="Debra", last_name="Payne") + + ArticleModel.objects.create( + headline="Amazing news", + reporter=r1, + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + editor=r2, + ) + ArticleModel.objects.create( + headline="Not so good news", + reporter=r2, + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + editor=r1, + ) + + film1 = FilmModel.objects.create(genre="ac") + film2 = FilmModel.objects.create(genre="ot") + film3 = FilmModel.objects.create(genre="do") + FilmDetailsModel.objects.create(location="Hollywood", film=film1) + FilmDetailsModel.objects.create(location="Antarctica", film=film3) + r1.films.add(film1, film2) + r2.films.add(film3) + + # We expect 2 queries to be performed based on the above resolver definition: one for all + # articles joined with the reporters model (for associated editors and reporters), and one + # for the films prefetch (which includes its `select_related` JOIN logic in its queryset) + with django_assert_num_queries(2) as captured: + result = schema.execute(query) + + assert not result.errors + assert result.data == { + "articles": [ + { + "headline": "Amazing news", + "editor": {"firstName": "Debra"}, + "reporter": { + "firstName": "Tara", + "films": [ + {"genre": "AC", "details": {"location": "Hollywood"}}, + {"genre": "OT", "details": None}, + ], + }, + }, + { + "headline": "Not so good news", + "editor": {"firstName": "Tara"}, + "reporter": { + "firstName": "Debra", + "films": [ + {"genre": "DO", "details": {"location": "Antarctica"}}, + ], + }, + }, + ] + } + + assert len(captured.captured_queries) == 2 # Sanity-check + + # First we should have queried for all articles in a single query, joining on the reporters + # model (for the editors and reporters ForeignKeys) + assert re.match( + r'SELECT .* "tests_article" INNER JOIN "tests_reporter"', + captured.captured_queries[0]["sql"], + ) + + # Then we should have queried for all of the films of all reporters, joined with the film + # details for each film, using a single query + assert re.match( + r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"', + captured.captured_queries[1]["sql"], + ) From a8ceca77ed96c46414099c7595449975a19954c8 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Wed, 3 May 2023 11:45:56 +0300 Subject: [PATCH 10/15] Bump version --- graphene_django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index 82c4fb3..12408a4 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,7 +1,7 @@ from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType -__version__ = "3.0.1" +__version__ = "3.0.2" __all__ = [ "__version__", From 95a064281869f23fac5b63f3ac11413a0bb2c483 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Wed, 3 May 2023 04:24:32 +0800 Subject: [PATCH 11/15] fix: fix graphiql request failure --- .../static/graphene_django/graphiql.js | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index 106b470..5b9d96d 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -60,40 +60,27 @@ function trueLambda() { return true; }; - var fetcher = GraphiQL.createFetcher({ + var headers = {}; + var cookies = ("; " + document.cookie).split("; csrftoken="); + if (cookies.length == 2) { + csrftoken = cookies.pop().split(";").shift(); + } else { + csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value; + } + if (csrftoken) { + headers['X-CSRFToken'] = csrftoken + } + + var graphQLFetcher = GraphiQL.createFetcher({ url: fetchURL, wsClient: graphqlWs.createClient({ url: subscribeURL, shouldRetry: trueLambda, lazy: true, - }) + }), + headers: headers }) - function graphQLFetcher(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'; - - // Parse the cookie value for a CSRF token - var csrftoken; - var cookies = ("; " + document.cookie).split("; csrftoken="); - if (cookies.length == 2) { - csrftoken = cookies.pop().split(";").shift(); - } else { - csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value; - } - if (csrftoken) { - headers['X-CSRFToken'] = csrftoken - } - - opts.headers = headers - - return fetcher(graphQLParams, opts) - } - // When the query and variables string is edited, update the URL bar so // that it can be easily shared. function onEditQuery(newQuery) { From c1a22bfd91aa3b298ea8f293b1a9f6ece7a46a8e Mon Sep 17 00:00:00 2001 From: Steven DeMartini Date: Tue, 2 May 2023 10:12:49 -0700 Subject: [PATCH 12/15] Add pre-commit to dev-setup pre-commit is currently configured nicely but hasn't been part of the Makefile setup and isn't mentioned in the contributing notes. This change makes it so that pre-commit is installed as a part of the dev setup, whereas before it had to be manually installed. --- Makefile | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 391c454..29c412b 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ help: .PHONY: dev-setup ## Install development dependencies dev-setup: pip install -e ".[dev]" + python -m pre_commit install .PHONY: tests ## Run unit tests tests: diff --git a/setup.py b/setup.py index d9aefef..cc9770c 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ dev_requires = [ "flake8==5.0.4", "flake8-black==0.3.3", "flake8-bugbear==22.9.11", + "pre-commit", ] + tests_require setup( From af8888f58e1004000ba90adfd42231546c08ccd2 Mon Sep 17 00:00:00 2001 From: Firas K <3097061+firaskafri@users.noreply.github.com> Date: Wed, 3 May 2023 13:25:16 +0300 Subject: [PATCH 13/15] Upgrade github actions versions, default python and dev dependencies (#1407) * Use Python 3.10 for deployments on PyPi * Update gh-action-pypi-publish version * Update python version * Update checkout and setup-python versions * Upgrade dev dependencies * fromat examples and few files to follow black new version * Upgrade pytest version --------- Co-authored-by: Firas Kafri --- .github/workflows/deploy.yml | 10 +++++----- .github/workflows/lint.yml | 8 ++++---- .github/workflows/tests.yml | 4 ++-- .pre-commit-config.yaml | 10 +++++----- .../cookbook/ingredients/migrations/0001_initial.py | 1 - .../ingredients/migrations/0002_auto_20161104_0050.py | 1 - .../ingredients/migrations/0003_auto_20181018_1746.py | 1 - .../cookbook/recipes/migrations/0001_initial.py | 1 - .../recipes/migrations/0002_auto_20161104_0106.py | 1 - .../recipes/migrations/0003_auto_20181018_1728.py | 1 - .../cookbook/ingredients/migrations/0001_initial.py | 1 - .../ingredients/migrations/0002_auto_20161104_0050.py | 1 - .../cookbook/recipes/migrations/0001_initial.py | 1 - .../recipes/migrations/0002_auto_20161104_0106.py | 1 - graphene_django/filter/tests/conftest.py | 1 - graphene_django/forms/mutation.py | 2 -- graphene_django/rest_framework/mutation.py | 1 - graphene_django/tests/schema_view.py | 1 - graphene_django/tests/test_query.py | 2 -- setup.py | 10 +++++----- 20 files changed, 21 insertions(+), 38 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 07c0766..a733c03 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,17 +10,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - name: Build wheel and source tarball run: | pip install wheel python setup.py sdist bdist_wheel - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@v1.1.0 + uses: pypa/gh-action-pypi-publish@v1.8.6 with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9f1c3ab..8cee90a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2cdc99..31b479e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,9 +14,9 @@ jobs: - django: "3.2" python-version: "3.7" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e64c4e1..adb54c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,8 @@ default_language_version: - python: python3.9 + python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-merge-conflict - id: check-json @@ -16,15 +16,15 @@ repos: - id: trailing-whitespace exclude: README.md - repo: https://github.com/asottile/pyupgrade - rev: v3.2.0 + rev: v3.3.2 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 23.3.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py index 345cadb..23d71e8 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py index 00fe255..5f9e7a0 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("ingredients", "0001_initial"), ] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py index 8015d1f..e823a2e 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py @@ -4,7 +4,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("ingredients", "0002_auto_20161104_0050"), ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py index fceeb9b..c415147 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py index 0156920..f38bb69 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("recipes", "0001_initial"), ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py index c54855b..dacdb30 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("recipes", "0002_auto_20161104_0106"), ] diff --git a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py index 345cadb..23d71e8 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py index 00fe255..5f9e7a0 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("ingredients", "0001_initial"), ] diff --git a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py index fceeb9b..c415147 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py index 0156920..f38bb69 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("recipes", "0001_initial"), ] diff --git a/graphene_django/filter/tests/conftest.py b/graphene_django/filter/tests/conftest.py index a11831c..f8a65d7 100644 --- a/graphene_django/filter/tests/conftest.py +++ b/graphene_django/filter/tests/conftest.py @@ -87,7 +87,6 @@ def Query(EventType): events = DjangoFilterConnectionField(EventType) def resolve_events(self, info, **kwargs): - events = [ Event(name="Live Show", tags=["concert", "music", "rock"]), Event(name="Musical", tags=["movie", "music"]), diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 3d59464..40d1d3c 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -82,7 +82,6 @@ class DjangoFormMutation(BaseDjangoFormMutation): def __init_subclass_with_meta__( cls, form_class=None, only_fields=(), exclude_fields=(), **options ): - if not form_class: raise Exception("form_class is required for DjangoFormMutation") @@ -129,7 +128,6 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): exclude_fields=(), **options, ): - if not form_class: raise Exception("form_class is required for DjangoModelFormMutation") diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index c01d915..4062a44 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -72,7 +72,6 @@ class SerializerMutation(ClientIDMutation): _meta=None, **options ): - if not serializer_class: raise Exception("serializer_class is required for the SerializerMutation") diff --git a/graphene_django/tests/schema_view.py b/graphene_django/tests/schema_view.py index 8ed2ecf..4d538ba 100644 --- a/graphene_django/tests/schema_view.py +++ b/graphene_django/tests/schema_view.py @@ -5,7 +5,6 @@ from .mutations import PetFormMutation, PetMutation class QueryRoot(ObjectType): - thrower = graphene.String(required=True) request = graphene.String(required=True) test = graphene.String(who=graphene.String()) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index df339d8..383ff2e 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -780,7 +780,6 @@ def test_should_query_promise_connectionfields(): def test_should_query_connectionfields_with_last(): - r = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) @@ -818,7 +817,6 @@ def test_should_query_connectionfields_with_last(): def test_should_query_connectionfields_with_manager(): - r = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) diff --git a/setup.py b/setup.py index cc9770c..96da8ff 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ rest_framework_require = ["djangorestframework>=3.6.3"] tests_require = [ - "pytest>=7.1.3", + "pytest>=7.3.1", "pytest-cov", "pytest-random-order", "coveralls", @@ -26,10 +26,10 @@ tests_require = [ dev_requires = [ - "black==22.8.0", - "flake8==5.0.4", - "flake8-black==0.3.3", - "flake8-bugbear==22.9.11", + "black==23.3.0", + "flake8==6.0.0", + "flake8-black==0.3.6", + "flake8-bugbear==23.3.23", "pre-commit", ] + tests_require From 8540a9332cda7bcbac92643cacbb5146f35080e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Clgen=20Sar=C4=B1kavak?= Date: Thu, 4 May 2023 15:19:24 +0300 Subject: [PATCH 14/15] Add support for Python 3.11 (#1365) * Add support for Python 3.11 * Fix Python 3.11 compatibility matrix * Add temporary fix for default enum description --------- Co-authored-by: Firas Kafri --- .github/workflows/deploy.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/tests.yml | 2 ++ .pre-commit-config.yaml | 2 +- graphene_django/converter.py | 7 ++++++- setup.py | 1 + tox.ini | 2 ++ 7 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a733c03..139c6f6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,10 +11,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Build wheel and source tarball run: | pip install wheel diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8cee90a..bfafa67 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31b479e..2c5b755 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,8 @@ jobs: include: - django: "3.2" python-version: "3.7" + - django: "4.1" + python-version: "3.11" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index adb54c7..9214d35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.10 + python: python3.11 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 9ad6c9d..375d683 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -96,7 +96,12 @@ def convert_choices_to_named_enum_with_descriptions(name, choices): def description(self): return str(named_choices_descriptions[self.name]) - return_type = Enum(name, list(named_choices), type=EnumWithDescriptionsType) + return_type = Enum( + name, + list(named_choices), + type=EnumWithDescriptionsType, + description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged + ) return return_type diff --git a/setup.py b/setup.py index 96da8ff..37b57a8 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ setup( "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Django", "Framework :: Django :: 3.2", diff --git a/tox.ini b/tox.ini index 285d046..e186f30 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py{37,38,39,310}-django32, py{38,39,310}-django{40,41,main}, + py311-django{41,main} pre-commit [gh-actions] @@ -10,6 +11,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [gh-actions:env] DJANGO = From 52f992183fcdbb841930357be086b2488f9d8472 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Fri, 5 May 2023 03:06:10 +0800 Subject: [PATCH 15/15] Add GraphiQL Explorer plugin (#1397) --- .../static/graphene_django/graphiql.js | 52 +++++++++++++------ .../templates/graphene/graphiql.html | 3 ++ graphene_django/views.py | 5 ++ 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index 5b9d96d..901c991 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -6,6 +6,7 @@ React, ReactDOM, graphqlWs, + GraphiQLPluginExplorer, fetch, history, location, @@ -98,24 +99,44 @@ function updateURL() { history.replaceState(null, null, locationQuery(parameters)); } - var options = { - fetcher: graphQLFetcher, - onEditQuery: onEditQuery, - onEditVariables: onEditVariables, - onEditOperationName: onEditOperationName, - isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, - shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, - query: parameters.query, - }; - if (parameters.variables) { - options.variables = parameters.variables; - } - if (parameters.operation_name) { - options.operationName = parameters.operation_name; + + function GraphiQLWithExplorer() { + var [query, setQuery] = React.useState(parameters.query); + + function handleQuery(query) { + setQuery(query); + onEditQuery(query); + } + + var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({ + query: query, + onEdit: handleQuery, + }); + + var options = { + fetcher: graphQLFetcher, + plugins: [explorerPlugin], + defaultEditorToolsVisibility: true, + onEditQuery: handleQuery, + onEditVariables: onEditVariables, + onEditOperationName: onEditOperationName, + isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, + shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, + query: query, + }; + if (parameters.variables) { + options.variables = parameters.variables; + } + if (parameters.operation_name) { + options.operationName = parameters.operation_name; + } + + return React.createElement(GraphiQL, options); } + // Render into the body. ReactDOM.render( - React.createElement(GraphiQL, options), + React.createElement(GraphiQLWithExplorer), document.getElementById("editor"), ); })( @@ -126,6 +147,7 @@ window.React, window.ReactDOM, window.graphqlWs, + window.GraphiQLPluginExplorer, window.fetch, window.history, window.location, diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 8fb00c4..ddff8fc 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -36,6 +36,9 @@ add "&raw" to the end of the URL within a browser. +
diff --git a/graphene_django/views.py b/graphene_django/views.py index b29aeed..d4d98b7 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -76,6 +76,9 @@ class GraphQLView(View): "sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw=" ) + graphiql_plugin_explorer_version = "0.1.15" + graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE=" + schema = None graphiql = False middleware = None @@ -158,6 +161,8 @@ class GraphQLView(View): graphiql_css_sri=self.graphiql_css_sri, subscriptions_transport_ws_version=self.subscriptions_transport_ws_version, subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri, + graphiql_plugin_explorer_version=self.graphiql_plugin_explorer_version, + graphiql_plugin_explorer_sri=self.graphiql_plugin_explorer_sri, # The SUBSCRIPTION_PATH setting. subscription_path=self.subscription_path, # GraphiQL headers tab,