mirror of
				https://github.com/graphql-python/graphene-django.git
				synced 2025-10-31 07:57:31 +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 | ||||
| from graphene_django import DjangoObjectType | ||||
| import graphene | ||||
| 
 | ||||
| class User(DjangoObjectType): | ||||
|     class Meta: | ||||
|  | @ -106,3 +107,21 @@ After developing, the full test suite can be evaluated by running: | |||
| ```sh | ||||
| 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 | ||||
| 
 | ||||
|     from graphene_django import DjangoObjectType | ||||
|     import graphene | ||||
| 
 | ||||
|     class User(DjangoObjectType): | ||||
|         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 | ||||
| 
 | ||||
| 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 | ||||
| .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master | ||||
|    :target: https://travis-ci.org/graphql-python/graphene-django | ||||
|  |  | |||
|  | @ -1,2 +1,3 @@ | |||
| sphinx | ||||
| # Docs template | ||||
| 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' | ||||
|     } | ||||
| 
 | ||||
| Alternatively, we can specify the schema to be used in the urls definition, | ||||
| as explained below. | ||||
| 
 | ||||
| Creating GraphQL and GraphiQL views | ||||
| ----------------------------------- | ||||
|  | @ -199,6 +201,22 @@ view. | |||
| This view will serve as GraphQL endpoint. As we want to have the | ||||
| 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 | ||||
| 
 | ||||
|     from django.conf.urls import url, include | ||||
|  | @ -210,7 +228,7 @@ aforementioned GraphiQL we specify that on the params with ``graphiql=True``. | |||
| 
 | ||||
|     urlpatterns = [ | ||||
|         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 | ||||
|  |  | |||
|  | @ -60,5 +60,5 @@ Now you should be ready to start the server: | |||
| Now head on over to | ||||
| [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) | ||||
| 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) | ||||
|  |  | |||
|  | @ -2,5 +2,9 @@ from django.contrib import admin | |||
| 
 | ||||
| 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) | ||||
|  |  | |||
|  | @ -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): | ||||
|     name = models.CharField(max_length=100) | ||||
|     notes = models.TextField() | ||||
|     notes = models.TextField(null=True,blank=True) | ||||
|     category = models.ForeignKey(Category, related_name='ingredients') | ||||
| 
 | ||||
|     def __str__(self): | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| 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.types import DjangoObjectType | ||||
| 
 | ||||
|  | @ -31,8 +31,8 @@ class IngredientNode(DjangoObjectType): | |||
| 
 | ||||
| 
 | ||||
| class Query(AbstractType): | ||||
|     category = Field(CategoryNode) | ||||
|     category = Node.Field(CategoryNode) | ||||
|     all_categories = DjangoFilterConnectionField(CategoryNode) | ||||
| 
 | ||||
|     ingredient = Field(IngredientNode) | ||||
|     ingredient = Node.Field(IngredientNode) | ||||
|     all_ingredients = DjangoFilterConnectionField(IngredientNode) | ||||
|  |  | |||
|  | @ -2,5 +2,9 @@ from django.contrib import admin | |||
| 
 | ||||
| from cookbook.recipes.models import Recipe, RecipeIngredient | ||||
| 
 | ||||
| admin.site.register(Recipe) | ||||
| admin.site.register(RecipeIngredient) | ||||
| class RecipeIngredientInline(admin.TabularInline): | ||||
|   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): | ||||
|     title = models.CharField(max_length=100) | ||||
|     instructions = models.TextField() | ||||
| 
 | ||||
|     __unicode__ = lambda self: self.title | ||||
| 
 | ||||
| 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') | ||||
|     amount = models.FloatField() | ||||
|     unit = models.CharField(max_length=20, choices=( | ||||
|         ('unit', 'Units'), | ||||
|         ('kg', 'Kilograms'), | ||||
|         ('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.recipes.schema | ||||
| import graphene | ||||
| 
 | ||||
| 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') | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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 %} | ||||
|         variables: '{{ variables|escapejs }}', | ||||
|         {% endif %} | ||||
|         {% if operationName %} | ||||
|         {% if operation_name %} | ||||
|         operationName: '{{ operation_name|escapejs }}', | ||||
|         {% endif %} | ||||
|       }), | ||||
|  |  | |||
|  | @ -38,6 +38,7 @@ class Article(models.Model): | |||
|     headline = models.CharField(max_length=100) | ||||
|     pub_date = models.DateField() | ||||
|     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=[ | ||||
|         ('es', 'Spanish'), | ||||
|         ('en', 'English') | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ def test_django_objecttype_map_correct_fields(): | |||
| 
 | ||||
| def test_django_objecttype_with_node_have_correct_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(): | ||||
|  | @ -66,6 +66,7 @@ type Article implements Node { | |||
|   headline: String! | ||||
|   pubDate: DateTime! | ||||
|   reporter: Reporter! | ||||
|   editor: Reporter! | ||||
|   lang: ArticleLang! | ||||
|   importance: ArticleImportance | ||||
| } | ||||
|  |  | |||
|  | @ -8,20 +8,23 @@ except ImportError: | |||
|     from urllib.parse import urlencode | ||||
| 
 | ||||
| 
 | ||||
| def url_string(**url_params): | ||||
|     string = '/graphql' | ||||
| 
 | ||||
| def url_string(string='/graphql', **url_params): | ||||
|     if url_params: | ||||
|         string += '?' + urlencode(url_params) | ||||
| 
 | ||||
|     return string | ||||
| 
 | ||||
| 
 | ||||
| def batch_url_string(**url_params): | ||||
|     return url_string('/graphql/batch', **url_params) | ||||
| 
 | ||||
| 
 | ||||
| def response_json(response): | ||||
|     return json.loads(response.content.decode()) | ||||
| 
 | ||||
| 
 | ||||
| j = lambda **kwargs: json.dumps(kwargs) | ||||
| jl = lambda **kwargs: json.dumps([kwargs]) | ||||
| 
 | ||||
| 
 | ||||
| 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): | ||||
|     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): | ||||
|     response = client.post(url_string(), j( | ||||
|         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): | ||||
|     response = client.post(url_string(), urlencode(dict( | ||||
|         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): | ||||
|     response = client.post(url_string( | ||||
|         operationName='helloWorld' | ||||
|  |  | |||
|  | @ -3,5 +3,6 @@ from django.conf.urls import url | |||
| from ..views import GraphQLView | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     url(r'^graphql/batch', GraphQLView.as_view(batch=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_already_created = name in options.fields | ||||
|         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 | ||||
|             # 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 | ||||
|         converted = convert_django_field_with_choices(field, options.registry) | ||||
|         if not converted: | ||||
|  |  | |||
|  | @ -62,8 +62,10 @@ class GraphQLView(View): | |||
|     middleware = None | ||||
|     root_value = None | ||||
|     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: | ||||
|             schema = graphene_settings.SCHEMA | ||||
| 
 | ||||
|  | @ -77,8 +79,10 @@ class GraphQLView(View): | |||
|         self.root_value = root_value | ||||
|         self.pretty = pretty | ||||
|         self.graphiql = graphiql | ||||
|         self.batch = batch | ||||
| 
 | ||||
|         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 | ||||
|     def get_root_value(self, request): | ||||
|  | @ -99,39 +103,20 @@ class GraphQLView(View): | |||
|             data = self.parse_body(request) | ||||
|             show_graphiql = self.graphiql and self.can_display_graphiql(request, data) | ||||
| 
 | ||||
|             query, variables, operation_name = self.get_graphql_params(request, data) | ||||
| 
 | ||||
|             execution_result = self.execute_graphql_request( | ||||
|                 request, | ||||
|                 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 | ||||
|             if self.batch: | ||||
|                 responses = [self.get_response(request, entry) for entry in data] | ||||
|                 result = '[{}]'.format(','.join([response[0] for response in responses])) | ||||
|                 status_code = max(responses, key=lambda response: response[1])[1] | ||||
|             else: | ||||
|                     status_code = 200 | ||||
|                     response['data'] = execution_result.data | ||||
| 
 | ||||
|                 result = self.json_encode(request, response, pretty=show_graphiql) | ||||
|             else: | ||||
|                 result = None | ||||
|                 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=variables or '', | ||||
|                     variables=json.dumps(variables) or '', | ||||
|                     operation_name=operation_name or '', | ||||
|                     result=result or '' | ||||
|                 ) | ||||
|  | @ -150,6 +135,43 @@ class GraphQLView(View): | |||
|             }) | ||||
|             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): | ||||
|         return render(request, self.graphiql_template, data) | ||||
| 
 | ||||
|  | @ -170,6 +192,9 @@ class GraphQLView(View): | |||
|         elif content_type == 'application/json': | ||||
|             try: | ||||
|                 request_json = json.loads(request.body.decode('utf-8')) | ||||
|                 if self.batch: | ||||
|                     assert isinstance(request_json, list) | ||||
|                 else: | ||||
|                     assert isinstance(request_json, dict) | ||||
|                 return request_json | ||||
|             except: | ||||
|  | @ -242,6 +267,7 @@ class GraphQLView(View): | |||
|     def get_graphql_params(request, data): | ||||
|         query = request.GET.get('query') or data.get('query') | ||||
|         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): | ||||
|             try: | ||||
|  | @ -251,7 +277,7 @@ class GraphQLView(View): | |||
| 
 | ||||
|         operation_name = request.GET.get('operationName') or data.get('operationName') | ||||
| 
 | ||||
|         return query, variables, operation_name | ||||
|         return query, variables, operation_name, id | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def format_error(error): | ||||
|  |  | |||
|  | @ -1,3 +1,6 @@ | |||
| [aliases] | ||||
| test=pytest | ||||
| 
 | ||||
| [tool:pytest] | ||||
| DJANGO_SETTINGS_MODULE = django_test_settings | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user