Merge remote-tracking branch 'up/master' into feature/improved-mutations

This commit is contained in:
Jason Kraus 2019-03-20 10:56:54 -07:00
commit 917851bd97
17 changed files with 207 additions and 156 deletions

View File

@ -20,6 +20,7 @@ pip install "graphene-django>=2.0"
```python ```python
INSTALLED_APPS = ( INSTALLED_APPS = (
# ... # ...
'django.contrib.staticfiles', # Required for GraphiQL
'graphene_django', 'graphene_django',
) )

View File

@ -114,7 +114,7 @@ method to your ``DjangoObjectType``.
interfaces = (relay.Node, ) interfaces = (relay.Node, )
@classmethod @classmethod
def get_node(cls, id, info): def get_node(cls, info, id):
try: try:
post = cls._meta.model.objects.get(id=id) post = cls._meta.model.objects.get(id=id)
except cls._meta.model.DoesNotExist: except cls._meta.model.DoesNotExist:

View File

@ -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. 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.* *Note: the API is experimental and will likely change in the future.*
FormMutation DjangoFormMutation
------------ ------------------
.. code:: python .. code:: python
from graphene_django.forms.mutation import DjangoFormMutation
class MyForm(forms.Form): class MyForm(forms.Form):
name = forms.CharField() name = forms.CharField()
class MyMutation(FormMutation): class MyMutation(DjangoFormMutation):
class Meta: class Meta:
form_class = MyForm 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. ``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 .. code:: python
from graphene_django.forms.mutation import DjangoModelFormMutation
class Pet(models.Model): class Pet(models.Model):
name = models.CharField() name = models.CharField()
@ -61,8 +65,8 @@ Form validation
Form mutations will call ``is_valid()`` on your forms. 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 If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method
the form is saved or to return a different Graphene object type. 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 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. containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.

View File

@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
interfaces = (relay.Node, ) interfaces = (relay.Node, )
class Query(object): class Query(graphene.ObjectType):
category = relay.Node.Field(CategoryNode) category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(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 <http://docs.graphene-python.org/projects/django/en/latest/introspection/>`__ part of this guide.

View File

@ -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'},
),
]

View File

@ -2,6 +2,8 @@ from django.db import models
class Category(models.Model): class Category(models.Model):
class Meta:
verbose_name_plural = 'Categories'
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
def __str__(self): def __str__(self):
@ -11,7 +13,7 @@ class Category(models.Model):
class Ingredient(models.Model): class Ingredient(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True) 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): def __str__(self):
return self.name return self.name

View File

@ -1,7 +1,7 @@
import graphene import graphene
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient from .models import Category, Ingredient
class CategoryType(DjangoObjectType): class CategoryType(DjangoObjectType):
@ -25,17 +25,14 @@ class Query(object):
name=graphene.String()) name=graphene.String())
all_ingredients = graphene.List(IngredientType) all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, args, context, info): def resolve_all_categories(self, context):
return Category.objects.all() 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 # We can easily optimize query count in the resolve method
return Ingredient.objects.select_related('category').all() return Ingredient.objects.select_related('category').all()
def resolve_category(self, args, context, info): def resolve_category(self, context, id=None, name=None):
id = args.get('id')
name = args.get('name')
if id is not None: if id is not None:
return Category.objects.get(pk=id) return Category.objects.get(pk=id)
@ -44,10 +41,7 @@ class Query(object):
return None return None
def resolve_ingredient(self, args, context, info): def resolve_ingredient(self, context, id=None, name=None):
id = args.get('id')
name = args.get('name')
if id is not None: if id is not None:
return Ingredient.objects.get(pk=id) return Ingredient.objects.get(pk=id)

View File

@ -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),
),
]

View File

@ -1,17 +1,18 @@
from django.db import models from django.db import models
from cookbook.ingredients.models import Ingredient from ..ingredients.models import Ingredient
class Recipe(models.Model): class Recipe(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
instructions = models.TextField() instructions = models.TextField()
__unicode__ = lambda self: self.title def __str__(self):
return self.title
class RecipeIngredient(models.Model): class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='amounts') recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, related_name='used_by') ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE)
amount = models.FloatField() amount = models.FloatField()
unit = models.CharField(max_length=20, choices=( unit = models.CharField(max_length=20, choices=(
('unit', 'Units'), ('unit', 'Units'),

View File

@ -1,7 +1,7 @@
import graphene import graphene
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
from cookbook.recipes.models import Recipe, RecipeIngredient from .models import Recipe, RecipeIngredient
class RecipeType(DjangoObjectType): class RecipeType(DjangoObjectType):
@ -24,10 +24,7 @@ class Query(object):
id=graphene.Int()) id=graphene.Int())
all_recipeingredients = graphene.List(RecipeIngredientType) all_recipeingredients = graphene.List(RecipeIngredientType)
def resolve_recipe(self, args, context, info): def resolve_recipe(self, context, id=None, title=None):
id = args.get('id')
title = args.get('title')
if id is not None: if id is not None:
return Recipe.objects.get(pk=id) return Recipe.objects.get(pk=id)
@ -36,17 +33,15 @@ class Query(object):
return None return None
def resolve_recipeingredient(self, args, context, info): def resolve_recipeingredient(self, context, id=None):
id = args.get('id')
if id is not None: if id is not None:
return RecipeIngredient.objects.get(pk=id) return RecipeIngredient.objects.get(pk=id)
return None return None
def resolve_all_recipes(self, args, context, info): def resolve_all_recipes(self, context):
return Recipe.objects.all() return Recipe.objects.all()
def resolve_all_recipeingredients(self, args, context, info): def resolve_all_recipeingredients(self, context):
related = ['recipe', 'ingredient'] related = ['recipe', 'ingredient']
return RecipeIngredient.objects.select_related(*related).all() return RecipeIngredient.objects.select_related(*related).all()

View File

@ -44,13 +44,12 @@ INSTALLED_APPS = [
'cookbook.recipes.apps.RecipesConfig', 'cookbook.recipes.apps.RecipesConfig',
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]

View File

@ -1,10 +1,10 @@
from django.conf.urls import url from django.urls import path
from django.contrib import admin from django.contrib import admin
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls), path('admin/', admin.site.urls),
url(r'^graphql', GraphQLView.as_view(graphiql=True)), path('graphql/', GraphQLView.as_view(graphiql=True)),
] ]

View File

@ -1,4 +1,4 @@
graphene graphene
graphene-django graphene-django
graphql-core>=2.1rc1 graphql-core>=2.1rc1
django==1.9 django==2.1.2

View File

@ -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 <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, options),
document.body
);
})();

View File

@ -5,6 +5,7 @@ exploring GraphQL.
If you wish to receive JSON, provide the header "Accept: application/json" or If you wish to receive JSON, provide the header "Accept: application/json" or
add "&raw" to the end of the URL within a browser. add "&raw" to the end of the URL within a browser.
--> -->
{% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -16,108 +17,22 @@ add "&raw" to the end of the URL within a browser.
width: 100%; width: 100%;
} }
</style> </style>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css"
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script> rel="stylesheet"
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script> crossorigin="anonymous" />
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script> integrity="sha384-dcF7KoWRaRpjcNbVPUFgatYgAijf8DqW6NWuqLdfB5Sb4Cdbb8iHX7bHsl9YhpKa"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"
integrity="sha384-j40ChW3xknV2Dsc9+kP3/6SW2UrR7gYSbx9pmyNU1YTacm/PEj/0bxB9vM8jWFqx"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"
integrity="sha384-P4XM5fEtXj1kXZzsm1EOHZ7HmQIuzyRjjvX4na21R4eRLjmm+oUZua5ALb2PIojw"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<script> <script src="{% static 'graphene_django/graphiql.js' %}"></script>
// 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.search.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 query 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));
}
// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
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 %}
}),
document.body
);
</script>
</body> </body>
</html> </html>

View File

@ -1,5 +1,7 @@
import six
from collections import OrderedDict from collections import OrderedDict
from django.db.models import Model
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from graphene import Field from graphene import Field
from graphene.relay import Connection, Node 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 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): def construct_fields(model, registry, only_fields, exclude_fields):
_model_fields = get_model_fields(model) _model_fields = get_model_fields(model)

View File

@ -124,6 +124,12 @@ class GraphQLView(View):
data = self.parse_body(request) data = self.parse_body(request)
show_graphiql = self.graphiql and self.can_display_graphiql(request, data) 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: if self.batch:
responses = [self.get_response(request, entry) for entry in data] responses = [self.get_response(request, entry) for entry in data]
result = "[{}]".format( result = "[{}]".format(
@ -137,19 +143,6 @@ class GraphQLView(View):
else: else:
result, status_code = self.get_response(request, data, show_graphiql) 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( return HttpResponse(
status=status_code, content=result, content_type="application/json" status=status_code, content=result, content_type="application/json"
) )