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
INSTALLED_APPS = (
# ...
'django.contrib.staticfiles', # Required for GraphiQL
'graphene_django',
)

View File

@ -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:

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.
*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.

View File

@ -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 <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 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

View File

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

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 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'),

View File

@ -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()

View File

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

View File

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

View File

@ -1,4 +1,4 @@
graphene
graphene-django
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
add "&raw" to the end of the URL within a browser.
-->
{% load static %}
<!DOCTYPE html>
<html>
<head>
@ -16,108 +17,22 @@ add "&raw" to the end of the URL within a browser.
width: 100%;
}
</style>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css"
rel="stylesheet"
crossorigin="anonymous" />
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"
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>
<body>
<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>
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
</body>
</html>

View File

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

View File

@ -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"
)