mirror of
				https://github.com/graphql-python/graphene-django.git
				synced 2025-10-31 16:07:36 +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: |  | ||||||
|                     status_code = 200 |  | ||||||
|                     response['data'] = execution_result.data |  | ||||||
| 
 |  | ||||||
|                 result = self.json_encode(request, response, pretty=show_graphiql) |  | ||||||
|             else: |             else: | ||||||
|                 result = None |                 result, status_code = self.get_response(request, data, show_graphiql) | ||||||
| 
 | 
 | ||||||
|             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,7 +192,10 @@ 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')) | ||||||
|                 assert isinstance(request_json, dict) |                 if self.batch: | ||||||
|  |                     assert isinstance(request_json, list) | ||||||
|  |                 else: | ||||||
|  |                     assert isinstance(request_json, dict) | ||||||
|                 return request_json |                 return request_json | ||||||
|             except: |             except: | ||||||
|                 raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.')) |                 raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.')) | ||||||
|  | @ -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