Merge remote-tracking branch 'remote/master' into recursive-nodes

This commit is contained in:
Tony Angerilli 2017-02-28 23:27:17 -08:00
commit 57dbcd34f4
No known key found for this signature in database
GPG Key ID: 621F8D351C20CC5A
44 changed files with 1335 additions and 53 deletions

13
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -6,7 +6,8 @@ Contents:
.. toctree::
:maxdepth: 0
tutorial
tutorial-plain
tutorial-relay
filtering
authorization
debug

516
docs/tutorial-plain.rst Normal file
View 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.**

View File

@ -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::

View 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)

View 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)

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class IngredientsConfig(AppConfig):
name = 'cookbook.ingredients'
label = 'ingredients'
verbose_name = 'Ingredients'

View File

@ -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}}]

View File

@ -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')),
],
),
]

View File

@ -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),
),
]

View 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

View 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

View 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]

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class RecipesConfig(AppConfig):
name = 'cookbook.recipes'
label = 'recipes'
verbose_name = 'Recipes'

View File

@ -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')),
],
),
]

View File

@ -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),
),
]

View 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'),
))

View 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()

View 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)

View 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,
}

View 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)),
]

View 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()

View 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)

View File

@ -0,0 +1,5 @@
graphene
graphene-django
django_graphiql
graphql-core
django==1.9

View File

@ -0,0 +1,2 @@
[flake8]
exclude=migrations,.git,__pycache__

View File

@ -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
---------------

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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.'}]

View File

@ -58,6 +58,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta):
only_fields=(),
exclude_fields=(),
interfaces=(),
skip_registry=False,
registry=None
)
if DJANGO_FILTER_INSTALLED:

View File

@ -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
View File

@ -0,0 +1,2 @@
[pytest]
DJANGO_SETTINGS_MODULE = django_test_settings

View File

@ -1,9 +1,6 @@
[aliases]
test=pytest
[tool:pytest]
DJANGO_SETTINGS_MODULE = django_test_settings
[flake8]
exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/*
max-line-length = 120