mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-02-16 19:40:36 +03:00
Merge remote-tracking branch 'remote/master' into recursive-nodes
This commit is contained in:
commit
57dbcd34f4
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -65,3 +65,16 @@ target/
|
||||||
# Databases
|
# Databases
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
# swap
|
||||||
|
[._]*.s[a-v][a-z]
|
||||||
|
[._]*.sw[a-p]
|
||||||
|
[._]s[a-v][a-z]
|
||||||
|
[._]sw[a-p]
|
||||||
|
# session
|
||||||
|
Session.vim
|
||||||
|
# temporary
|
||||||
|
.netrwhist
|
||||||
|
*~
|
||||||
|
# auto-generated tag files
|
||||||
|
tags
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Authorization in Django
|
Authorization in Django
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
There are two main ways you may want to limit access to data when
|
There are several ways you may want to limit access to data when
|
||||||
working with Graphene and Django: limiting which fields are accessible
|
working with Graphene and Django: limiting which fields are accessible
|
||||||
via GraphQL and limiting which objects a user can access.
|
via GraphQL and limiting which objects a user can access.
|
||||||
|
|
||||||
|
@ -34,6 +34,20 @@ This is easy, simply use the ``only_fields`` meta attribute.
|
||||||
only_fields = ('title', 'content')
|
only_fields = ('title', 'content')
|
||||||
interfaces = (relay.Node, )
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
|
conversely you can use ``exclude_fields`` meta atrribute.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene import relay
|
||||||
|
from graphene_django.types import DjangoObjectType
|
||||||
|
from .models import Post
|
||||||
|
|
||||||
|
class PostNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Post
|
||||||
|
exclude_fields = ('published', 'owner')
|
||||||
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
Queryset Filtering On Lists
|
Queryset Filtering On Lists
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
@ -108,3 +122,28 @@ method to your ``DjangoObjectType``.
|
||||||
if post.published or context.user == post.owner:
|
if post.published or context.user == post.owner:
|
||||||
return post
|
return post
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
Adding login required
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
If you want to use the standard Django LoginRequiredMixin_ you can create your own view, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
After this, you can use the new ``PrivateGraphQLView`` in ``urls.py``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# some other urls
|
||||||
|
url(r'^graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
|
||||||
|
]
|
||||||
|
|
||||||
|
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.10/topics/auth/default/#the-loginrequired-mixin
|
||||||
|
|
|
@ -63,7 +63,7 @@ master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'Graphene Django'
|
project = u'Graphene Django'
|
||||||
copyright = u'Graphene 2016'
|
copyright = u'Graphene 2017'
|
||||||
author = u'Syrus Akbary'
|
author = u'Syrus Akbary'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
|
|
@ -6,7 +6,8 @@ Contents:
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 0
|
:maxdepth: 0
|
||||||
|
|
||||||
tutorial
|
tutorial-plain
|
||||||
|
tutorial-relay
|
||||||
filtering
|
filtering
|
||||||
authorization
|
authorization
|
||||||
debug
|
debug
|
||||||
|
|
516
docs/tutorial-plain.rst
Normal file
516
docs/tutorial-plain.rst
Normal file
|
@ -0,0 +1,516 @@
|
||||||
|
Introduction tutorial - Graphene and Django
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
Graphene has a number of additional features that are designed to make
|
||||||
|
working with Django *really simple*.
|
||||||
|
|
||||||
|
Our primary focus here is to give a good understanding of how to connect models from Django ORM to graphene object types.
|
||||||
|
|
||||||
|
A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__ documentation first.
|
||||||
|
|
||||||
|
Setup the Django project
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
You can find the entire project in ``examples/cookbook-plain``.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
We will setup the project, create the following:
|
||||||
|
|
||||||
|
- A Django project called ``cookbook``
|
||||||
|
- An app within ``cookbook`` called ``ingredients``
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
# Create the project directory
|
||||||
|
mkdir cookbook
|
||||||
|
cd cookbook
|
||||||
|
|
||||||
|
# Create a virtualenv to isolate our package dependencies locally
|
||||||
|
virtualenv env
|
||||||
|
source env/bin/activate # On Windows use `env\Scripts\activate`
|
||||||
|
|
||||||
|
# Install Django and Graphene with Django support
|
||||||
|
pip install django
|
||||||
|
pip install graphene_django
|
||||||
|
|
||||||
|
# Set up a new project with a single application
|
||||||
|
django-admin.py startproject cookbook . # Note the trailing '.' character
|
||||||
|
cd cookbook
|
||||||
|
django-admin.py startapp ingredients
|
||||||
|
|
||||||
|
Now sync your database for the first time:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
Let's create a few simple models...
|
||||||
|
|
||||||
|
Defining our models
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Let's get started with these models:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# cookbook/ingredients/models.py
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Category(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Ingredient(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
notes = models.TextField()
|
||||||
|
category = models.ForeignKey(Category, related_name='ingredients')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
Don't forget to create & run migrations:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
Load some test data
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Now is a good time to load up some test data. The easiest option will be
|
||||||
|
to `download the
|
||||||
|
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
|
||||||
|
fixture and place it in
|
||||||
|
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
|
||||||
|
following:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
$ python ./manage.py loaddata ingredients
|
||||||
|
|
||||||
|
Installed 6 object(s) from 1 fixture(s)
|
||||||
|
|
||||||
|
Alternatively you can use the Django admin interface to create some data
|
||||||
|
yourself. You'll need to run the development server (see below), and
|
||||||
|
create a login for yourself too (``./manage.py createsuperuser``).
|
||||||
|
|
||||||
|
Hello GraphQL - Schema and Object Types
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
In order to make queries to our Django project, we are going to need few things:
|
||||||
|
|
||||||
|
* Schema with defined object types
|
||||||
|
* A view, taking queries as input and returning the result
|
||||||
|
|
||||||
|
GraphQL presents your objects to the world as a graph structure rather
|
||||||
|
than a more hierarchical structure to which you may be accustomed. In
|
||||||
|
order to create this representation, Graphene needs to know about each
|
||||||
|
*type* of object which will appear in the graph.
|
||||||
|
|
||||||
|
This graph also has a *root type* through which all access begins. This
|
||||||
|
is the ``Query`` class below.
|
||||||
|
|
||||||
|
This means, for each of our models, we are going to create a type, subclassing ``DjangoObjectType``
|
||||||
|
|
||||||
|
After we've done that, we will list those types as fields in the ``Query`` class.
|
||||||
|
|
||||||
|
Create ``cookbook/ingredients/schema.py`` and type the following:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# cookbook/ingredients/schema.py
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class Query(graphene.AbstractType):
|
||||||
|
all_categories = graphene.List(CategoryType)
|
||||||
|
all_ingredients = graphene.List(IngredientType)
|
||||||
|
|
||||||
|
def resolve_all_categories(self, args, context, info):
|
||||||
|
return Category.objects.all()
|
||||||
|
|
||||||
|
def resolve_all_ingredients(self, args, context, info):
|
||||||
|
# We can easily optimize query count in the resolve method
|
||||||
|
return Ingredient.objects.select_related('category').all()
|
||||||
|
|
||||||
|
|
||||||
|
Note that the above ``Query`` class is marked as 'abstract'. This is
|
||||||
|
because we will now create a project-level query which will combine all
|
||||||
|
our app-level queries.
|
||||||
|
|
||||||
|
Create the parent project-level ``cookbook/schema.py``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
import cookbook.ingredients.schema
|
||||||
|
|
||||||
|
|
||||||
|
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType):
|
||||||
|
# This class will inherit from multiple Queries
|
||||||
|
# as we begin to add more apps to our project
|
||||||
|
pass
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
You can think of this as being something like your top-level ``urls.py``
|
||||||
|
file (although it currently lacks any namespacing).
|
||||||
|
|
||||||
|
Testing everything so far
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
We are going to do some configuration work, in order to have a working Django where we can test queries, before we move on, updating our schema.
|
||||||
|
|
||||||
|
Update settings
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Next, install your app and GraphiQL in your Django project. GraphiQL is
|
||||||
|
a web-based integrated development environment to assist in the writing
|
||||||
|
and executing of GraphQL queries. It will provide us with a simple and
|
||||||
|
easy way of testing our cookbook project.
|
||||||
|
|
||||||
|
Add ``ingredients`` and ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
...
|
||||||
|
# This will also make the `graphql_schema` management command available
|
||||||
|
'graphene_django',
|
||||||
|
|
||||||
|
# Install the ingredients app
|
||||||
|
'ingredients',
|
||||||
|
]
|
||||||
|
|
||||||
|
And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'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
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Unlike a RESTful API, there is only a single URL from which GraphQL is
|
||||||
|
accessed. Requests to this URL are handled by Graphene's ``GraphQLView``
|
||||||
|
view.
|
||||||
|
|
||||||
|
This view will serve as GraphQL endpoint. As we want to have the
|
||||||
|
aforementioned GraphiQL we specify that on the parameters 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
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
from cookbook.schema import schema
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^admin/', admin.site.urls),
|
||||||
|
url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Testing our GraphQL schema
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
We're now ready to test the API we've built. Let's fire up the server
|
||||||
|
from the command line.
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
$ python ./manage.py runserver
|
||||||
|
|
||||||
|
Performing system checks...
|
||||||
|
Django version 1.9, using settings 'cookbook.settings'
|
||||||
|
Starting development server at http://127.0.0.1:8000/
|
||||||
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
Go to `localhost:8000/graphql <http://localhost:8000/graphql>`__ and
|
||||||
|
type your first query!
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
query {
|
||||||
|
allIngredients {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
If you are using the provided fixtures, you will see the following response:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"allIngredients": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Eggs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "Milk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"name": "Beef"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"name": "Chicken"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
You can experiment with ``allCategories`` too.
|
||||||
|
|
||||||
|
Something to have in mind is the `auto camelcasing <http://docs.graphene-python.org/en/latest/types/schema/#auto-camelcase-field-names>`__ that is happening.
|
||||||
|
|
||||||
|
|
||||||
|
Getting relations
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Right now, with this simple setup in place, we can query for relations too. This is where graphql becomes really powerful!
|
||||||
|
|
||||||
|
For example, we may want to list all categories and in each category, all ingredients that are in that category.
|
||||||
|
|
||||||
|
We can do that with the following query:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
query {
|
||||||
|
allCategories {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
ingredients {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
This will give you (in case you are using the fixtures) the following result:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"allCategories": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Dairy",
|
||||||
|
"ingredients": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Eggs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "Milk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "Meat",
|
||||||
|
"ingredients": [
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"name": "Beef"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"name": "Chicken"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
We can also list all ingredients and get information for the category they are in:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
query {
|
||||||
|
allIngredients {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
category {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Getting single objects
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
So far, we have been able to fetch list of objects and follow relation. But what about single objects?
|
||||||
|
|
||||||
|
We can update our schema to support that, by adding new query for ``ingredient`` and ``category`` and adding arguments, so we can query for specific objects.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class Query(graphene.AbstractType):
|
||||||
|
category = graphene.Field(CategoryType,
|
||||||
|
id=graphene.Int(),
|
||||||
|
name=graphene.String())
|
||||||
|
all_categories = graphene.List(CategoryType)
|
||||||
|
|
||||||
|
|
||||||
|
ingredient = graphene.Field(IngredientType,
|
||||||
|
id=graphene.Int(),
|
||||||
|
name=graphene.String())
|
||||||
|
all_ingredients = graphene.List(IngredientType)
|
||||||
|
|
||||||
|
def resolve_all_categories(self, args, context, info):
|
||||||
|
return Category.objects.all()
|
||||||
|
|
||||||
|
def resolve_all_ingredients(self, args, context, info):
|
||||||
|
return Ingredient.objects.all()
|
||||||
|
|
||||||
|
def resolve_category(self, args, context, info):
|
||||||
|
id = args.get('id')
|
||||||
|
name = args.get('name')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Category.objects.get(pk=id)
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
return Category.objects.get(name=name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_ingredient(self, args, context, info):
|
||||||
|
id = args.get('id')
|
||||||
|
name = args.get('name')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Ingredient.objects.get(pk=id)
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
return Ingredient.objects.get(name=name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
Now, with the code in place, we can query for single objects.
|
||||||
|
|
||||||
|
For example, lets query ``category``:
|
||||||
|
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
query {
|
||||||
|
category(id: 1) {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
anotherCategory: category(name: "Dairy") {
|
||||||
|
ingredients {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
This will give us the following results:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"category": {
|
||||||
|
"name": "Dairy"
|
||||||
|
},
|
||||||
|
"anotherCategory": {
|
||||||
|
"ingredients": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Eggs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "Milk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
As an exercise, you can try making some queries to ``ingredient``.
|
||||||
|
|
||||||
|
Something to keep in mind - since we are using one field several times in our query, we need `aliases <http://graphql.org/learn/queries/#aliases>`__
|
||||||
|
|
||||||
|
|
||||||
|
Summary
|
||||||
|
-------
|
||||||
|
|
||||||
|
As you can see, GraphQL is very powerful but there are a lot of repetitions in our example. We can do a lot of improvements by adding layers of abstraction on top of ``graphene-django``.
|
||||||
|
|
||||||
|
If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.**
|
|
@ -1,5 +1,5 @@
|
||||||
Graphene-Django Tutorial
|
Graphene and Django Tutorial using Relay
|
||||||
========================
|
========================================
|
||||||
|
|
||||||
Graphene has a number of additional features that are designed to make
|
Graphene has a number of additional features that are designed to make
|
||||||
working with Django *really simple*.
|
working with Django *really simple*.
|
||||||
|
@ -7,6 +7,11 @@ working with Django *really simple*.
|
||||||
Note: The code in this quickstart is pulled from the `cookbook example
|
Note: The code in this quickstart is pulled from the `cookbook example
|
||||||
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
|
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
|
||||||
|
|
||||||
|
A good idea is to check the following things first:
|
||||||
|
|
||||||
|
* `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__
|
||||||
|
* `GraphQL Relay Specification <https://facebook.github.io/relay/docs/graphql-relay-specification.html>`__
|
||||||
|
|
||||||
Setup the Django project
|
Setup the Django project
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
@ -43,7 +48,7 @@ Now sync your database for the first time:
|
||||||
Let's create a few simple models...
|
Let's create a few simple models...
|
||||||
|
|
||||||
Defining our models
|
Defining our models
|
||||||
-------------------
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Let's get started with these models:
|
Let's get started with these models:
|
||||||
|
|
||||||
|
@ -68,6 +73,33 @@ Let's get started with these models:
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
Don't forget to create & run migrations:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
Load some test data
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Now is a good time to load up some test data. The easiest option will be
|
||||||
|
to `download the
|
||||||
|
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
|
||||||
|
fixture and place it in
|
||||||
|
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
|
||||||
|
following:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
$ python ./manage.py loaddata ingredients
|
||||||
|
|
||||||
|
Installed 6 object(s) from 1 fixture(s)
|
||||||
|
|
||||||
|
Alternatively you can use the Django admin interface to create some data
|
||||||
|
yourself. You'll need to run the development server (see below), and
|
||||||
|
create a login for yourself too (``./manage.py createsuperuser``).
|
||||||
|
|
||||||
Schema
|
Schema
|
||||||
------
|
------
|
||||||
|
|
||||||
|
@ -90,7 +122,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
from cookbook.ingredients.models import Category, Ingredient
|
from ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
# Graphene will automatically map the Category model's fields onto the CategoryNode.
|
# Graphene will automatically map the Category model's fields onto the CategoryNode.
|
||||||
|
@ -145,10 +177,10 @@ Create the parent project-level ``cookbook/schema.py``:
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
import cookbook.ingredients.schema
|
import ingredients.schema
|
||||||
|
|
||||||
|
|
||||||
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType):
|
class Query(ingredients.schema.Query, graphene.ObjectType):
|
||||||
# This class will inherit from multiple Queries
|
# This class will inherit from multiple Queries
|
||||||
# as we begin to add more apps to our project
|
# as we begin to add more apps to our project
|
||||||
pass
|
pass
|
||||||
|
@ -158,8 +190,11 @@ Create the parent project-level ``cookbook/schema.py``:
|
||||||
You can think of this as being something like your top-level ``urls.py``
|
You can think of this as being something like your top-level ``urls.py``
|
||||||
file (although it currently lacks any namespacing).
|
file (although it currently lacks any namespacing).
|
||||||
|
|
||||||
|
Testing everything so far
|
||||||
|
-------------------------
|
||||||
|
|
||||||
Update settings
|
Update settings
|
||||||
---------------
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Next, install your app and GraphiQL in your Django project. GraphiQL is
|
Next, install your app and GraphiQL in your Django project. GraphiQL is
|
||||||
a web-based integrated development environment to assist in the writing
|
a web-based integrated development environment to assist in the writing
|
||||||
|
@ -191,7 +226,7 @@ Alternatively, we can specify the schema to be used in the urls definition,
|
||||||
as explained below.
|
as explained below.
|
||||||
|
|
||||||
Creating GraphQL and GraphiQL views
|
Creating GraphQL and GraphiQL views
|
||||||
-----------------------------------
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Unlike a RESTful API, there is only a single URL from which GraphQL is
|
Unlike a RESTful API, there is only a single URL from which GraphQL is
|
||||||
accessed. Requests to this URL are handled by Graphene's ``GraphQLView``
|
accessed. Requests to this URL are handled by Graphene's ``GraphQLView``
|
||||||
|
@ -230,39 +265,9 @@ as explained above, we can do so here using:
|
||||||
url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)),
|
url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)),
|
||||||
]
|
]
|
||||||
|
|
||||||
Apply model changes to database
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Tell Django that we've added models and update the database schema to
|
|
||||||
reflect these additions.
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
python manage.py makemigrations
|
|
||||||
python manage.py migrate
|
|
||||||
|
|
||||||
Load some test data
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
Now is a good time to load up some test data. The easiest option will be
|
|
||||||
to `download the
|
|
||||||
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
|
|
||||||
fixture and place it in
|
|
||||||
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
|
|
||||||
following:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
$ python ./manage.py loaddata ingredients
|
|
||||||
|
|
||||||
Installed 6 object(s) from 1 fixture(s)
|
|
||||||
|
|
||||||
Alternatively you can use the Django admin interface to create some data
|
|
||||||
yourself. You'll need to run the development server (see below), and
|
|
||||||
create a login for yourself too (``./manage.py createsuperuser``).
|
|
||||||
|
|
||||||
Testing our GraphQL schema
|
Testing our GraphQL schema
|
||||||
--------------------------
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
We're now ready to test the API we've built. Let's fire up the server
|
We're now ready to test the API we've built. Let's fire up the server
|
||||||
from the command line.
|
from the command line.
|
||||||
|
@ -276,7 +281,7 @@ from the command line.
|
||||||
Starting development server at http://127.0.0.1:8000/
|
Starting development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
Go to `localhost:8000/graphiql <http://localhost:8000/graphiql>`__ and
|
Go to `localhost:8000/graphql <http://localhost:8000/graphql>`__ and
|
||||||
type your first query!
|
type your first query!
|
||||||
|
|
||||||
.. code::
|
.. code::
|
64
examples/cookbook-plain/README.md
Normal file
64
examples/cookbook-plain/README.md
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
Cookbook Example Django Project
|
||||||
|
===============================
|
||||||
|
|
||||||
|
This example project demos integration between Graphene and Django.
|
||||||
|
The project contains two apps, one named `ingredients` and another
|
||||||
|
named `recepies`.
|
||||||
|
|
||||||
|
Getting started
|
||||||
|
---------------
|
||||||
|
|
||||||
|
First you'll need to get the source of the project. Do this by cloning the
|
||||||
|
whole Graphene repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get the example project code
|
||||||
|
git clone https://github.com/graphql-python/graphene-django.git
|
||||||
|
cd graphene-django/examples/cookbook
|
||||||
|
```
|
||||||
|
|
||||||
|
It is good idea (but not required) to create a virtual environment
|
||||||
|
for this project. We'll do this using
|
||||||
|
[virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/)
|
||||||
|
to keep things simple,
|
||||||
|
but you may also find something like
|
||||||
|
[virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/)
|
||||||
|
to be useful:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a virtualenv in which we can install the dependencies
|
||||||
|
virtualenv env
|
||||||
|
source env/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can install our dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Now setup our database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup the database
|
||||||
|
./manage.py migrate
|
||||||
|
|
||||||
|
# Load some example data
|
||||||
|
./manage.py loaddata ingredients
|
||||||
|
|
||||||
|
# Create an admin user (useful for logging into the admin UI
|
||||||
|
# at http://127.0.0.1:8000/admin)
|
||||||
|
./manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you should be ready to start the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
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 [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema)
|
||||||
|
for some example queries)
|
0
examples/cookbook-plain/cookbook/__init__.py
Normal file
0
examples/cookbook-plain/cookbook/__init__.py
Normal file
12
examples/cookbook-plain/cookbook/ingredients/admin.py
Normal file
12
examples/cookbook-plain/cookbook/ingredients/admin.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Ingredient)
|
||||||
|
class IngredientAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'name', 'category')
|
||||||
|
list_editable = ('name', 'category')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Category)
|
7
examples/cookbook-plain/cookbook/ingredients/apps.py
Normal file
7
examples/cookbook-plain/cookbook/ingredients/apps.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientsConfig(AppConfig):
|
||||||
|
name = 'cookbook.ingredients'
|
||||||
|
label = 'ingredients'
|
||||||
|
verbose_name = 'Ingredients'
|
|
@ -0,0 +1 @@
|
||||||
|
[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}]
|
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2015-12-04 18:15
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Ingredient',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('notes', models.TextField()),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.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),
|
||||||
|
),
|
||||||
|
]
|
17
examples/cookbook-plain/cookbook/ingredients/models.py
Normal file
17
examples/cookbook-plain/cookbook/ingredients/models.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Category(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Ingredient(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
notes = models.TextField(null=True, blank=True)
|
||||||
|
category = models.ForeignKey(Category, related_name='ingredients')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
57
examples/cookbook-plain/cookbook/ingredients/schema.py
Normal file
57
examples/cookbook-plain/cookbook/ingredients/schema.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import graphene
|
||||||
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class Query(graphene.AbstractType):
|
||||||
|
category = graphene.Field(CategoryType,
|
||||||
|
id=graphene.Int(),
|
||||||
|
name=graphene.String())
|
||||||
|
all_categories = graphene.List(CategoryType)
|
||||||
|
|
||||||
|
ingredient = graphene.Field(IngredientType,
|
||||||
|
id=graphene.Int(),
|
||||||
|
name=graphene.String())
|
||||||
|
all_ingredients = graphene.List(IngredientType)
|
||||||
|
|
||||||
|
def resolve_all_categories(self, args, context, info):
|
||||||
|
return Category.objects.all()
|
||||||
|
|
||||||
|
def resolve_all_ingredients(self, args, context, info):
|
||||||
|
# We can easily optimize query count in the resolve method
|
||||||
|
return Ingredient.objects.select_related('category').all()
|
||||||
|
|
||||||
|
def resolve_category(self, args, context, info):
|
||||||
|
id = args.get('id')
|
||||||
|
name = args.get('name')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Category.objects.get(pk=id)
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
return Category.objects.get(name=name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_ingredient(self, args, context, info):
|
||||||
|
id = args.get('id')
|
||||||
|
name = args.get('name')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Ingredient.objects.get(pk=id)
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
return Ingredient.objects.get(name=name)
|
||||||
|
|
||||||
|
return None
|
12
examples/cookbook-plain/cookbook/recipes/admin.py
Normal file
12
examples/cookbook-plain/cookbook/recipes/admin.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from cookbook.recipes.models import Recipe, RecipeIngredient
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredientInline(admin.TabularInline):
|
||||||
|
model = RecipeIngredient
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Recipe)
|
||||||
|
class RecipeAdmin(admin.ModelAdmin):
|
||||||
|
inlines = [RecipeIngredientInline]
|
7
examples/cookbook-plain/cookbook/recipes/apps.py
Normal file
7
examples/cookbook-plain/cookbook/recipes/apps.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RecipesConfig(AppConfig):
|
||||||
|
name = 'cookbook.recipes'
|
||||||
|
label = 'recipes'
|
||||||
|
verbose_name = 'Recipes'
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2015-12-04 18:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ingredients', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Recipe',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=100)),
|
||||||
|
('instructions', models.TextField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RecipeIngredient',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount', models.FloatField()),
|
||||||
|
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)),
|
||||||
|
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')),
|
||||||
|
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
20
examples/cookbook-plain/cookbook/recipes/models.py
Normal file
20
examples/cookbook-plain/cookbook/recipes/models.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from cookbook.ingredients.models import Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe(models.Model):
|
||||||
|
title = models.CharField(max_length=100)
|
||||||
|
instructions = models.TextField()
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredient(models.Model):
|
||||||
|
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'),
|
||||||
|
('st', 'Shots'),
|
||||||
|
))
|
52
examples/cookbook-plain/cookbook/recipes/schema.py
Normal file
52
examples/cookbook-plain/cookbook/recipes/schema.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import graphene
|
||||||
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from cookbook.recipes.models import Recipe, RecipeIngredient
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Recipe
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredientType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = RecipeIngredient
|
||||||
|
|
||||||
|
|
||||||
|
class Query(graphene.AbstractType):
|
||||||
|
recipe = graphene.Field(RecipeType,
|
||||||
|
id=graphene.Int(),
|
||||||
|
title=graphene.String())
|
||||||
|
all_recipes = graphene.List(RecipeType)
|
||||||
|
|
||||||
|
recipeingredient = graphene.Field(RecipeIngredientType,
|
||||||
|
id=graphene.Int())
|
||||||
|
all_recipeingredients = graphene.List(RecipeIngredientType)
|
||||||
|
|
||||||
|
def resolve_recipe(self, args, context, info):
|
||||||
|
id = args.get('id')
|
||||||
|
title = args.get('title')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Recipe.objects.get(pk=id)
|
||||||
|
|
||||||
|
if title is not None:
|
||||||
|
return Recipe.objects.get(title=title)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_recipeingredient(self, args, context, info):
|
||||||
|
id = args.get('id')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return RecipeIngredient.objects.get(pk=id)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_all_recipes(self, args, context, info):
|
||||||
|
return Recipe.objects.all()
|
||||||
|
|
||||||
|
def resolve_all_recipeingredients(self, args, context, info):
|
||||||
|
related = ['recipe', 'ingredient']
|
||||||
|
return RecipeIngredient.objects.select_related(*related).all()
|
14
examples/cookbook-plain/cookbook/schema.py
Normal file
14
examples/cookbook-plain/cookbook/schema.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import cookbook.ingredients.schema
|
||||||
|
import cookbook.recipes.schema
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from graphene_django.debug import DjangoDebug
|
||||||
|
|
||||||
|
|
||||||
|
class Query(cookbook.ingredients.schema.Query,
|
||||||
|
cookbook.recipes.schema.Query,
|
||||||
|
graphene.ObjectType):
|
||||||
|
debug = graphene.Field(DjangoDebug, name='__debug')
|
||||||
|
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
138
examples/cookbook-plain/cookbook/settings.py
Normal file
138
examples/cookbook-plain/cookbook/settings.py
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
# flake8: noqa
|
||||||
|
"""
|
||||||
|
Django settings for cookbook project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 1.9.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.9/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/1.9/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'graphene_django',
|
||||||
|
|
||||||
|
'cookbook.ingredients.apps.IngredientsConfig',
|
||||||
|
'cookbook.recipes.apps.RecipesConfig',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE_CLASSES = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'SCHEMA': 'cookbook.schema.schema',
|
||||||
|
'MIDDLEWARE': (
|
||||||
|
'graphene_django.debug.DjangoDebugMiddleware',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'cookbook.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'cookbook.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.9/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'SCHEMA': 'cookbook.schema.schema',
|
||||||
|
'SCHEMA_INDENT': 2,
|
||||||
|
}
|
10
examples/cookbook-plain/cookbook/urls.py
Normal file
10
examples/cookbook-plain/cookbook/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
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)),
|
||||||
|
]
|
16
examples/cookbook-plain/cookbook/wsgi.py
Normal file
16
examples/cookbook-plain/cookbook/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for cookbook project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
10
examples/cookbook-plain/manage.py
Executable file
10
examples/cookbook-plain/manage.py
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings")
|
||||||
|
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
5
examples/cookbook-plain/requirements.txt
Normal file
5
examples/cookbook-plain/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
graphene
|
||||||
|
graphene-django
|
||||||
|
django_graphiql
|
||||||
|
graphql-core
|
||||||
|
django==1.9
|
2
examples/cookbook-plain/setup.cfg
Normal file
2
examples/cookbook-plain/setup.cfg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[flake8]
|
||||||
|
exclude=migrations,.git,__pycache__
|
|
@ -3,7 +3,7 @@ Cookbook Example Django Project
|
||||||
|
|
||||||
This example project demos integration between Graphene and Django.
|
This example project demos integration between Graphene and Django.
|
||||||
The project contains two apps, one named `ingredients` and another
|
The project contains two apps, one named `ingredients` and another
|
||||||
named `recepies`.
|
named `recipes`.
|
||||||
|
|
||||||
Getting started
|
Getting started
|
||||||
---------------
|
---------------
|
||||||
|
|
|
@ -27,12 +27,16 @@ def convert_choice_name(name):
|
||||||
|
|
||||||
|
|
||||||
def get_choices(choices):
|
def get_choices(choices):
|
||||||
|
converted_names = []
|
||||||
for value, help_text in choices:
|
for value, help_text in choices:
|
||||||
if isinstance(help_text, (tuple, list)):
|
if isinstance(help_text, (tuple, list)):
|
||||||
for choice in get_choices(help_text):
|
for choice in get_choices(help_text):
|
||||||
yield choice
|
yield choice
|
||||||
else:
|
else:
|
||||||
name = convert_choice_name(value)
|
name = convert_choice_name(value)
|
||||||
|
while name in converted_names:
|
||||||
|
name += '_' + str(len(converted_names))
|
||||||
|
converted_names.append(name)
|
||||||
description = help_text
|
description = help_text
|
||||||
yield name, value, description
|
yield name, value, description
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ class DjangoConnectionField(ConnectionField):
|
||||||
iterable = default_manager
|
iterable = default_manager
|
||||||
iterable = maybe_queryset(iterable)
|
iterable = maybe_queryset(iterable)
|
||||||
if isinstance(iterable, QuerySet):
|
if isinstance(iterable, QuerySet):
|
||||||
|
iterable &= maybe_queryset(default_manager)
|
||||||
_len = iterable.count()
|
_len = iterable.count()
|
||||||
else:
|
else:
|
||||||
_len = len(iterable)
|
_len = len(iterable)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from functools import partial
|
||||||
|
|
||||||
from graphene.types.argument import to_arguments
|
from graphene.types.argument import to_arguments
|
||||||
from ..fields import DjangoConnectionField
|
from ..fields import DjangoConnectionField
|
||||||
|
from graphene.relay import is_node
|
||||||
from .utils import get_filtering_args_from_filterset, get_filterset_class
|
from .utils import get_filtering_args_from_filterset, get_filterset_class
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,7 +29,15 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def meta(self):
|
def meta(self):
|
||||||
meta = dict(model=self.node_type._meta.model,
|
if is_node(self.node_type):
|
||||||
|
_model = self.node_type._meta.model
|
||||||
|
else:
|
||||||
|
# ConnectionFields can also be passed Connections,
|
||||||
|
# in which case, we need to use the Node of the connection
|
||||||
|
# to get our relevant args.
|
||||||
|
_model = self.node_type._meta.node._meta.model
|
||||||
|
|
||||||
|
meta = dict(model=_model,
|
||||||
fields=self.fields)
|
fields=self.fields)
|
||||||
if self._extra_filter_meta:
|
if self._extra_filter_meta:
|
||||||
meta.update(self._extra_filter_meta)
|
meta.update(self._extra_filter_meta)
|
||||||
|
@ -36,7 +45,16 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fields(self):
|
def fields(self):
|
||||||
return self._fields or self.node_type._meta.filter_fields
|
if self._fields:
|
||||||
|
return self._fields
|
||||||
|
|
||||||
|
if is_node(self.node_type):
|
||||||
|
return self.node_type._meta.filter_fields
|
||||||
|
else:
|
||||||
|
# ConnectionFields can also be passed Connections,
|
||||||
|
# in which case, we need to use the Node of the connection
|
||||||
|
# to get our relevant args.
|
||||||
|
return self.node_type._meta.node._meta.filter_fields
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def args(self):
|
def args(self):
|
||||||
|
|
|
@ -13,7 +13,8 @@ class Registry(object):
|
||||||
# assert self.get_type_for_model(cls._meta.model) == cls, (
|
# assert self.get_type_for_model(cls._meta.model) == cls, (
|
||||||
# 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model)
|
# 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model)
|
||||||
# )
|
# )
|
||||||
self._registry[cls._meta.model] = cls
|
if not getattr(cls._meta, 'skip_registry', False):
|
||||||
|
self._registry[cls._meta.model] = cls
|
||||||
|
|
||||||
def get_type_for_model(self, model):
|
def get_type_for_model(self, model):
|
||||||
return self._registry.get(model)
|
return self._registry.get(model)
|
||||||
|
|
|
@ -176,6 +176,22 @@ def test_field_with_choices_gettext():
|
||||||
convert_django_field_with_choices(field)
|
convert_django_field_with_choices(field)
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_with_choices_collision():
|
||||||
|
field = models.CharField(help_text='Timezone', choices=(
|
||||||
|
('Etc/GMT+1+2', 'Fake choice to produce double collision'),
|
||||||
|
('Etc/GMT+1', 'Greenwich Mean Time +1'),
|
||||||
|
('Etc/GMT-1', 'Greenwich Mean Time -1'),
|
||||||
|
))
|
||||||
|
|
||||||
|
class CollisionChoicesModel(models.Model):
|
||||||
|
timezone = field
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'test'
|
||||||
|
|
||||||
|
convert_django_field_with_choices(field)
|
||||||
|
|
||||||
|
|
||||||
def test_should_float_convert_float():
|
def test_should_float_convert_float():
|
||||||
assert_conversion(models.FloatField, graphene.Float)
|
assert_conversion(models.FloatField, graphene.Float)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ from py.test import raises
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
|
||||||
|
from ..utils import DJANGO_FILTER_INSTALLED
|
||||||
from ..compat import MissingType, RangeField
|
from ..compat import MissingType, RangeField
|
||||||
from ..fields import DjangoConnectionField
|
from ..fields import DjangoConnectionField
|
||||||
from ..types import DjangoObjectType
|
from ..types import DjangoObjectType
|
||||||
|
@ -281,3 +282,85 @@ def test_should_query_connectionfields():
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED,
|
||||||
|
reason="django-filter should be installed")
|
||||||
|
def test_should_query_node_filtering():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node, )
|
||||||
|
|
||||||
|
class ArticleType(DjangoObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
interfaces = (Node, )
|
||||||
|
filter_fields = ('lang', )
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
r = Reporter.objects.create(
|
||||||
|
first_name='John',
|
||||||
|
last_name='Doe',
|
||||||
|
email='johndoe@example.com',
|
||||||
|
a_choice=1
|
||||||
|
)
|
||||||
|
Article.objects.create(
|
||||||
|
headline='Article Node 1',
|
||||||
|
pub_date=datetime.date.today(),
|
||||||
|
reporter=r,
|
||||||
|
editor=r,
|
||||||
|
lang='es'
|
||||||
|
)
|
||||||
|
Article.objects.create(
|
||||||
|
headline='Article Node 2',
|
||||||
|
pub_date=datetime.date.today(),
|
||||||
|
reporter=r,
|
||||||
|
editor=r,
|
||||||
|
lang='en'
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = '''
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
articles(lang: "es") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'allReporters': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'id': 'UmVwb3J0ZXJUeXBlOjE=',
|
||||||
|
'articles': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'id': 'QXJ0aWNsZVR5cGU6MQ=='
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
|
@ -183,6 +183,15 @@ def test_batch_allows_post_with_json_encoding(client):
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_fails_if_is_empty(client):
|
||||||
|
response = client.post(batch_url_string(), '[]', 'application/json')
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response_json(response) == {
|
||||||
|
'errors': [{'message': 'Received an empty list in the batch request.'}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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')
|
||||||
|
|
||||||
|
@ -432,9 +441,18 @@ def test_handles_errors_caused_by_a_lack_of_query(client):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_handles_invalid_json_bodies(client):
|
def test_handles_not_expected_json_bodies(client):
|
||||||
response = client.post(url_string(), '[]', 'application/json')
|
response = client.post(url_string(), '[]', 'application/json')
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response_json(response) == {
|
||||||
|
'errors': [{'message': 'The received data is not a valid JSON query.'}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_handles_invalid_json_bodies(client):
|
||||||
|
response = client.post(url_string(), '[oh}', 'application/json')
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response_json(response) == {
|
assert response_json(response) == {
|
||||||
'errors': [{'message': 'POST body sent invalid JSON.'}]
|
'errors': [{'message': 'POST body sent invalid JSON.'}]
|
||||||
|
|
|
@ -58,6 +58,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta):
|
||||||
only_fields=(),
|
only_fields=(),
|
||||||
exclude_fields=(),
|
exclude_fields=(),
|
||||||
interfaces=(),
|
interfaces=(),
|
||||||
|
skip_registry=False,
|
||||||
registry=None
|
registry=None
|
||||||
)
|
)
|
||||||
if DJANGO_FILTER_INSTALLED:
|
if DJANGO_FILTER_INSTALLED:
|
||||||
|
|
|
@ -193,10 +193,19 @@ class GraphQLView(View):
|
||||||
try:
|
try:
|
||||||
request_json = json.loads(request.body.decode('utf-8'))
|
request_json = json.loads(request.body.decode('utf-8'))
|
||||||
if self.batch:
|
if self.batch:
|
||||||
assert isinstance(request_json, list)
|
assert isinstance(request_json, list), (
|
||||||
|
'Batch requests should receive a list, but received {}.'
|
||||||
|
).format(repr(request_json))
|
||||||
|
assert len(request_json) > 0, (
|
||||||
|
'Received an empty list in the batch request.'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
assert isinstance(request_json, dict)
|
assert isinstance(request_json, dict), (
|
||||||
|
'The received data is not a valid JSON query.'
|
||||||
|
)
|
||||||
return request_json
|
return request_json
|
||||||
|
except AssertionError as e:
|
||||||
|
raise HttpError(HttpResponseBadRequest(str(e)))
|
||||||
except:
|
except:
|
||||||
raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.'))
|
raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.'))
|
||||||
|
|
||||||
|
|
2
pytest.ini
Normal file
2
pytest.ini
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[pytest]
|
||||||
|
DJANGO_SETTINGS_MODULE = django_test_settings
|
|
@ -1,9 +1,6 @@
|
||||||
[aliases]
|
[aliases]
|
||||||
test=pytest
|
test=pytest
|
||||||
|
|
||||||
[tool:pytest]
|
|
||||||
DJANGO_SETTINGS_MODULE = django_test_settings
|
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/*
|
exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/*
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
|
Loading…
Reference in New Issue
Block a user