Merge branch 'master' into recursive-nodes

This commit is contained in:
Tony Angerilli 2016-11-14 00:32:56 -08:00
commit 73f4a92b4f
24 changed files with 305 additions and 49 deletions

View File

@ -58,6 +58,7 @@ To create a GraphQL schema for it you simply have to write the following:
```python ```python
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
import graphene
class User(DjangoObjectType): class User(DjangoObjectType):
class Meta: class Meta:
@ -106,3 +107,21 @@ After developing, the full test suite can be evaluated by running:
```sh ```sh
python setup.py test # Use --pytest-args="-v -s" for verbose mode python setup.py test # Use --pytest-args="-v -s" for verbose mode
``` ```
### Documentation
The documentation is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
The documentation dependencies are installed by running:
```sh
cd docs
pip install -r requirements.txt
```
Then to produce a HTML version of the documentation:
```sh
make html
```

View File

@ -68,6 +68,7 @@ following:
.. code:: python .. code:: python
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
import graphene
class User(DjangoObjectType): class User(DjangoObjectType):
class Meta: class Meta:
@ -116,6 +117,25 @@ After developing, the full test suite can be evaluated by running:
python setup.py test # Use --pytest-args="-v -s" for verbose mode python setup.py test # Use --pytest-args="-v -s" for verbose mode
Documentation
~~~~~~~~~~~~~
The documentation can be generated using the excellent
`Sphinx <http://www.sphinx-doc.org/>`__ and a custom theme.
To install the documentation dependencies, run the following:
.. code:: sh
cd docs
pip install -r requirements.txt
Then to produce a HTML version of the documentation:
.. code:: sh
make html
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png .. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master
:target: https://travis-ci.org/graphql-python/graphene-django :target: https://travis-ci.org/graphql-python/graphene-django

View File

@ -1,2 +1,3 @@
sphinx
# Docs template # Docs template
https://github.com/graphql-python/graphene-python.org/archive/docs.zip https://github.com/graphql-python/graphene-python.org/archive/docs.zip

View File

@ -188,6 +188,8 @@ And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py
'SCHEMA': 'cookbook.schema.schema' 'SCHEMA': 'cookbook.schema.schema'
} }
Alternatively, we can specify the schema to be used in the urls definition,
as explained below.
Creating GraphQL and GraphiQL views Creating GraphQL and GraphiQL views
----------------------------------- -----------------------------------
@ -199,6 +201,22 @@ view.
This view will serve as GraphQL endpoint. As we want to have the This view will serve as GraphQL endpoint. As we want to have the
aforementioned GraphiQL we specify that on the params with ``graphiql=True``. aforementioned GraphiQL we specify that on the params with ``graphiql=True``.
.. code:: python
from django.conf.urls import url, include
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)),
]
If we didn't specify the target schema in the Django settings file
as explained above, we can do so here using:
.. code:: python .. code:: python
from django.conf.urls import url, include from django.conf.urls import url, include
@ -210,7 +228,7 @@ aforementioned GraphiQL we specify that on the params with ``graphiql=True``.
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^graphql', GraphQLView.as_view(graphiql=True)), url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)),
] ]
Apply model changes to database Apply model changes to database

View File

@ -60,5 +60,5 @@ Now you should be ready to start the server:
Now head on over to Now head on over to
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
and run some queries! and run some queries!
(See the [Django quickstart guide](http://graphene-python.org/docs/quickstart-django/) (See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial.html#testing-our-graphql-schema)
for some example queries) for some example queries)

View File

@ -2,5 +2,9 @@ from django.contrib import admin
from cookbook.ingredients.models import Category, Ingredient from cookbook.ingredients.models import Category, Ingredient
admin.site.register(Ingredient) @admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin):
list_display = ("id","name","category")
list_editable = ("name","category")
admin.site.register(Category) admin.site.register(Category)

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ingredients', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='ingredient',
name='notes',
field=models.TextField(blank=True, null=True),
),
]

View File

@ -10,7 +10,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() notes = models.TextField(null=True,blank=True)
category = models.ForeignKey(Category, related_name='ingredients') category = models.ForeignKey(Category, related_name='ingredients')
def __str__(self): def __str__(self):

View File

@ -1,5 +1,5 @@
from cookbook.ingredients.models import Category, Ingredient from cookbook.ingredients.models import Category, Ingredient
from graphene import AbstractType, Field, Node from graphene import AbstractType, Node
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
@ -31,8 +31,8 @@ class IngredientNode(DjangoObjectType):
class Query(AbstractType): class Query(AbstractType):
category = Field(CategoryNode) category = Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)
ingredient = Field(IngredientNode) ingredient = Node.Field(IngredientNode)
all_ingredients = DjangoFilterConnectionField(IngredientNode) all_ingredients = DjangoFilterConnectionField(IngredientNode)

View File

@ -2,5 +2,9 @@ from django.contrib import admin
from cookbook.recipes.models import Recipe, RecipeIngredient from cookbook.recipes.models import Recipe, RecipeIngredient
admin.site.register(Recipe) class RecipeIngredientInline(admin.TabularInline):
admin.site.register(RecipeIngredient) model = RecipeIngredient
@admin.register(Recipe)
class RecipeAdmin(admin.ModelAdmin):
inlines = [RecipeIngredientInline]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipes', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='recipeingredient',
old_name='recipes',
new_name='recipe',
),
migrations.AlterField(
model_name='recipeingredient',
name='unit',
field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20),
),
]

View File

@ -6,14 +6,15 @@ from cookbook.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
class RecipeIngredient(models.Model): class RecipeIngredient(models.Model):
recipes = models.ForeignKey(Recipe, related_name='amounts') recipe = models.ForeignKey(Recipe, related_name='amounts')
ingredient = models.ForeignKey(Ingredient, related_name='used_by') ingredient = models.ForeignKey(Ingredient, related_name='used_by')
amount = models.FloatField() amount = models.FloatField()
unit = models.CharField(max_length=20, choices=( unit = models.CharField(max_length=20, choices=(
('unit', 'Units'),
('kg', 'Kilograms'), ('kg', 'Kilograms'),
('l', 'Litres'), ('l', 'Litres'),
('', 'Units'), ('st', 'Shots'),
)) ))

View File

@ -0,0 +1,32 @@
from cookbook.recipes.models import Recipe, RecipeIngredient
from graphene import AbstractType, Node
from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType
class RecipeNode(DjangoObjectType):
class Meta:
model = Recipe
interfaces = (Node, )
filter_fields = ['title','amounts']
filter_order_by = ['title']
class RecipeIngredientNode(DjangoObjectType):
class Meta:
model = RecipeIngredient
# Allow for some more advanced filtering here
interfaces = (Node, )
filter_fields = {
'ingredient__name': ['exact', 'icontains', 'istartswith'],
'recipe': ['exact'],
'recipe__title': ['icontains'],
}
filter_order_by = ['ingredient__name', 'recipe__title',]
class Query(AbstractType):
recipe = Node.Field(RecipeNode)
all_recipes = DjangoFilterConnectionField(RecipeNode)
recipeingredient = Node.Field(RecipeIngredientNode)
all_recipeingredients = DjangoFilterConnectionField(RecipeIngredientNode)

View File

@ -1,10 +1,11 @@
import cookbook.ingredients.schema import cookbook.ingredients.schema
import cookbook.recipes.schema
import graphene import graphene
from graphene_django.debug import DjangoDebug from graphene_django.debug import DjangoDebug
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType): class Query(cookbook.recipes.schema.Query, cookbook.ingredients.schema.Query, graphene.ObjectType):
debug = graphene.Field(DjangoDebug, name='__debug') debug = graphene.Field(DjangoDebug, name='__debug')

View File

@ -0,0 +1 @@
[{"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=", "last_login": "2016-11-04T00:46:58Z", "is_superuser": true, "username": "admin", "first_name": "", "last_name": "", "email": "asdf@example.com", "is_staff": true, "is_active": true, "date_joined": "2016-11-03T18:24:40Z", "groups": [], "user_permissions": []}}, {"model": "recipes.recipe", "pk": 1, "fields": {"title": "Cheerios With a Shot of Vermouth", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 2, "fields": {"title": "Quail Eggs in Whipped Cream and MSG", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 3, "fields": {"title": "Deep Fried Skittles", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 4, "fields": {"title": "Newt ala Doritos", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 5, "fields": {"title": "Fruit Salad", "instructions": "Chop up and add together"}}, {"model": "recipes.recipeingredient", "pk": 1, "fields": {"recipes": 5, "ingredient": 9, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 2, "fields": {"recipes": 5, "ingredient": 10, "amount": 2.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 3, "fields": {"recipes": 5, "ingredient": 7, "amount": 3.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 4, "fields": {"recipes": 5, "ingredient": 8, "amount": 4.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 5, "fields": {"recipes": 4, "ingredient": 5, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 6, "fields": {"recipes": 4, "ingredient": 6, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 7, "fields": {"recipes": 3, "ingredient": 4, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 8, "fields": {"recipes": 2, "ingredient": 2, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 9, "fields": {"recipes": 2, "ingredient": 11, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 10, "fields": {"recipes": 2, "ingredient": 12, "amount": 3.0, "unit": "st"}}, {"model": "recipes.recipeingredient", "pk": 11, "fields": {"recipes": 1, "ingredient": 1, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 12, "fields": {"recipes": 1, "ingredient": 3, "amount": 1.0, "unit": "st"}}, {"model": "ingredients.category", "pk": 1, "fields": {"name": "fruit"}}, {"model": "ingredients.category", "pk": 3, "fields": {"name": "xkcd"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Cheerios", "notes": "this is a note", "category": 3}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Quail Eggs", "notes": "has more notes", "category": 3}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Vermouth", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Skittles", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 5, "fields": {"name": "Newt", "notes": "Braised and Confuesd", "category": 3}}, {"model": "ingredients.ingredient", "pk": 6, "fields": {"name": "Doritos", "notes": "Crushed", "category": 3}}, {"model": "ingredients.ingredient", "pk": 7, "fields": {"name": "Apple", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 8, "fields": {"name": "Orange", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 9, "fields": {"name": "Banana", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 10, "fields": {"name": "Grapes", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 11, "fields": {"name": "Whipped Cream", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 12, "fields": {"name": "MSG", "notes": "", "category": 3}}]

View File

@ -112,7 +112,7 @@ add "&raw" to the end of the URL within a browser.
{% if variables %} {% if variables %}
variables: '{{ variables|escapejs }}', variables: '{{ variables|escapejs }}',
{% endif %} {% endif %}
{% if operationName %} {% if operation_name %}
operationName: '{{ operation_name|escapejs }}', operationName: '{{ operation_name|escapejs }}',
{% endif %} {% endif %}
}), }),

View File

@ -38,6 +38,7 @@ class Article(models.Model):
headline = models.CharField(max_length=100) headline = models.CharField(max_length=100)
pub_date = models.DateField() pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name='articles') reporter = models.ForeignKey(Reporter, related_name='articles')
editor = models.ForeignKey(Reporter, related_name='edited_articles_+')
lang = models.CharField(max_length=2, help_text='Language', choices=[ lang = models.CharField(max_length=2, help_text='Language', choices=[
('es', 'Spanish'), ('es', 'Spanish'),
('en', 'English') ('en', 'English')

View File

@ -52,7 +52,7 @@ def test_django_objecttype_map_correct_fields():
def test_django_objecttype_with_node_have_correct_fields(): def test_django_objecttype_with_node_have_correct_fields():
fields = Article._meta.fields fields = Article._meta.fields
assert list(fields.keys()) == ['id', 'headline', 'pub_date', 'reporter', 'lang', 'importance'] assert list(fields.keys()) == ['id', 'headline', 'pub_date', 'reporter', 'editor', 'lang', 'importance']
def test_schema_representation(): def test_schema_representation():
@ -66,6 +66,7 @@ type Article implements Node {
headline: String! headline: String!
pubDate: DateTime! pubDate: DateTime!
reporter: Reporter! reporter: Reporter!
editor: Reporter!
lang: ArticleLang! lang: ArticleLang!
importance: ArticleImportance importance: ArticleImportance
} }

View File

@ -8,20 +8,23 @@ except ImportError:
from urllib.parse import urlencode from urllib.parse import urlencode
def url_string(**url_params): def url_string(string='/graphql', **url_params):
string = '/graphql'
if url_params: if url_params:
string += '?' + urlencode(url_params) string += '?' + urlencode(url_params)
return string return string
def batch_url_string(**url_params):
return url_string('/graphql/batch', **url_params)
def response_json(response): def response_json(response):
return json.loads(response.content.decode()) return json.loads(response.content.decode())
j = lambda **kwargs: json.dumps(kwargs) j = lambda **kwargs: json.dumps(kwargs)
jl = lambda **kwargs: json.dumps([kwargs])
def test_graphiql_is_enabled(client): def test_graphiql_is_enabled(client):
@ -169,6 +172,17 @@ def test_allows_post_with_json_encoding(client):
} }
def test_batch_allows_post_with_json_encoding(client):
response = client.post(batch_url_string(), jl(id=1, query='{test}'), 'application/json')
assert response.status_code == 200
assert response_json(response) == [{
'id': 1,
'payload': { 'data': {'test': "Hello World"} },
'status': 200,
}]
def test_allows_sending_a_mutation_via_post(client): def test_allows_sending_a_mutation_via_post(client):
response = client.post(url_string(), j(query='mutation TestMutation { writeTest { test } }'), 'application/json') response = client.post(url_string(), j(query='mutation TestMutation { writeTest { test } }'), 'application/json')
@ -199,6 +213,22 @@ def test_supports_post_json_query_with_string_variables(client):
} }
def test_batch_supports_post_json_query_with_string_variables(client):
response = client.post(batch_url_string(), jl(
id=1,
query='query helloWho($who: String){ test(who: $who) }',
variables=json.dumps({'who': "Dolly"})
), 'application/json')
assert response.status_code == 200
assert response_json(response) == [{
'id': 1,
'payload': { 'data': {'test': "Hello Dolly"} },
'status': 200,
}]
def test_supports_post_json_query_with_json_variables(client): def test_supports_post_json_query_with_json_variables(client):
response = client.post(url_string(), j( response = client.post(url_string(), j(
query='query helloWho($who: String){ test(who: $who) }', query='query helloWho($who: String){ test(who: $who) }',
@ -211,6 +241,21 @@ def test_supports_post_json_query_with_json_variables(client):
} }
def test_batch_supports_post_json_query_with_json_variables(client):
response = client.post(batch_url_string(), jl(
id=1,
query='query helloWho($who: String){ test(who: $who) }',
variables={'who': "Dolly"}
), 'application/json')
assert response.status_code == 200
assert response_json(response) == [{
'id': 1,
'payload': { 'data': {'test': "Hello Dolly"} },
'status': 200,
}]
def test_supports_post_url_encoded_query_with_string_variables(client): def test_supports_post_url_encoded_query_with_string_variables(client):
response = client.post(url_string(), urlencode(dict( response = client.post(url_string(), urlencode(dict(
query='query helloWho($who: String){ test(who: $who) }', query='query helloWho($who: String){ test(who: $who) }',
@ -285,6 +330,33 @@ def test_allows_post_with_operation_name(client):
} }
def test_batch_allows_post_with_operation_name(client):
response = client.post(batch_url_string(), jl(
id=1,
query='''
query helloYou { test(who: "You"), ...shared }
query helloWorld { test(who: "World"), ...shared }
query helloDolly { test(who: "Dolly"), ...shared }
fragment shared on QueryRoot {
shared: test(who: "Everyone")
}
''',
operationName='helloWorld'
), 'application/json')
assert response.status_code == 200
assert response_json(response) == [{
'id': 1,
'payload': {
'data': {
'test': 'Hello World',
'shared': 'Hello Everyone'
}
},
'status': 200,
}]
def test_allows_post_with_get_operation_name(client): def test_allows_post_with_get_operation_name(client):
response = client.post(url_string( response = client.post(url_string(
operationName='helloWorld' operationName='helloWorld'

View File

@ -3,5 +3,6 @@ from django.conf.urls import url
from ..views import GraphQLView from ..views import GraphQLView
urlpatterns = [ urlpatterns = [
url(r'^graphql/batch', GraphQLView.as_view(batch=True)),
url(r'^graphql', GraphQLView.as_view(graphiql=True)), url(r'^graphql', GraphQLView.as_view(graphiql=True)),
] ]

View File

@ -26,9 +26,12 @@ def construct_fields(options):
is_not_in_only = only_fields and name not in options.only_fields is_not_in_only = only_fields and name not in options.only_fields
is_already_created = name in options.fields is_already_created = name in options.fields
is_excluded = name in exclude_fields or is_already_created is_excluded = name in exclude_fields or is_already_created
if is_not_in_only or is_excluded: # https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.ForeignKey.related_query_name
is_no_backref = str(name).endswith('+')
if is_not_in_only or is_excluded or is_no_backref:
# We skip this field if we specify only_fields and is not # We skip this field if we specify only_fields and is not
# in there. Or when we exclude this field in exclude_fields # in there. Or when we exclude this field in exclude_fields.
# Or when there is no back reference.
continue continue
converted = convert_django_field_with_choices(field, options.registry) converted = convert_django_field_with_choices(field, options.registry)
if not converted: if not converted:

View File

@ -62,8 +62,10 @@ class GraphQLView(View):
middleware = None middleware = None
root_value = None root_value = None
pretty = False pretty = False
batch = False
def __init__(self, schema=None, executor=None, middleware=None, root_value=None, graphiql=False, pretty=False): def __init__(self, schema=None, executor=None, middleware=None, root_value=None, graphiql=False, pretty=False,
batch=False):
if not schema: if not schema:
schema = graphene_settings.SCHEMA schema = graphene_settings.SCHEMA
@ -77,8 +79,10 @@ class GraphQLView(View):
self.root_value = root_value self.root_value = root_value
self.pretty = pretty self.pretty = pretty
self.graphiql = graphiql self.graphiql = graphiql
self.batch = batch
assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.' assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
assert not all((graphiql, batch)), 'Use either graphiql or batch processing'
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def get_root_value(self, request): def get_root_value(self, request):
@ -99,39 +103,20 @@ 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)
query, variables, operation_name = self.get_graphql_params(request, data) if self.batch:
responses = [self.get_response(request, entry) for entry in data]
execution_result = self.execute_graphql_request( result = '[{}]'.format(','.join([response[0] for response in responses]))
request, status_code = max(responses, key=lambda response: response[1])[1]
data,
query,
variables,
operation_name,
show_graphiql
)
if execution_result:
response = {}
if execution_result.errors:
response['errors'] = [self.format_error(e) for e in execution_result.errors]
if execution_result.invalid:
status_code = 400
else: else:
status_code = 200 result, status_code = self.get_response(request, data, show_graphiql)
response['data'] = execution_result.data
result = self.json_encode(request, response, pretty=show_graphiql)
else:
result = None
if show_graphiql: if show_graphiql:
query, variables, operation_name, id = self.get_graphql_params(request, data)
return self.render_graphiql( return self.render_graphiql(
request, request,
graphiql_version=self.graphiql_version, graphiql_version=self.graphiql_version,
query=query or '', query=query or '',
variables=variables or '', variables=json.dumps(variables) or '',
operation_name=operation_name or '', operation_name=operation_name or '',
result=result or '' result=result or ''
) )
@ -150,6 +135,43 @@ class GraphQLView(View):
}) })
return response return response
def get_response(self, request, data, show_graphiql=False):
query, variables, operation_name, id = self.get_graphql_params(request, data)
execution_result = self.execute_graphql_request(
request,
data,
query,
variables,
operation_name,
show_graphiql
)
status_code = 200
if execution_result:
response = {}
if execution_result.errors:
response['errors'] = [self.format_error(e) for e in execution_result.errors]
if execution_result.invalid:
status_code = 400
else:
response['data'] = execution_result.data
if self.batch:
response = {
'id': id,
'payload': response,
'status': status_code,
}
result = self.json_encode(request, response, pretty=show_graphiql)
else:
result = None
return result, status_code
def render_graphiql(self, request, **data): def render_graphiql(self, request, **data):
return render(request, self.graphiql_template, data) return render(request, self.graphiql_template, data)
@ -170,6 +192,9 @@ class GraphQLView(View):
elif content_type == 'application/json': elif content_type == 'application/json':
try: try:
request_json = json.loads(request.body.decode('utf-8')) request_json = json.loads(request.body.decode('utf-8'))
if self.batch:
assert isinstance(request_json, list)
else:
assert isinstance(request_json, dict) assert isinstance(request_json, dict)
return request_json return request_json
except: except:
@ -242,6 +267,7 @@ class GraphQLView(View):
def get_graphql_params(request, data): def get_graphql_params(request, data):
query = request.GET.get('query') or data.get('query') query = request.GET.get('query') or data.get('query')
variables = request.GET.get('variables') or data.get('variables') variables = request.GET.get('variables') or data.get('variables')
id = request.GET.get('id') or data.get('id')
if variables and isinstance(variables, six.text_type): if variables and isinstance(variables, six.text_type):
try: try:
@ -251,7 +277,7 @@ class GraphQLView(View):
operation_name = request.GET.get('operationName') or data.get('operationName') operation_name = request.GET.get('operationName') or data.get('operationName')
return query, variables, operation_name return query, variables, operation_name, id
@staticmethod @staticmethod
def format_error(error): def format_error(error):

View File

@ -1,3 +1,6 @@
[aliases]
test=pytest
[tool:pytest] [tool:pytest]
DJANGO_SETTINGS_MODULE = django_test_settings DJANGO_SETTINGS_MODULE = django_test_settings

View File

@ -38,6 +38,9 @@ setup(
'iso8601', 'iso8601',
'singledispatch>=3.4.0.3', 'singledispatch>=3.4.0.3',
], ],
setup_requires=[
'pytest-runner',
],
tests_require=[ tests_require=[
'django-filter>=0.10.0', 'django-filter>=0.10.0',
'pytest', 'pytest',