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',
)
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:
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.
diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst
index f2502d7..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)
@@ -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.
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/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..e7ef688 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,17 +25,14 @@ 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):
return Category.objects.all()
- def resolve_all_ingredients(self, args, context, info):
+ 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, args, context, info):
- id = args.get('id')
- name = args.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, args, context, info):
- id = args.get('id')
- name = args.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/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),
+ ),
+ ]
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..74692f8 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,10 +24,7 @@ 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, 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, args, context, info):
- id = args.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, args, context, info):
+ def resolve_all_recipes(self, context):
return Recipe.objects.all()
- def resolve_all_recipeingredients(self, args, context, info):
+ def resolve_all_recipeingredients(self, context):
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
diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js
new file mode 100644
index 0000000..2be7e3c
--- /dev/null
+++ b/graphene_django/static/graphene_django/graphiql.js
@@ -0,0 +1,99 @@
+(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];
+ }
+ }
+
+ var fetchURL = locationQuery(otherParams);
+
+ // Defines a GraphQL fetcher using the fetch API.
+ function graphQLFetcher(graphQLParams) {
+ 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 1ba0613..c0c9af1 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 %}
@@ -16,108 +17,22 @@ add "&raw" to the end of the URL within a browser.
width: 100%;
}
-
-
-
-
-
+
+
+
+
+
-
+
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)
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"
)