mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-06-11 09:03:12 +03:00
Merge branch 'master' into recursive-nodes
This commit is contained in:
commit
73f4a92b4f
19
README.md
19
README.md
|
@ -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
|
||||||
|
```
|
||||||
|
|
20
README.rst
20
README.rst
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
))
|
))
|
||||||
|
|
32
examples/cookbook/cookbook/recipes/schema.py
Normal file
32
examples/cookbook/cookbook/recipes/schema.py
Normal 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)
|
|
@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|
1
examples/cookbook/dummy_data.json
Normal file
1
examples/cookbook/dummy_data.json
Normal 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}}]
|
|
@ -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 %}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
[aliases]
|
||||||
|
test=pytest
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
DJANGO_SETTINGS_MODULE = django_test_settings
|
DJANGO_SETTINGS_MODULE = django_test_settings
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user