mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-22 09:37:07 +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
|
||||
*.sqlite3
|
||||
.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
|
||||
=======================
|
||||
|
||||
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
|
||||
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')
|
||||
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
|
||||
---------------------------
|
||||
|
||||
|
@ -108,3 +122,28 @@ method to your ``DjangoObjectType``.
|
|||
if post.published or context.user == post.owner:
|
||||
return post
|
||||
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.
|
||||
project = u'Graphene Django'
|
||||
copyright = u'Graphene 2016'
|
||||
copyright = u'Graphene 2017'
|
||||
author = u'Syrus Akbary'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
|
|
@ -6,7 +6,8 @@ Contents:
|
|||
.. toctree::
|
||||
:maxdepth: 0
|
||||
|
||||
tutorial
|
||||
tutorial-plain
|
||||
tutorial-relay
|
||||
filtering
|
||||
authorization
|
||||
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
|
||||
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
|
||||
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
|
||||
------------------------
|
||||
|
||||
|
@ -43,7 +48,7 @@ Now sync your database for the first time:
|
|||
Let's create a few simple models...
|
||||
|
||||
Defining our models
|
||||
-------------------
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Let's get started with these models:
|
||||
|
||||
|
@ -68,6 +73,33 @@ Let's get started with these models:
|
|||
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``).
|
||||
|
||||
Schema
|
||||
------
|
||||
|
||||
|
@ -90,7 +122,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
|||
from graphene_django import DjangoObjectType
|
||||
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.
|
||||
|
@ -145,10 +177,10 @@ Create the parent project-level ``cookbook/schema.py``:
|
|||
|
||||
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
|
||||
# as we begin to add more apps to our project
|
||||
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``
|
||||
file (although it currently lacks any namespacing).
|
||||
|
||||
Testing everything so far
|
||||
-------------------------
|
||||
|
||||
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
|
||||
|
@ -191,7 +226,7 @@ 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``
|
||||
|
@ -230,39 +265,9 @@ as explained above, we can do so here using:
|
|||
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
|
||||
--------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
We're now ready to test the API we've built. Let's fire up the server
|
||||
from the command line.
|
||||
|
@ -276,7 +281,7 @@ from the command line.
|
|||
Starting development server at http://127.0.0.1:8000/
|
||||
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!
|
||||
|
||||
.. 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.
|
||||
The project contains two apps, one named `ingredients` and another
|
||||
named `recepies`.
|
||||
named `recipes`.
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
|
|
@ -27,12 +27,16 @@ def convert_choice_name(name):
|
|||
|
||||
|
||||
def get_choices(choices):
|
||||
converted_names = []
|
||||
for value, help_text in choices:
|
||||
if isinstance(help_text, (tuple, list)):
|
||||
for choice in get_choices(help_text):
|
||||
yield choice
|
||||
else:
|
||||
name = convert_choice_name(value)
|
||||
while name in converted_names:
|
||||
name += '_' + str(len(converted_names))
|
||||
converted_names.append(name)
|
||||
description = help_text
|
||||
yield name, value, description
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ class DjangoConnectionField(ConnectionField):
|
|||
iterable = default_manager
|
||||
iterable = maybe_queryset(iterable)
|
||||
if isinstance(iterable, QuerySet):
|
||||
iterable &= maybe_queryset(default_manager)
|
||||
_len = iterable.count()
|
||||
else:
|
||||
_len = len(iterable)
|
||||
|
|
|
@ -5,6 +5,7 @@ from functools import partial
|
|||
|
||||
from graphene.types.argument import to_arguments
|
||||
from ..fields import DjangoConnectionField
|
||||
from graphene.relay import is_node
|
||||
from .utils import get_filtering_args_from_filterset, get_filterset_class
|
||||
|
||||
|
||||
|
@ -28,7 +29,15 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
|||
|
||||
@property
|
||||
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)
|
||||
if self._extra_filter_meta:
|
||||
meta.update(self._extra_filter_meta)
|
||||
|
@ -36,7 +45,16 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
|||
|
||||
@property
|
||||
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
|
||||
def args(self):
|
||||
|
|
|
@ -13,7 +13,8 @@ class Registry(object):
|
|||
# assert self.get_type_for_model(cls._meta.model) == cls, (
|
||||
# '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):
|
||||
return self._registry.get(model)
|
||||
|
|
|
@ -176,6 +176,22 @@ def test_field_with_choices_gettext():
|
|||
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():
|
||||
assert_conversion(models.FloatField, graphene.Float)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from py.test import raises
|
|||
import graphene
|
||||
from graphene.relay import Node
|
||||
|
||||
from ..utils import DJANGO_FILTER_INSTALLED
|
||||
from ..compat import MissingType, RangeField
|
||||
from ..fields import DjangoConnectionField
|
||||
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):
|
||||
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')
|
||||
|
||||
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_json(response) == {
|
||||
'errors': [{'message': 'POST body sent invalid JSON.'}]
|
||||
|
|
|
@ -58,6 +58,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta):
|
|||
only_fields=(),
|
||||
exclude_fields=(),
|
||||
interfaces=(),
|
||||
skip_registry=False,
|
||||
registry=None
|
||||
)
|
||||
if DJANGO_FILTER_INSTALLED:
|
||||
|
|
|
@ -193,10 +193,19 @@ class GraphQLView(View):
|
|||
try:
|
||||
request_json = json.loads(request.body.decode('utf-8'))
|
||||
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:
|
||||
assert isinstance(request_json, dict)
|
||||
assert isinstance(request_json, dict), (
|
||||
'The received data is not a valid JSON query.'
|
||||
)
|
||||
return request_json
|
||||
except AssertionError as e:
|
||||
raise HttpError(HttpResponseBadRequest(str(e)))
|
||||
except:
|
||||
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
|
Loading…
Reference in New Issue
Block a user