From 219005952a1d143fce6363be3d4d3bc8f8d0ecc5 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 19:29:33 +0100 Subject: [PATCH 01/19] Don't execute on GET for GraphiQL We can also now return GraphiQL earlier in the request handling. --- graphene_django/views.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index be7ccf9..9a530de 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -124,6 +124,12 @@ class GraphQLView(View): data = self.parse_body(request) show_graphiql = self.graphiql and self.can_display_graphiql(request, data) + if show_graphiql: + return self.render_graphiql( + request, + graphiql_version=self.graphiql_version, + ) + if self.batch: responses = [self.get_response(request, entry) for entry in data] result = "[{}]".format( @@ -137,19 +143,6 @@ class GraphQLView(View): else: result, status_code = self.get_response(request, data, show_graphiql) - if show_graphiql: - query, variables, operation_name, id = self.get_graphql_params( - request, data - ) - return self.render_graphiql( - request, - graphiql_version=self.graphiql_version, - query=query or "", - variables=json.dumps(variables) or "", - operation_name=operation_name or "", - result=result or "", - ) - return HttpResponse( status=status_code, content=result, content_type="application/json" ) From 3755850c2e5de11eb80f7d39d0f8fd31f3cc5d66 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 19:47:48 +0100 Subject: [PATCH 02/19] Use the fragment for the URL --- graphene_django/templates/graphene/graphiql.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 1ba0613..5bc5e04 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -32,7 +32,7 @@ add "&raw" to the end of the URL within a browser. // Collect the URL parameters var parameters = {}; - window.location.search.substr(1).split('&').forEach(function (entry) { + window.location.hash.substr(1).split('&').forEach(function (entry) { var eq = entry.indexOf('='); if (eq >= 0) { parameters[decodeURIComponent(entry.slice(0, eq))] = @@ -41,7 +41,7 @@ add "&raw" to the end of the URL within a browser. }); // Produce a Location query string from a parameter object. function locationQuery(params) { - return '?' + Object.keys(params).map(function (key) { + return '#' + Object.keys(params).map(function (key) { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }).join('&'); From 0d8f9db3fbeef93be194d386f87dea627f69715e Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 19:48:21 +0100 Subject: [PATCH 03/19] Pass options from the fragment, not the template context --- .../templates/graphene/graphiql.html | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 5bc5e04..6515da8 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -100,22 +100,27 @@ add "&raw" to the end of the URL within a browser. function updateURL() { history.replaceState(null, null, locationQuery(parameters)); } - // Render into the body. - ReactDOM.render( - React.createElement(GraphiQL, { - fetcher: graphQLFetcher, + // If there are any fragment parameters, confirm the user wants to use them. + if (Object.keys(parameters).length + && !window.confirm("An untrusted query has been loaded, continue loading query?")) { + parameters = {}; + } + var options = { + fetcher: graphQLFetcher, onEditQuery: onEditQuery, onEditVariables: onEditVariables, onEditOperationName: onEditOperationName, - query: '{{ query|escapejs }}', - response: '{{ result|escapejs }}', - {% if variables %} - variables: '{{ variables|escapejs }}', - {% endif %} - {% if operation_name %} - operationName: '{{ operation_name|escapejs }}', - {% endif %} - }), + query: parameters.query, + } + if (parameters.variables) { + options.variables = parameters.variables; + } + if (parameters.operation_name) { + options.operationName = parameters.operation_name; + } + // Render into the body. + ReactDOM.render( + React.createElement(GraphiQL, options), document.body ); From 9a5b3556d3d13ec3c46ea4bc953f1df844f0925d Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 19:48:38 +0100 Subject: [PATCH 04/19] Special case reloads as allowed if we can --- graphene_django/templates/graphene/graphiql.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 6515da8..0303883 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -101,7 +101,9 @@ add "&raw" to the end of the URL within a browser. history.replaceState(null, null, locationQuery(parameters)); } // If there are any fragment parameters, confirm the user wants to use them. + var isReload = window.performance ? performance.navigation.type === 1 : false; if (Object.keys(parameters).length + && !isReload && !window.confirm("An untrusted query has been loaded, continue loading query?")) { parameters = {}; } From d1b734f07df87f97f3acc557d990952e3a250e7d Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 20:31:39 +0100 Subject: [PATCH 05/19] Allow the user to see the query before prompting This also allows the introspection query through so that the user can edit with intellisense before being prompted. --- .../templates/graphene/graphiql.html | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 0303883..de3126a 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -58,9 +58,31 @@ add "&raw" to the end of the URL within a browser. otherParams[k] = parameters[k]; } } + + // If there are any fragment parameters, confirm the user wants to use them. + var isReload = window.performance ? performance.navigation.type === 1 : false; + var isQueryTrusted = Object.keys(parameters).length === 0 || isReload; + var fetchURL = locationQuery(otherParams); + // Defines a GraphQL fetcher using the fetch API. function graphQLFetcher(graphQLParams) { + var isIntrospectionQuery = ( + graphQLParams.query !== parameters.query + && graphQLParams.query.indexOf('IntrospectionQuery') !== -1 + ); + + if (!isQueryTrusted + && !isIntrospectionQuery + && !window.confirm("This query was loaded from a link, are you sure you want to execute it?")) { + return Promise.resolve('Aborting query.'); + } + + // We don't want to set this for the introspection query + if (!isIntrospectionQuery) { + isQueryTrusted = true; + } + var headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' @@ -100,13 +122,6 @@ add "&raw" to the end of the URL within a browser. function updateURL() { history.replaceState(null, null, locationQuery(parameters)); } - // If there are any fragment parameters, confirm the user wants to use them. - var isReload = window.performance ? performance.navigation.type === 1 : false; - if (Object.keys(parameters).length - && !isReload - && !window.confirm("An untrusted query has been loaded, continue loading query?")) { - parameters = {}; - } var options = { fetcher: graphQLFetcher, onEditQuery: onEditQuery, From 24ebc20bf449b4688220e2dac0b43240d0fc5a6c Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 20:32:38 +0100 Subject: [PATCH 06/19] Fix comment --- graphene_django/templates/graphene/graphiql.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index de3126a..cf61686 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -39,7 +39,7 @@ add "&raw" to the end of the URL within a browser. decodeURIComponent(entry.slice(eq + 1)); } }); - // Produce a Location query string from a parameter object. + // Produce a Location fragment string from a parameter object. function locationQuery(params) { return '#' + Object.keys(params).map(function (key) { return encodeURIComponent(key) + '=' + From e50e12bc9fa693bde1af1b61f2a97eaba90007bd Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 20:36:26 +0100 Subject: [PATCH 07/19] Move GraphiQL's JS into a separate file for ease of CSP --- .../static/graphene_django/graphiql.js | 119 +++++++++++++++++ .../templates/graphene/graphiql.html | 120 +----------------- 2 files changed, 121 insertions(+), 118 deletions(-) create mode 100644 graphene_django/static/graphene_django/graphiql.js diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js new file mode 100644 index 0000000..ad55e03 --- /dev/null +++ b/graphene_django/static/graphene_django/graphiql.js @@ -0,0 +1,119 @@ +(function() { + + // 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(); + + // Collect the URL parameters + var parameters = {}; + window.location.hash.substr(1).split('&').forEach(function (entry) { + var eq = entry.indexOf('='); + if (eq >= 0) { + parameters[decodeURIComponent(entry.slice(0, eq))] = + decodeURIComponent(entry.slice(eq + 1)); + } + }); + // Produce a Location fragment string from a parameter object. + function locationQuery(params) { + return '#' + Object.keys(params).map(function (key) { + return encodeURIComponent(key) + '=' + + encodeURIComponent(params[key]); + }).join('&'); + } + // Derive a fetch URL from the current URL, sans the GraphQL parameters. + var graphqlParamNames = { + query: true, + variables: true, + operationName: true + }; + var otherParams = {}; + for (var k in parameters) { + if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) { + otherParams[k] = parameters[k]; + } + } + + // If there are any fragment parameters, confirm the user wants to use them. + var isReload = window.performance ? performance.navigation.type === 1 : false; + var isQueryTrusted = Object.keys(parameters).length === 0 || isReload; + + var fetchURL = locationQuery(otherParams); + + // Defines a GraphQL fetcher using the fetch API. + function graphQLFetcher(graphQLParams) { + var isIntrospectionQuery = ( + graphQLParams.query !== parameters.query + && graphQLParams.query.indexOf('IntrospectionQuery') !== -1 + ); + + if (!isQueryTrusted + && !isIntrospectionQuery + && !window.confirm("This query was loaded from a link, are you sure you want to execute it?")) { + return Promise.resolve('Aborting query.'); + } + + // We don't want to set this for the introspection query + if (!isIntrospectionQuery) { + isQueryTrusted = true; + } + + var headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + if (csrftoken) { + 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; + } + }); + } + // When the query and variables string is edited, update the URL bar so + // that it can be easily shared. + function onEditQuery(newQuery) { + parameters.query = newQuery; + updateURL(); + } + function onEditVariables(newVariables) { + parameters.variables = newVariables; + updateURL(); + } + function onEditOperationName(newOperationName) { + parameters.operationName = newOperationName; + updateURL(); + } + function updateURL() { + history.replaceState(null, null, locationQuery(parameters)); + } + var options = { + fetcher: graphQLFetcher, + onEditQuery: onEditQuery, + onEditVariables: onEditVariables, + onEditOperationName: onEditOperationName, + query: parameters.query, + } + if (parameters.variables) { + options.variables = parameters.variables; + } + if (parameters.operation_name) { + options.operationName = parameters.operation_name; + } + // Render into the body. + ReactDOM.render( + React.createElement(GraphiQL, options), + document.body + ); +})(); diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index cf61686..7bd1178 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -5,6 +5,7 @@ exploring GraphQL. If you wish to receive JSON, provide the header "Accept: application/json" or add "&raw" to the end of the URL within a browser. --> +{% load static %} @@ -23,123 +24,6 @@ add "&raw" to the end of the URL within a browser. - + From 7e8f6dbd4ec0fe633507e5ba6bc22bda4a0e59fc Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 20:58:00 +0100 Subject: [PATCH 08/19] Change quotes to improve some syntax highlighting --- graphene_django/templates/graphene/graphiql.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 7bd1178..af11274 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -24,6 +24,6 @@ add "&raw" to the end of the URL within a browser. - + From cb87f4016546aac5f4973e6d01fb31b0cbc10d4e Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Thu, 30 Aug 2018 20:59:09 +0100 Subject: [PATCH 09/19] Document that staticfiles is now a dependency. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4e0b01d..ef3f40c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ pip install "graphene-django>=2.0" ```python INSTALLED_APPS = ( # ... + 'django.contrib.staticfiles', # Required for GraphiQL 'graphene_django', ) From 2b08e59bea2a0d84dc68832dc6d2deae98e77d3a Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Sun, 9 Sep 2018 21:44:30 +0100 Subject: [PATCH 10/19] Revert to default query execution behaviour The only security risk here is persuading a user to execute a mutation, which is probably not a big risk. To mitigate this risk and still keep the same UX (that is so valuable), would require more work than is proportionate for this PR. --- .../static/graphene_django/graphiql.js | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index ad55e03..2be7e3c 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -35,30 +35,10 @@ } } - // If there are any fragment parameters, confirm the user wants to use them. - var isReload = window.performance ? performance.navigation.type === 1 : false; - var isQueryTrusted = Object.keys(parameters).length === 0 || isReload; - var fetchURL = locationQuery(otherParams); // Defines a GraphQL fetcher using the fetch API. function graphQLFetcher(graphQLParams) { - var isIntrospectionQuery = ( - graphQLParams.query !== parameters.query - && graphQLParams.query.indexOf('IntrospectionQuery') !== -1 - ); - - if (!isQueryTrusted - && !isIntrospectionQuery - && !window.confirm("This query was loaded from a link, are you sure you want to execute it?")) { - return Promise.resolve('Aborting query.'); - } - - // We don't want to set this for the introspection query - if (!isIntrospectionQuery) { - isQueryTrusted = true; - } - var headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' From f3144bf996cac8f137b1838a0d2f3717b32907bf Mon Sep 17 00:00:00 2001 From: Alonso Date: Wed, 26 Sep 2018 13:07:50 -0500 Subject: [PATCH 11/19] fix order in params --- docs/authorization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index 7a08481..86ad66a 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -114,7 +114,7 @@ method to your ``DjangoObjectType``. interfaces = (relay.Node, ) @classmethod - def get_node(cls, id, info): + def get_node(cls, info, id): try: post = cls._meta.model.objects.get(id=id) except cls._meta.model.DoesNotExist: From 19ef9a094ad8dbf20234624b21d567fe86f7b413 Mon Sep 17 00:00:00 2001 From: Khaled Alqenaei Date: Thu, 18 Oct 2018 11:33:53 -0700 Subject: [PATCH 12/19] Making the example working for Django 2.1.2 --- .../cookbook/ingredients/models.py | 4 +++- .../cookbook/ingredients/schema.py | 18 +++++++++--------- .../cookbook-plain/cookbook/recipes/models.py | 9 +++++---- .../cookbook-plain/cookbook/recipes/schema.py | 16 ++++++++-------- examples/cookbook-plain/cookbook/settings.py | 3 +-- examples/cookbook-plain/cookbook/urls.py | 6 +++--- examples/cookbook-plain/requirements.txt | 2 +- 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/examples/cookbook-plain/cookbook/ingredients/models.py b/examples/cookbook-plain/cookbook/ingredients/models.py index 2f0eba3..5836949 100644 --- a/examples/cookbook-plain/cookbook/ingredients/models.py +++ b/examples/cookbook-plain/cookbook/ingredients/models.py @@ -2,6 +2,8 @@ from django.db import models class Category(models.Model): + class Meta: + verbose_name_plural = 'Categories' name = models.CharField(max_length=100) def __str__(self): @@ -11,7 +13,7 @@ class Category(models.Model): class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, related_name='ingredients') + category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE) def __str__(self): return self.name diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py index 1f3bb18..51f25ed 100644 --- a/examples/cookbook-plain/cookbook/ingredients/schema.py +++ b/examples/cookbook-plain/cookbook/ingredients/schema.py @@ -1,7 +1,7 @@ import graphene from graphene_django.types import DjangoObjectType -from cookbook.ingredients.models import Category, Ingredient +from .models import Category, Ingredient class CategoryType(DjangoObjectType): @@ -25,16 +25,16 @@ class Query(object): name=graphene.String()) all_ingredients = graphene.List(IngredientType) - def resolve_all_categories(self, args, context, info): + def resolve_all_categories(self, context, **kwargs): return Category.objects.all() - def resolve_all_ingredients(self, args, context, info): + def resolve_all_ingredients(self, context, **kwargs): # We can easily optimize query count in the resolve method return Ingredient.objects.select_related('category').all() - def resolve_category(self, args, context, info): - id = args.get('id') - name = args.get('name') + def resolve_category(self, context, **kwargs): + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Category.objects.get(pk=id) @@ -44,9 +44,9 @@ class Query(object): return None - def resolve_ingredient(self, args, context, info): - id = args.get('id') - name = args.get('name') + def resolve_ingredient(self, context, **kwargs): + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Ingredient.objects.get(pk=id) diff --git a/examples/cookbook-plain/cookbook/recipes/models.py b/examples/cookbook-plain/cookbook/recipes/models.py index ca12fac..382b88e 100644 --- a/examples/cookbook-plain/cookbook/recipes/models.py +++ b/examples/cookbook-plain/cookbook/recipes/models.py @@ -1,17 +1,18 @@ from django.db import models -from cookbook.ingredients.models import Ingredient +from ..ingredients.models import Ingredient class Recipe(models.Model): title = models.CharField(max_length=100) instructions = models.TextField() - __unicode__ = lambda self: self.title + def __str__(self): + return self.title class RecipeIngredient(models.Model): - recipe = models.ForeignKey(Recipe, related_name='amounts') - ingredient = models.ForeignKey(Ingredient, related_name='used_by') + recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE) + ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE) amount = models.FloatField() unit = models.CharField(max_length=20, choices=( ('unit', 'Units'), diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py index 040c985..620d510 100644 --- a/examples/cookbook-plain/cookbook/recipes/schema.py +++ b/examples/cookbook-plain/cookbook/recipes/schema.py @@ -1,7 +1,7 @@ import graphene from graphene_django.types import DjangoObjectType -from cookbook.recipes.models import Recipe, RecipeIngredient +from .models import Recipe, RecipeIngredient class RecipeType(DjangoObjectType): @@ -24,9 +24,9 @@ class Query(object): id=graphene.Int()) all_recipeingredients = graphene.List(RecipeIngredientType) - def resolve_recipe(self, args, context, info): - id = args.get('id') - title = args.get('title') + def resolve_recipe(self, context, **kwargs): + id = kwargs.get('id') + title = kwargs.get('title') if id is not None: return Recipe.objects.get(pk=id) @@ -36,17 +36,17 @@ class Query(object): return None - def resolve_recipeingredient(self, args, context, info): - id = args.get('id') + def resolve_recipeingredient(self, context, **kwargs): + id = kwargs.get('id') if id is not None: return RecipeIngredient.objects.get(pk=id) return None - def resolve_all_recipes(self, args, context, info): + def resolve_all_recipes(self, context, **kwargs): return Recipe.objects.all() - def resolve_all_recipeingredients(self, args, context, info): + def resolve_all_recipeingredients(self, context, **kwargs): related = ['recipe', 'ingredient'] return RecipeIngredient.objects.select_related(*related).all() diff --git a/examples/cookbook-plain/cookbook/settings.py b/examples/cookbook-plain/cookbook/settings.py index 948292d..d846db4 100644 --- a/examples/cookbook-plain/cookbook/settings.py +++ b/examples/cookbook-plain/cookbook/settings.py @@ -44,13 +44,12 @@ INSTALLED_APPS = [ 'cookbook.recipes.apps.RecipesConfig', ] -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] diff --git a/examples/cookbook-plain/cookbook/urls.py b/examples/cookbook-plain/cookbook/urls.py index 9f8755b..4f87da0 100644 --- a/examples/cookbook-plain/cookbook/urls.py +++ b/examples/cookbook-plain/cookbook/urls.py @@ -1,10 +1,10 @@ -from django.conf.urls import url +from django.urls import path from django.contrib import admin from graphene_django.views import GraphQLView urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^graphql', GraphQLView.as_view(graphiql=True)), + path('admin/', admin.site.urls), + path('graphql/', GraphQLView.as_view(graphiql=True)), ] diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index 362a39a..539fd67 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene graphene-django graphql-core>=2.1rc1 -django==1.9 +django==2.1.2 From 4359e1f312d3d27b203dec2a5bfd4c9a8346d42b Mon Sep 17 00:00:00 2001 From: Khaled Alqenaei Date: Thu, 18 Oct 2018 11:36:35 -0700 Subject: [PATCH 13/19] Making the example working for Django 2.1.2 --- .../migrations/0003_auto_20181018_1746.py | 17 +++++++++++++++++ .../migrations/0003_auto_20181018_1728.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py create mode 100644 examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py 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 new file mode 100644 index 0000000..184e79e --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0 on 2018-10-18 17:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ingredients', '0002_auto_20161104_0050'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'Categories'}, + ), + ] 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 new file mode 100644 index 0000000..7a8df49 --- /dev/null +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0 on 2018-10-18 17:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recipes', '0002_auto_20161104_0106'), + ] + + operations = [ + migrations.AlterField( + model_name='recipeingredient', + name='unit', + field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20), + ), + ] From 905b4249f3ff71b35105638242c3f908ead70324 Mon Sep 17 00:00:00 2001 From: Khaled Alqenaei Date: Thu, 18 Oct 2018 12:27:23 -0700 Subject: [PATCH 14/19] Updated ingredients/schema.py and recipes/schema.py to be more readable. --- .../cookbook-plain/cookbook/ingredients/schema.py | 14 ++++---------- examples/cookbook-plain/cookbook/recipes/schema.py | 13 ++++--------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py index 51f25ed..e7ef688 100644 --- a/examples/cookbook-plain/cookbook/ingredients/schema.py +++ b/examples/cookbook-plain/cookbook/ingredients/schema.py @@ -25,17 +25,14 @@ class Query(object): name=graphene.String()) all_ingredients = graphene.List(IngredientType) - def resolve_all_categories(self, context, **kwargs): + def resolve_all_categories(self, context): return Category.objects.all() - def resolve_all_ingredients(self, context, **kwargs): + def resolve_all_ingredients(self, context): # We can easily optimize query count in the resolve method return Ingredient.objects.select_related('category').all() - def resolve_category(self, context, **kwargs): - id = kwargs.get('id') - name = kwargs.get('name') - + def resolve_category(self, context, id=None, name=None): if id is not None: return Category.objects.get(pk=id) @@ -44,10 +41,7 @@ class Query(object): return None - def resolve_ingredient(self, context, **kwargs): - id = kwargs.get('id') - name = kwargs.get('name') - + def resolve_ingredient(self, context, id=None, name=None): if id is not None: return Ingredient.objects.get(pk=id) diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py index 620d510..74692f8 100644 --- a/examples/cookbook-plain/cookbook/recipes/schema.py +++ b/examples/cookbook-plain/cookbook/recipes/schema.py @@ -24,10 +24,7 @@ class Query(object): id=graphene.Int()) all_recipeingredients = graphene.List(RecipeIngredientType) - def resolve_recipe(self, context, **kwargs): - id = kwargs.get('id') - title = kwargs.get('title') - + def resolve_recipe(self, context, id=None, title=None): if id is not None: return Recipe.objects.get(pk=id) @@ -36,17 +33,15 @@ class Query(object): return None - def resolve_recipeingredient(self, context, **kwargs): - id = kwargs.get('id') - + def resolve_recipeingredient(self, context, id=None): if id is not None: return RecipeIngredient.objects.get(pk=id) return None - def resolve_all_recipes(self, context, **kwargs): + def resolve_all_recipes(self, context): return Recipe.objects.all() - def resolve_all_recipeingredients(self, context, **kwargs): + def resolve_all_recipeingredients(self, context): related = ['recipe', 'ingredient'] return RecipeIngredient.objects.select_related(*related).all() From ce8fa7f9f2fa6afabae0d7fa93c8cc85dc5945b9 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Sat, 9 Mar 2019 22:39:04 +0100 Subject: [PATCH 15/19] Fix lint error --- graphene_django/types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/graphene_django/types.py b/graphene_django/types.py index aa8b5a3..4441a9a 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -1,5 +1,7 @@ +import six from collections import OrderedDict +from django.db.models import Model from django.utils.functional import SimpleLazyObject from graphene import Field from graphene.relay import Connection, Node @@ -11,6 +13,10 @@ from .registry import Registry, get_global_registry from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model +if six.PY3: + from typing import Type + + def construct_fields(model, registry, only_fields, exclude_fields): _model_fields = get_model_fields(model) From ae126a6dc3d7ebc24121e6201ad078f413c81455 Mon Sep 17 00:00:00 2001 From: Charles Bradshaw Date: Tue, 19 Mar 2019 16:20:26 -0400 Subject: [PATCH 16/19] Add Introspection Schema Link in Documentation (#489) By the end of the Graphene and Django Tutorial using Relay, one might think they have finished everything needed server side for Relay, but six sections later, in a section that doesn't mention Relay in the title, the final required step for Relay is documented. --- docs/tutorial-relay.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index f2502d7..188a108 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -345,3 +345,10 @@ Or you can get only 'meat' ingredients containing the letter 'e': } } } + + + +Final Steps +^^^^^^^^^^^ + +We have created a GraphQL endpoint that will work with Relay, but for Relay to work it needs access to a (non python) schema. Instructions to export the schema can be found on the `Introspection Schema `__ part of this guide. From 75bf39852326c06236efe93f50c8d2ade3cd933f Mon Sep 17 00:00:00 2001 From: Liam O'Flynn Date: Tue, 19 Mar 2019 13:22:04 -0700 Subject: [PATCH 17/19] Ensure code example is fully functional (#477) Simple change, it took me a while to figure out why the documentation's code was throwing an AssertionError :) --- docs/tutorial-relay.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index 188a108..3627311 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: interfaces = (relay.Node, ) - class Query(object): + class Query(graphene.ObjectType): category = relay.Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) From 263c7267cbc3ba1511a28234edf484896f38d3d2 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 19 Mar 2019 16:24:24 -0400 Subject: [PATCH 18/19] Fix code errors in form-mutations.rst (#499) This fixes what appear to be some code errors/typos: * The `FormMutation` class was renamed to `DjangoFormMutation`, and `ModelFormMutation` to `DjangoModelFormMutation`, in 40610c64a3be003719d88db439e442085ed18072. * `form_valid` was renamed to `perform_mutate` in 463ce68b16b070c0a49637dc71755a33acdf9d49. It also clarifies a few things that I found confusing: * It explicitly mentions that `perform_mutate` is a class method. * The code samples now import the form classes from their packages, so readers know where to import them from too. --- docs/form-mutations.rst | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst index e721a78..bbaadb1 100644 --- a/docs/form-mutations.rst +++ b/docs/form-mutations.rst @@ -4,27 +4,31 @@ Integration with Django forms Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation. *Note: the API is experimental and will likely change in the future.* -FormMutation ------------- +DjangoFormMutation +------------------ .. code:: python + from graphene_django.forms.mutation import DjangoFormMutation + class MyForm(forms.Form): name = forms.CharField() - class MyMutation(FormMutation): + class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm ``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string. -ModelFormMutation ------------------ +DjangoModelFormMutation +----------------------- -``ModelFormMutation`` will pull the fields from a ``ModelForm``. +``DjangoModelFormMutation`` will pull the fields from a ``ModelForm``. .. code:: python + from graphene_django.forms.mutation import DjangoModelFormMutation + class Pet(models.Model): name = models.CharField() @@ -61,8 +65,8 @@ Form validation Form mutations will call ``is_valid()`` on your forms. -If the form is valid then ``form_valid(form, info)`` is called on the mutation. Override this method to change how -the form is saved or to return a different Graphene object type. +If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method +to change how the form is saved or to return a different Graphene object type. If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages. From ea2cd9894fa1c8a65c365d1b7c042e27df519324 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 19 Mar 2019 20:34:10 +0000 Subject: [PATCH 19/19] Always use HTTPS for CDN files (#498) * Always use HTTPS for CDN files There's no point using insecure, deprecated HTTP even if the current page is on HTTP. * add integrity and crossorigin attributes --- .../templates/graphene/graphiql.html | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index af11274..c0c9af1 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -17,11 +17,20 @@ add "&raw" to the end of the URL within a browser. width: 100%; } - - - - - + + + + +