Merge pull request #60 from adamcharnock/feature/django

Django integration implementation
This commit is contained in:
Syrus Akbary 2016-01-13 18:45:52 -08:00
commit c733b38b5c
64 changed files with 1713 additions and 134 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,rst,ini}]
indent_style = space
indent_size = 4

6
.gitignore vendored
View File

@ -72,3 +72,9 @@ target/
/docs/static/playground/lib
/docs/static/playground
# PyCharm
.idea
# Databases
*.sqlite3

View File

@ -2,7 +2,6 @@ language: python
sudo: false
python:
- 2.7
- 3.3
- 3.4
- 3.5
- pypy
@ -24,8 +23,9 @@ before_install:
install:
- |
if [ "$TEST_TYPE" = build ]; then
pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django
pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter
pip install --download-cache $HOME/.cache/pip/ -e .[django]
pip install django==$DJANGO_VERSION
python setup.py develop
elif [ "$TEST_TYPE" = build_website ]; then
pip install --download-cache $HOME/.cache/pip/ -e .
@ -79,6 +79,14 @@ env:
matrix:
fast_finish: true
include:
- python: '2.7'
env: TEST_TYPE=build DJANGO_VERSION=1.6
- python: '2.7'
env: TEST_TYPE=build DJANGO_VERSION=1.7
- python: '2.7'
env: TEST_TYPE=build DJANGO_VERSION=1.8
- python: '2.7'
env: TEST_TYPE=build DJANGO_VERSION=1.9
- python: '2.7'
env: TEST_TYPE=build_website
- python: '2.7'

View File

@ -1,5 +1,7 @@
#!/bin/bash
# Install the required scripts with
# pip install autoflake autopep8 isort
autoflake ./examples/ ./graphene/ -r --remove-unused-variables --remove-all-unused-imports --in-place
autopep8 ./examples/ ./graphene/ -r --in-place --experimental --aggressive --max-line-length 120
isort -rc ./examples/ ./graphene/

View File

@ -16,4 +16,5 @@ ga = "UA-12613282-7"
"/docs/mutations/",
"/docs/basic-types/",
"/docs/relay/",
"/docs/filtering/",
]

View File

@ -0,0 +1,150 @@
---
title: Filtering (Django)
description: Details of how to perform filtering
---
# Filtering (Django)
Graphene integrates with [django-filter](https://django-filter.readthedocs.org)
to provide filtering of results. See the
[usage documentation](https://django-filter.readthedocs.org/en/latest/usage.html#the-filter)
for details on the format for `filter_fields`.
This filtering is only available when using the Django integrations
(i.e. nodes which extend `DjangoNode`). Additionally `django-filter`
is an optional dependency of Graphene. You will need to
install it manually, which can be done as follows:
```bash
# You'll need to django-filter
pip install django-filter
```
**Note: The techniques below are demoed in the
[cookbook example app](https://github.com/graphql-python/graphene/tree/feature/django/examples/cookbook).**
## Filterable fields
The `filter_fields` parameter is used to specify the fields which can be filtered upon.
The value specified here is passed directly to `django-filter`, so see the
[filtering documentation](https://django-filter.readthedocs.org/en/latest/usage.html#the-filter)
for full details on the range of options available.
For example:
```python
class AnimalNode(DjangoNode):
class Meta:
# Assume you have an Animal model defined with the following fields
model = Animal
filter_fields = ['name', 'genus', 'is_domesticated']
class Query(ObjectType):
animal = relay.NodeField(AnimalNode)
all_animals = DjangoFilterConnectionField(AnimalNode)
```
You could then perform a query such as:
```graphql
query {
# Note that fields names become camelcased
allAnimals(genus: "cat", isDomesticated: true) {
edges {
node {
id,
name
}}}}
```
You can also make more complex lookup types available:
```python
class AnimalNode(DjangoNode):
class Meta:
model = Animal
# Provide more complex lookup types
filter_fields = {
'name': ['exact', 'icontains', 'istartswith'],
'genus': ['exact'],
'is_domesticated': ['exact'],
}
```
Which you could query as follows:
```graphql
query {
# Note that fields names become camelcased
allAnimals(nameIcontains: "lion") {
edges {
node {
id,
name
}}}}
```
## Orderable fields
Ordering can also be specified using `filter_order_by`. Like `filter_fields`,
this value is also passed directly to `django-filter` as the `order_by` field.
For full details see the
[order_by documentation](https://django-filter.readthedocs.org/en/latest/usage.html#ordering-using-order-by).
For example:
```python
class AnimalNode(DjangoNode):
class Meta:
model = Animal
filter_fields = ['name', 'genus', 'is_domesticated']
# Either a tuple/list of fields upon which ordering is allowed, or
# True to allow filtering on all fields specified in filter_fields
order_by_fields = True
```
You can then control the ordering via the `orderBy` argument:
```graphql
query {
allAnimals(orderBy: "name") {
edges {
node {
id,
name
}}}}
```
## Custom Filtersets
By default Graphene provides easy access to the most commonly used
features of `django-filter`. This is done by transparently creating a
`django_filters.FilterSet` class for you and passing in the values for
`filter_fields` and `order_by_fields`.
However, you may find this to be insufficient. In these cases you can
create your own `Filterset` as follows:
```python
class AnimalNode(DjangoNode):
class Meta:
# Assume you have an Animal model defined with the following fields
model = Animal
filter_fields = ['name', 'genus', 'is_domesticated']
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
name = django_filters.CharFilter(lookup_type='iexact')
class Meta:
model = Animal
fields = ['name', 'genus', 'is_domesticated']
class Query(ObjectType):
animal = relay.NodeField(AnimalNode)
# We specify our custom AnimalFilter using the filterset_class param
all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter)
```

View File

@ -1,154 +1,259 @@
---
title: Django Tutorial
title: Django Quickstart
description: A Quick guide to Graphene in Django
---
# Django Tutorial
In our previous quickstart page we created a very simple schema.
Graphene has a number of additional features that are designed to make
working with Django simple.
Now we will adapt the schema to automatically map some Django models,
and expose this schema in a `/graphql` API endpoint.
If you need help getting started with django then head over to
Django's getting started page.
## Project setup
First let's create a few simple models...
```bash
# Create the project directory
mkdir tutorial
cd tutorial
**Note: The code in this quickstart is pulled from the
[cookbook example app](https://github.com/graphql-python/graphene/tree/feature/django/examples/cookbook)**.
# Create a virtualenv to isolate our package dependencies locally
virtualenv env
source env/bin/activate # On Windows use `env\Scripts\activate`
## Defining our models
# Install Django and Graphene with Django support
pip install django
pip install graphene[django]
pip install django-graphiql
Before continuing, create the following:
# Set up a new project with a single application
django-admin.py startproject tutorial . # Note the trailing '.' character
django-admin.py startapp quickstart
* A Django project called `cookbook`
* An app within `cookbook` called `ingredients`
Let's get started with these models:
```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
```
Now sync your database for the first time:
```bash
python manage.py migrate
```
We'll also create an initial user named `admin` with a password of `password`.
```bash
python manage.py createsuperuser
```
Once you've set up a database and initial user created and ready to go, open up the app's directory and we'll get coding...
## Schema
GraphQL presents your objects to the world as a graph structure rather than a more
heiricarcal structure to which you may be acustomed. In order to create this
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. Below we define these as the `UserType` and `GroupType` classes.
This graph also has a 'root' through which all access begins. This is the `Query` class below.
In this example, we provide the ability to list all users via `all_users`, and the
ability to obtain a single user via `get_user`.
ability to obtain a specific user via `get_user`.
Open `tutorial/quickstart/schema.py` and type the following:
Create `cookbook/ingredients/schema.py` and type the following:
```python
# cookbook/ingredients/schema.py
from graphene import relay, ObjectType
from graphene.contrib.django.filter import DjangoFilterConnectionField
from graphene.contrib.django.types import DjangoNode
from cookbook.ingredients.models import Category, Ingredient
# Graphene will automatically map the User model's fields onto the UserType.
# This is configured in the UserType's Meta class (as you can see below)
class CategoryNode(DjangoNode):
class Meta:
model = Category
filter_fields = ['name', 'ingredients']
filter_order_by = ['name']
class IngredientNode(DjangoNode):
class Meta:
model = Ingredient
# Allow for some more advanced filtering here
filter_fields = {
'name': ['exact', 'icontains', 'istartswith'],
'notes': ['exact', 'icontains'],
'category': ['exact'],
'category__name': ['exact'],
}
filter_order_by = ['name', 'category__name']
class Query(ObjectType):
category = relay.NodeField(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)
ingredient = relay.NodeField(IngredientNode)
all_ingredients = DjangoFilterConnectionField(IngredientNode)
class Meta:
abstract = True
```
The filtering functionality is provided by
[django-filter](https://django-filter.readthedocs.org). See the
[usage documentation](https://django-filter.readthedocs.org/en/latest/usage.html#the-filter)
for details on the format for `filter_fields`.
Note that the above `Query` class is marked as 'abstract'. This is because we
want will now create a project-level query which will combine all our app-level
queries.
Create the parent project-level `cookbook/schema.py`:
```python
import graphene
from graphene.contrib.django import DjangoObjectType
from django.contrib.auth.models import User, Group
# Graphene will automatically map the User model's fields onto the UserType.
# This is configured in the UserType's Meta class
class UserType(DjangoObjectType):
class Meta:
model = User
only_fields = ('username', 'email', 'groups')
import cookbook.ingredients.schema
class GroupType(DjangoObjectType):
class Meta:
model = Group
only_fields = ('name', )
class Query(cookbook.ingredients.schema.Query):
# This class will inherit from multiple Queries
# as we begin to add more apps to our project
pass
class Query(graphene.ObjectType):
all_users = graphene.List(UserType)
get_user = graphene.Field(UserType,
id=graphene.String().NonNull)
get_group = graphene.Field(GroupType,
id=graphene.String().NonNull)
def resolve_all_users(self, args, info):
return User.objects.all()
def resolve_get_user(self, args, info):
return User.objects.get(id=args.get('id'))
def resolve_get_group(self, args, info):
return Group.objects.get(id=args.get('id'))
schema = graphene.Schema(query=Query)
schema = graphene.Schema(name='Cookbook Schema')
schema.query = Query
```
You can think of this as being something like your top-level `urls.py`
file (although it currently lacks any namespacing).
## Adding GraphiQL
For having the GraphiQL static assets we need to append `django_graphiql` in `INSTALLED_APPS` in `tutorial/settings.py`:
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 `django_graphiql` to `INSTALLED_APPS` in `cookbook/settings.py`:
```python
INSTALLED_APPS = [
# The other installed apps
...
'django_graphiql',
# This will also make the `graphql_schema` management command available
'graphene.contrib.django',
]
```
## Creating GraphQL and GraphiQL views
Unlike a RESTful API, there is only a single URL from which a GraphQL is accessed.
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.
Additionally, an interface for navigating this API will be very useful. Graphene
includes the [graphiql](https://github.com/graphql/graphiql) in-browser IDE
which assists in exploring and querying your new API. Well add a URL for this too.
Additionally, we'll add a URL for aforementioned GraphiQL, and for the Django admin
interface (the latter can be useful for creating test data).
```python
from django.conf.urls import url, include
from django.contrib import admin
from django.views.decorators.csrf import csrf_exempt
from graphene.contrib.django.views import GraphQLView
from quickstart.schema import schema
from cookbook.schema import schema
# Wire up our GraphQL schema to /graphql.
# Additionally, we include GraphiQL view for querying easily our schema.
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^graphql', csrf_exempt(GraphQLView.as_view(schema=schema))),
url(r'^graphiql', include('django_graphiql.urls')),
]
```
## 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/feature/django/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json)
fixture and place it in
`cookbook/ingredients/fixtures/ingredients.json`. You can then run the following:
```
$ 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 youself.
You'll need to run the development server (see below), and probably 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.
```bash
python ./manage.py runserver
$ 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:8080/graphiql](http://localhost:8080/graphiql) and type your first query!
Go to [localhost:8000/graphiql](http://localhost:8000/graphiql) and type your first query!
```graphql
myQuery {
getUser(id:"1") {
username
query {
allIngredients {
edges {
node {
id,
name
}
}
}
}
```
The above will return the names & IDs for all ingredients. But perhaps you want
a specific ingredient:
```graphql
query {
# Graphene creates globally unique IDs for all objects.
# You may need to copy this value from the results of the first query
ingredient(id: "SW5ncmVkaWVudE5vZGU6MQ==") {
name
}
}
```
You can also get each ingredient for each category:
```graphql
query {
allCategories {
edges {
node {
name,
ingredients {
edges {
node {
name
}}}}}}}
```
Or you can get only 'meat' ingredients containing the letter 'e':
```graphql
query {
# You can also use `category: "CATEGORY GLOBAL ID"`
allIngredients(nameIcontains: "e", categoryName: "Meat") {
edges {
node {
name
}}}}
```

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.git
cd graphene/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/graphiql](http://127.0.0.1:8000/graphiql)
and run some queries!
(See the [Django quickstart guide](http://graphene-python.org/docs/quickstart-django/)
for some example queries)

View File

@ -0,0 +1,6 @@
from django.contrib import admin
from cookbook.ingredients.models import Category, Ingredient
admin.site.register(Ingredient)
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,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()
category = models.ForeignKey(Category, related_name='ingredients')
def __str__(self):
return self.name

View File

@ -0,0 +1,39 @@
from cookbook.ingredients.models import Category, Ingredient
from graphene import ObjectType, relay
from graphene.contrib.django.filter import DjangoFilterConnectionField
from graphene.contrib.django.types import DjangoNode
# Graphene will automatically map the User model's fields onto the UserType.
# This is configured in the UserType's Meta class (as you can see below)
class CategoryNode(DjangoNode):
class Meta:
model = Category
filter_fields = ['name', 'ingredients']
filter_order_by = ['name']
class IngredientNode(DjangoNode):
class Meta:
model = Ingredient
# Allow for some more advanced filtering here
filter_fields = {
'name': ['exact', 'icontains', 'istartswith'],
'notes': ['exact', 'icontains'],
'category': ['exact'],
'category__name': ['exact'],
}
filter_order_by = ['name', 'category__name']
class Query(ObjectType):
category = relay.NodeField(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)
ingredient = relay.NodeField(IngredientNode)
all_ingredients = DjangoFilterConnectionField(IngredientNode)
class Meta:
abstract = True

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -0,0 +1,6 @@
from django.contrib import admin
from cookbook.recipes.models import Recipe, RecipeIngredient
admin.site.register(Recipe)
admin.site.register(RecipeIngredient)

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
from django.db import migrations, models
import django.db.models.deletion
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,19 @@
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):
recipes = models.ForeignKey(Recipe, related_name='amounts')
ingredient = models.ForeignKey(Ingredient, related_name='used_by')
amount = models.FloatField()
unit = models.CharField(max_length=20, choices=(
('kg', 'Kilograms'),
('l', 'Litres'),
('', 'Units'),
))

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -0,0 +1,9 @@
import cookbook.ingredients.schema
import graphene
class Query(cookbook.ingredients.schema.Query):
pass
schema = graphene.Schema(name='Cookbook Schema')
schema.query = Query

View File

@ -0,0 +1,125 @@
"""
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',
'django_graphiql',
'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',
]
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/'

View File

@ -0,0 +1,12 @@
from django.conf.urls import include, url
from django.contrib import admin
from django.views.decorators.csrf import csrf_exempt
from cookbook.schema import schema
from graphene.contrib.django.views import GraphQLView
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^graphql', csrf_exempt(GraphQLView.as_view(schema=schema))),
url(r'^graphiql', include('django_graphiql.urls')),
]

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[django]
django_graphiql
graphql-core
django==1.9
django-filter==0.11.0

View File

@ -9,4 +9,4 @@ from graphene.contrib.django.fields import (
)
__all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection',
'DjangoConnectionField', 'DjangoModelField']
'DjangoModelField', 'DjangoConnectionField']

View File

@ -0,0 +1,15 @@
from django.db import models
try:
UUIDField = models.UUIDField
except AttributeError:
# Improved compatibility for Django 1.6
class UUIDField(object):
pass
try:
from django.db.models.related import RelatedObject
except:
# Improved compatibility for Django 1.6
class RelatedObject(object):
pass

View File

@ -1,15 +1,10 @@
from django.db import models
from singledispatch import singledispatch
from ...core.types.scalars import ID, Boolean, Float, Int, String
from .fields import ConnectionOrListField, DjangoModelField
from .compat import RelatedObject, UUIDField
from .utils import get_related_model, import_single_dispatch
try:
UUIDField = models.UUIDField
except AttributeError:
# Improved compatibility for Django 1.6
class UUIDField(object):
pass
singledispatch = import_single_dispatch()
@singledispatch
@ -64,11 +59,21 @@ def convert_field_to_float(field):
@convert_django_field.register(models.ManyToManyField)
@convert_django_field.register(models.ManyToOneRel)
def convert_field_to_list_or_connection(field):
model_field = DjangoModelField(field.related_model)
from .fields import DjangoModelField, ConnectionOrListField
model_field = DjangoModelField(get_related_model(field))
return ConnectionOrListField(model_field)
# For Django 1.6
@convert_django_field.register(RelatedObject)
def convert_relatedfield_to_djangomodel(field):
from .fields import DjangoModelField, ConnectionOrListField
model_field = DjangoModelField(field.model)
return ConnectionOrListField(model_field)
@convert_django_field.register(models.OneToOneField)
@convert_django_field.register(models.ForeignKey)
def convert_field_to_djangomodel(field):
return DjangoModelField(field.related_model, description=field.help_text)
from .fields import DjangoModelField
return DjangoModelField(get_related_model(field), description=field.help_text)

View File

@ -2,8 +2,8 @@ from contextlib import contextmanager
from django.db import connections
from ....core.types import Field
from ....core.schema import GraphQLSchema
from ....core.types import Field
from ....plugins import Plugin
from .sql.tracking import unwrap_cursor, wrap_cursor
from .sql.types import DjangoDebugSQL

View File

@ -1,4 +1,4 @@
from .....core import Float, ObjectType, String, Boolean
from .....core import Boolean, Float, ObjectType, String
class DjangoDebugSQL(ObjectType):

View File

@ -1,33 +1,53 @@
import warnings
from ...core.exceptions import SkipField
from ...core.fields import Field
from ...core.types.base import FieldType
from ...core.types.definitions import List
from ...relay import ConnectionField
from ...relay.utils import is_node
from .utils import get_type_for_model
from .utils import get_type_for_model, maybe_queryset
class DjangoConnectionField(ConnectionField):
def __init__(self, *args, **kwargs):
cls = self.__class__
warnings.warn("Using {} will be not longer supported."
" Use relay.ConnectionField instead".format(cls.__name__),
FutureWarning)
self.on = kwargs.pop('on', False)
return super(DjangoConnectionField, self).__init__(*args, **kwargs)
@property
def model(self):
return self.type._meta.model
def get_manager(self):
if self.on:
return getattr(self.model, self.on)
else:
return self.model._default_manager
def get_queryset(self, resolved_qs, args, info):
return resolved_qs
def from_list(self, connection_type, resolved, args, info):
if not resolved:
resolved = self.get_manager()
resolved_qs = maybe_queryset(resolved)
qs = self.get_queryset(resolved_qs, args, info)
return super(DjangoConnectionField, self).from_list(connection_type, qs, args, info)
class ConnectionOrListField(Field):
def internal_type(self, schema):
from .filter.fields import DjangoFilterConnectionField
model_field = self.type
field_object_type = model_field.get_object_type(schema)
if not field_object_type:
raise SkipField()
if is_node(field_object_type):
field = ConnectionField(field_object_type)
if field_object_type._meta.filter_fields:
field = DjangoFilterConnectionField(field_object_type)
else:
field = DjangoConnectionField(field_object_type)
else:
field = Field(List(field_object_type))
field.contribute_to_class(self.object_type, self.attname)

View File

@ -0,0 +1,13 @@
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
if not DJANGO_FILTER_INSTALLED:
raise Exception(
"Use of django filtering requires the django-filter package "
"be installed. You can do so using `pip install django-filter`"
)
from .fields import DjangoFilterConnectionField
from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter
__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet',
'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter']

View File

@ -0,0 +1,36 @@
from ..fields import DjangoConnectionField
from .utils import get_filtering_args_from_filterset, get_filterset_class
class DjangoFilterConnectionField(DjangoConnectionField):
def __init__(self, type, fields=None, order_by=None,
extra_filter_meta=None, filterset_class=None,
*args, **kwargs):
self.order_by = order_by or type._meta.filter_order_by
self.fields = fields or type._meta.filter_fields
meta = dict(model=type._meta.model,
fields=self.fields,
order_by=self.order_by)
if extra_filter_meta:
meta.update(extra_filter_meta)
self.filterset_class = get_filterset_class(filterset_class, **meta)
self.filtering_args = get_filtering_args_from_filterset(self.filterset_class, type)
kwargs.setdefault('args', {})
kwargs['args'].update(**self.filtering_args)
super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs)
def get_queryset(self, qs, args, info):
filterset_class = self.filterset_class
filter_kwargs = self.get_filter_kwargs(args)
order = self.get_order(args)
if order:
qs = qs.order_by(order)
return filterset_class(data=filter_kwargs, queryset=qs)
def get_filter_kwargs(self, args):
return {k: v for k, v in args.items() if k in self.filtering_args}
def get_order(self, args):
return args.get('order_by', None)

View File

@ -0,0 +1,116 @@
import six
from django.conf import settings
from django.db import models
from django.utils.text import capfirst
from django_filters import Filter, MultipleChoiceFilter
from django_filters.filterset import FilterSet, FilterSetMetaclass
from graphql_relay.node.node import from_global_id
from graphene.contrib.django.forms import (GlobalIDFormField,
GlobalIDMultipleChoiceField)
class GlobalIDFilter(Filter):
field_class = GlobalIDFormField
def filter(self, qs, value):
gid = from_global_id(value)
return super(GlobalIDFilter, self).filter(qs, gid.id)
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
field_class = GlobalIDMultipleChoiceField
def filter(self, qs, value):
gids = [from_global_id(v).id for v in value]
return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids)
ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order_by')
GRAPHENE_FILTER_SET_OVERRIDES = {
models.AutoField: {
'filter_class': GlobalIDFilter,
},
models.OneToOneField: {
'filter_class': GlobalIDFilter,
},
models.ForeignKey: {
'filter_class': GlobalIDFilter,
},
models.ManyToManyField: {
'filter_class': GlobalIDMultipleChoiceFilter,
}
}
class GrapheneFilterSetMetaclass(FilterSetMetaclass):
def __new__(cls, name, bases, attrs):
new_class = super(GrapheneFilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
# Customise the filter_overrides for Graphene
for k, v in GRAPHENE_FILTER_SET_OVERRIDES.items():
new_class.filter_overrides.setdefault(k, v)
return new_class
class GrapheneFilterSetMixin(object):
order_by_field = ORDER_BY_FIELD
@classmethod
def filter_for_reverse_field(cls, f, name):
"""Handles retrieving filters for reverse relationships
We override the default implementation so that we can handle
Global IDs (the default implementation expects database
primary keys)
"""
rel = f.field.rel
default = {
'name': name,
'label': capfirst(rel.related_name)
}
if rel.multiple:
# For to-many relationships
return GlobalIDMultipleChoiceFilter(**default)
else:
# For to-one relationships
return GlobalIDFilter(**default)
class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneFilterSetMixin, FilterSet)):
""" Base class for FilterSets used by Graphene
You shouldn't usually need to use this class. The
DjangoFilterConnectionField will wrap FilterSets with this class as
necessary
"""
def setup_filterset(filterset_class):
""" Wrap a provided filterset in Graphene-specific functionality
"""
return type(
'Graphene{}'.format(filterset_class.__name__),
(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneFilterSetMixin, filterset_class),),
{},
)
def custom_filterset_factory(model, filterset_base_class=GrapheneFilterSet,
**meta):
""" Create a filterset for the given model using the provided meta data
"""
meta.update({
'model': model,
})
meta_class = type(str('Meta'), (object,), meta)
filterset = type(
str('%sFilterSet' % model._meta.object_name),
(filterset_base_class,),
{
'Meta': meta_class
}
)
return filterset

View File

@ -0,0 +1,31 @@
import django_filters
from graphene.contrib.django.tests.models import Article, Pet, Reporter
class ArticleFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = {
'headline': ['exact', 'icontains'],
'pub_date': ['gt', 'lt', 'exact'],
'reporter': ['exact'],
}
order_by = True
class ReporterFilter(django_filters.FilterSet):
class Meta:
model = Reporter
fields = ['first_name', 'last_name', 'email', 'pets']
order_by = False
class PetFilter(django_filters.FilterSet):
class Meta:
model = Pet
fields = ['name']
order_by = False

View File

@ -0,0 +1,287 @@
from datetime import datetime
import pytest
from graphene import ObjectType, Schema
from graphene.contrib.django import DjangoNode
from graphene.contrib.django.forms import (GlobalIDFormField,
GlobalIDMultipleChoiceField)
from graphene.contrib.django.tests.models import Article, Pet, Reporter
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
from graphene.relay import NodeField
pytestmark = []
if DJANGO_FILTER_INSTALLED:
import django_filters
from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField,
GlobalIDMultipleChoiceFilter)
from graphene.contrib.django.filter.tests.filters import ArticleFilter, PetFilter
else:
pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed'))
pytestmark.append(pytest.mark.django_db)
class ArticleNode(DjangoNode):
class Meta:
model = Article
class ReporterNode(DjangoNode):
class Meta:
model = Reporter
class PetNode(DjangoNode):
class Meta:
model = Pet
schema = Schema()
def assert_arguments(field, *arguments):
ignore = ('after', 'before', 'first', 'last', 'orderBy')
actual = [
name
for name in schema.T(field.arguments)
if name not in ignore and not name.startswith('_')
]
assert set(arguments) == set(actual), \
'Expected arguments ({}) did not match actual ({})'.format(
arguments,
actual
)
def assert_orderable(field):
assert 'orderBy' in schema.T(field.arguments), \
'Field cannot be ordered'
def assert_not_orderable(field):
assert 'orderBy' not in schema.T(field.arguments), \
'Field can be ordered'
def test_filter_explicit_filterset_arguments():
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter)
assert_arguments(field,
'headline', 'headlineIcontains',
'pubDate', 'pubDateGt', 'pubDateLt',
'reporter',
)
def test_filter_shortcut_filterset_arguments_list():
field = DjangoFilterConnectionField(ArticleNode, fields=['pub_date', 'reporter'])
assert_arguments(field,
'pubDate',
'reporter',
)
def test_filter_shortcut_filterset_arguments_dict():
field = DjangoFilterConnectionField(ArticleNode, fields={
'headline': ['exact', 'icontains'],
'reporter': ['exact'],
})
assert_arguments(field,
'headline', 'headlineIcontains',
'reporter',
)
def test_filter_explicit_filterset_orderable():
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter)
assert_orderable(field)
def test_filter_shortcut_filterset_orderable_true():
field = DjangoFilterConnectionField(ArticleNode, order_by=True)
assert_orderable(field)
def test_filter_shortcut_filterset_orderable_headline():
field = DjangoFilterConnectionField(ArticleNode, order_by=['headline'])
assert_orderable(field)
def test_filter_explicit_filterset_not_orderable():
field = DjangoFilterConnectionField(PetNode, filterset_class=PetFilter)
assert_not_orderable(field)
def test_filter_shortcut_filterset_extra_meta():
field = DjangoFilterConnectionField(ArticleNode, extra_filter_meta={
'order_by': True
})
assert_orderable(field)
def test_filter_filterset_information_on_meta():
class ReporterFilterNode(DjangoNode):
class Meta:
model = Reporter
filter_fields = ['first_name', 'articles']
filter_order_by = True
field = DjangoFilterConnectionField(ReporterFilterNode)
assert_arguments(field, 'firstName', 'articles')
assert_orderable(field)
def test_filter_filterset_information_on_meta_related():
class ReporterFilterNode(DjangoNode):
class Meta:
model = Reporter
filter_fields = ['first_name', 'articles']
filter_order_by = True
class ArticleFilterNode(DjangoNode):
class Meta:
model = Article
filter_fields = ['headline', 'reporter']
filter_order_by = True
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
reporter = NodeField(ReporterFilterNode)
article = NodeField(ArticleFilterNode)
schema = Schema(query=Query)
schema.schema # Trigger the schema loading
articles_field = schema.get_type('ReporterFilterNode')._meta.fields_map['articles']
assert_arguments(articles_field, 'headline', 'reporter')
assert_orderable(articles_field)
def test_filter_filterset_related_results():
class ReporterFilterNode(DjangoNode):
class Meta:
model = Reporter
filter_fields = ['first_name', 'articles']
filter_order_by = True
class ArticleFilterNode(DjangoNode):
class Meta:
model = Article
filter_fields = ['headline', 'reporter']
filter_order_by = True
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
reporter = NodeField(ReporterFilterNode)
article = NodeField(ArticleFilterNode)
r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com')
r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com')
Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1)
Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2)
query = '''
query {
allReporters {
edges {
node {
articles {
edges {
node {
headline
}
}
}
}
}
}
}
'''
schema = Schema(query=Query)
result = schema.execute(query)
assert not result.errors
# We should only get back a single article for each reporter
assert len(result.data['allReporters']['edges'][0]['node']['articles']['edges']) == 1
assert len(result.data['allReporters']['edges'][1]['node']['articles']['edges']) == 1
def test_global_id_field_implicit():
field = DjangoFilterConnectionField(ArticleNode, fields=['id'])
filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['id']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
def test_global_id_field_explicit():
class ArticleIdFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = ['id']
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter)
filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['id']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
def test_global_id_field_relation():
field = DjangoFilterConnectionField(ArticleNode, fields=['reporter'])
filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['reporter']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
def test_global_id_multiple_field_implicit():
field = DjangoFilterConnectionField(ReporterNode, fields=['pets'])
filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['pets']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_global_id_multiple_field_explicit():
class ReporterPetsFilter(django_filters.FilterSet):
class Meta:
model = Reporter
fields = ['pets']
field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter)
filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['pets']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_global_id_multiple_field_implicit_reverse():
field = DjangoFilterConnectionField(ReporterNode, fields=['articles'])
filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['articles']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_global_id_multiple_field_explicit_reverse():
class ReporterPetsFilter(django_filters.FilterSet):
class Meta:
model = Reporter
fields = ['articles']
field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter)
filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['articles']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField

View File

@ -0,0 +1,31 @@
import six
from ....core.types import Argument, String
from .filterset import custom_filterset_factory, setup_filterset
def get_filtering_args_from_filterset(filterset_class, type):
""" Inspect a FilterSet and produce the arguments to pass to
a Graphene Field. These arguments will be available to
filter against in the GraphQL
"""
from graphene.contrib.django.form_converter import convert_form_field
args = {}
for name, filter_field in six.iteritems(filterset_class.base_filters):
field_type = Argument(convert_form_field(filter_field.field))
args[name] = field_type
# Also add the 'order_by' field
if filterset_class._meta.order_by:
args[filterset_class.order_by_field] = Argument(String())
return args
def get_filterset_class(filterset_class, **meta):
"""Get the class to be used as the FilterSet"""
if filterset_class:
# If were given a FilterSet class, then set it up and
# return it
return setup_filterset(filterset_class)
return custom_filterset_factory(**meta)

View File

@ -0,0 +1,73 @@
from django import forms
from django.forms.fields import BaseTemporalField
from graphene import ID, Boolean, Float, Int, String
from graphene.contrib.django.forms import (GlobalIDFormField,
GlobalIDMultipleChoiceField)
from graphene.contrib.django.utils import import_single_dispatch
from graphene.core.types.definitions import List
singledispatch = import_single_dispatch()
try:
UUIDField = forms.UUIDField
except AttributeError:
class UUIDField(object):
pass
@singledispatch
def convert_form_field(field):
raise Exception(
"Don't know how to convert the Django form field %s (%s) "
"to Graphene type" %
(field, field.__class__)
)
@convert_form_field.register(BaseTemporalField)
@convert_form_field.register(forms.CharField)
@convert_form_field.register(forms.EmailField)
@convert_form_field.register(forms.SlugField)
@convert_form_field.register(forms.URLField)
@convert_form_field.register(forms.ChoiceField)
@convert_form_field.register(forms.RegexField)
@convert_form_field.register(forms.Field)
@convert_form_field.register(UUIDField)
def convert_form_field_to_string(field):
return String(description=field.help_text)
@convert_form_field.register(forms.IntegerField)
@convert_form_field.register(forms.NumberInput)
def convert_form_field_to_int(field):
return Int(description=field.help_text)
@convert_form_field.register(forms.BooleanField)
@convert_form_field.register(forms.NullBooleanField)
def convert_form_field_to_boolean(field):
return Boolean(description=field.help_text, required=True)
@convert_form_field.register(forms.NullBooleanField)
def convert_form_field_to_nullboolean(field):
return Boolean(description=field.help_text)
@convert_form_field.register(forms.DecimalField)
@convert_form_field.register(forms.FloatField)
def convert_form_field_to_float(field):
return Float(description=field.help_text)
@convert_form_field.register(forms.ModelMultipleChoiceField)
@convert_form_field.register(GlobalIDMultipleChoiceField)
def convert_form_field_to_list(field):
return List(ID())
@convert_form_field.register(forms.ModelChoiceField)
@convert_form_field.register(GlobalIDFormField)
def convert_form_field_to_id(field):
return ID()

View File

@ -0,0 +1,41 @@
import binascii
from django.core.exceptions import ValidationError
from django.forms import CharField, Field, IntegerField, MultipleChoiceField
from django.utils.translation import ugettext_lazy as _
from graphql_relay import from_global_id
class GlobalIDFormField(Field):
default_error_messages = {
'invalid': _('Invalid ID specified.'),
}
def clean(self, value):
if not value and not self.required:
return None
try:
gid = from_global_id(value)
except (TypeError, ValueError, UnicodeDecodeError, binascii.Error):
raise ValidationError(self.error_messages['invalid'])
try:
IntegerField().clean(gid.id)
CharField().clean(gid.type)
except ValidationError:
raise ValidationError(self.error_messages['invalid'])
return value
class GlobalIDMultipleChoiceField(MultipleChoiceField):
default_error_messages = {
'invalid_choice': _('One of the specified IDs was invalid (%(value)s).'),
'invalid_list': _('Enter a list of values.'),
}
def valid_value(self, value):
# Clean will raise a validation error if there is a problem
GlobalIDFormField().clean(value)
return True

View File

@ -1,8 +1,8 @@
from django.core.management.base import BaseCommand, CommandError
import importlib
import json
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
help = 'Dump Graphene schema JSON to file'

View File

@ -1,9 +1,13 @@
from ...core.classtypes.objecttype import ObjectTypeOptions
from ...relay.types import Node
from ...relay.utils import is_node
from .utils import DJANGO_FILTER_INSTALLED
VALID_ATTRS = ('model', 'only_fields', 'exclude_fields')
if DJANGO_FILTER_INSTALLED:
VALID_ATTRS += ('filter_fields', 'filter_order_by')
class DjangoOptions(ObjectTypeOptions):
@ -13,6 +17,8 @@ class DjangoOptions(ObjectTypeOptions):
self.valid_attrs += VALID_ATTRS
self.only_fields = None
self.exclude_fields = []
self.filter_fields = None
self.filter_order_by = None
def contribute_to_class(self, cls, name):
super(DjangoOptions, self).contribute_to_class(cls, name)

View File

@ -9,8 +9,8 @@ from graphene.contrib.django.fields import (ConnectionOrListField,
from .models import Article, Reporter
def assert_conversion(django_field, graphene_field, *args):
field = django_field(*args, help_text='Custom Help Text')
def assert_conversion(django_field, graphene_field, *args, **kwargs):
field = django_field(help_text='Custom Help Text', *args, **kwargs)
graphene_type = convert_django_field(field)
assert isinstance(graphene_type, graphene_field)
field = graphene_type.as_field()
@ -53,7 +53,7 @@ def test_should_ipaddress_convert_string():
def test_should_auto_convert_id():
assert_conversion(models.AutoField, graphene.ID)
assert_conversion(models.AutoField, graphene.ID, primary_key=True)
def test_should_positive_integer_convert_int():
@ -98,7 +98,10 @@ def test_should_manytomany_convert_connectionorlist():
def test_should_manytoone_convert_connectionorlist():
graphene_type = convert_django_field(Reporter.articles.related)
# Django 1.9 uses 'rel', <1.9 uses 'related
related = getattr(Reporter.articles, 'rel', None) or \
getattr(Reporter.articles, 'related')
graphene_type = convert_django_field(related)
assert isinstance(graphene_type, ConnectionOrListField)
assert isinstance(graphene_type.type, DjangoModelField)
assert graphene_type.type.model == Article

View File

@ -0,0 +1,103 @@
from django import forms
from py.test import raises
import graphene
from graphene.contrib.django.form_converter import convert_form_field
from graphene.core.types import ID, List
from .models import Reporter
def assert_conversion(django_field, graphene_field, *args):
field = django_field(*args, help_text='Custom Help Text')
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, graphene_field)
field = graphene_type.as_field()
assert field.description == 'Custom Help Text'
return field
def test_should_unknown_django_field_raise_exception():
with raises(Exception) as excinfo:
convert_form_field(None)
assert 'Don\'t know how to convert the Django form field' in str(excinfo.value)
def test_should_date_convert_string():
assert_conversion(forms.DateField, graphene.String)
def test_should_time_convert_string():
assert_conversion(forms.TimeField, graphene.String)
def test_should_date_time_convert_string():
assert_conversion(forms.DateTimeField, graphene.String)
def test_should_char_convert_string():
assert_conversion(forms.CharField, graphene.String)
def test_should_email_convert_string():
assert_conversion(forms.EmailField, graphene.String)
def test_should_slug_convert_string():
assert_conversion(forms.SlugField, graphene.String)
def test_should_url_convert_string():
assert_conversion(forms.URLField, graphene.String)
def test_should_choice_convert_string():
assert_conversion(forms.ChoiceField, graphene.String)
def test_should_base_field_convert_string():
assert_conversion(forms.Field, graphene.String)
def test_should_regex_convert_string():
assert_conversion(forms.RegexField, graphene.String, '[0-9]+')
def test_should_uuid_convert_string():
if hasattr(forms, 'UUIDField'):
assert_conversion(forms.UUIDField, graphene.String)
def test_should_integer_convert_int():
assert_conversion(forms.IntegerField, graphene.Int)
def test_should_boolean_convert_boolean():
field = assert_conversion(forms.BooleanField, graphene.Boolean)
assert field.required is True
def test_should_nullboolean_convert_boolean():
field = assert_conversion(forms.NullBooleanField, graphene.Boolean)
assert field.required is False
def test_should_float_convert_float():
assert_conversion(forms.FloatField, graphene.Float)
def test_should_decimal_convert_float():
assert_conversion(forms.DecimalField, graphene.Float)
def test_should_multiple_choice_convert_connectionorlist():
field = forms.ModelMultipleChoiceField(Reporter.objects.all())
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, List)
assert isinstance(graphene_type.of_type, ID)
def test_should_manytoone_convert_connectionorlist():
field = forms.ModelChoiceField(Reporter.objects.all())
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, graphene.ID)

View File

@ -0,0 +1,36 @@
from django.core.exceptions import ValidationError
from py.test import raises
from graphene.contrib.django.forms import GlobalIDFormField
# 'TXlUeXBlOjEwMA==' -> 'MyType', 100
# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc'
def test_global_id_valid():
field = GlobalIDFormField()
field.clean('TXlUeXBlOjEwMA==')
def test_global_id_invalid():
field = GlobalIDFormField()
with raises(ValidationError):
field.clean('badvalue')
def test_global_id_none():
field = GlobalIDFormField()
with raises(ValidationError):
field.clean(None)
def test_global_id_none_optional():
field = GlobalIDFormField(required=False)
field.clean(None)
def test_global_id_bad_int():
field = GlobalIDFormField()
with raises(ValidationError):
field.clean('TXlUeXBlOmFiYw==')

View File

@ -1,3 +1,4 @@
import pytest
from py.test import raises
import graphene
@ -6,6 +7,8 @@ from graphene.contrib.django import DjangoNode, DjangoObjectType
from .models import Article, Reporter
pytestmark = pytest.mark.django_db
def test_should_query_only_fields():
with raises(Exception):

View File

@ -1,7 +1,7 @@
from py.test import raises
from tests.utils import assert_equal_lists
from graphene.contrib.django import DjangoObjectType
from tests.utils import assert_equal_lists
from .models import Reporter

View File

@ -1,12 +1,12 @@
from graphql.core.type import GraphQLObjectType
from mock import patch
from tests.utils import assert_equal_lists
from graphene import Schema
from graphene.contrib.django.types import DjangoNode, DjangoObjectType
from graphene.core.fields import Field
from graphene.core.types.scalars import Int
from graphene.relay.fields import GlobalIDField
from tests.utils import assert_equal_lists
from .models import Article, Reporter

View File

@ -7,7 +7,7 @@ from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta
from ...relay.types import Connection, Node, NodeMeta
from .converter import convert_django_field
from .options import DjangoOptions
from .utils import get_reverse_fields, maybe_queryset
from .utils import get_reverse_fields
class DjangoObjectTypeMeta(ObjectTypeMeta):
@ -82,11 +82,7 @@ class DjangoObjectType(six.with_metaclass(
class DjangoConnection(Connection):
@classmethod
def from_list(cls, iterable, *args, **kwargs):
iterable = maybe_queryset(iterable)
return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs)
pass
class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta):
@ -112,5 +108,3 @@ class DjangoNode(six.with_metaclass(
return cls(instance)
except cls._meta.model.DoesNotExist:
return None
connection_type = DjangoConnection

View File

@ -4,6 +4,14 @@ from django.db.models.query import QuerySet
from graphene.utils import LazyList
from .compat import RelatedObject
try:
import django_filters # noqa
DJANGO_FILTER_INSTALLED = True
except ImportError:
DJANGO_FILTER_INSTALLED = False
def get_type_for_model(schema, model):
schema = schema
@ -17,8 +25,15 @@ def get_type_for_model(schema, model):
def get_reverse_fields(model):
for name, attr in model.__dict__.items():
related = getattr(attr, 'related', None)
if isinstance(related, models.ManyToOneRel):
# Django =>1.9 uses 'rel', django <1.9 uses 'related'
related = getattr(attr, 'rel', None) or \
getattr(attr, 'related', None)
if isinstance(related, RelatedObject):
# Hack for making it compatible with Django 1.6
new_related = RelatedObject(related.parent_model, related.model, related.field)
new_related.name = name
yield new_related
elif isinstance(related, models.ManyToOneRel):
yield related
@ -37,3 +52,33 @@ def maybe_queryset(value):
if isinstance(value, QuerySet):
return WrappedQueryset(value)
return value
def get_related_model(field):
if hasattr(field, 'rel'):
# Django 1.6, 1.7
return field.rel.to
return field.related_model
def import_single_dispatch():
try:
from functools import singledispatch
except ImportError:
singledispatch = None
if not singledispatch:
try:
from singledispatch import singledispatch
except ImportError:
pass
if not singledispatch:
raise Exception(
"It seems your python version does not include "
"functools.singledispatch. Please install the 'singledispatch' "
"package. More information here: "
"https://pypi.python.org/pypi/singledispatch"
)
return singledispatch

View File

@ -1,10 +1,10 @@
from graphql.core import graphql
from py.test import raises
from tests.utils import assert_equal_lists
from graphene import Interface, List, ObjectType, Schema, String
from graphene.core.fields import Field
from graphene.core.types.base import LazyType
from tests.utils import assert_equal_lists
schema = Schema(name='My own schema')

View File

@ -9,7 +9,8 @@ from ..classtypes.inputobjecttype import InputObjectType
from ..classtypes.mutation import Mutation
from ..exceptions import SkipField
from .argument import Argument, ArgumentsGroup, snake_case_args
from .base import GroupNamedType, LazyType, MountType, NamedType, ArgumentType, OrderedType
from .base import (ArgumentType, GroupNamedType, LazyType, MountType,
NamedType, OrderedType)
from .definitions import NonNull
@ -50,6 +51,16 @@ class Field(NamedType, OrderedType):
def resolver(self):
return self.resolver_fn or self.get_resolver_fn()
@property
def default(self):
if callable(self._default):
return self._default()
return self._default
@default.setter
def default(self, value):
self._default = value
def get_resolver_fn(self):
resolve_fn_name = 'resolve_%s' % self.attname
if hasattr(self.object_type, resolve_fn_name):

View File

@ -23,15 +23,15 @@ class ConnectionField(Field):
self.connection_type = connection_type
self.edge_type = edge_type
def wrap_resolved(self, value, instance, args, info):
return value
def resolver(self, instance, args, info):
schema = info.schema.graphene_schema
connection_type = self.get_type(schema)
resolved = super(ConnectionField, self).resolver(instance, args, info)
if isinstance(resolved, connection_type):
return resolved
return self.from_list(connection_type, resolved, args, info)
def from_list(self, connection_type, resolved, args, info):
return connection_type.from_list(resolved, args, info)
def get_connection_type(self, node):

View File

@ -4,7 +4,6 @@ from collections import Iterable
from functools import wraps
import six
from graphql_relay.connection.arrayconnection import connection_from_list
from graphql_relay.node.node import to_global_id

View File

@ -1,5 +1,5 @@
[flake8]
exclude = setup.py,docs/*
exclude = setup.py,docs/*,examples/cookbook_django/*
max-line-length = 120
[coverage:run]

View File

@ -55,17 +55,18 @@ setup(
install_requires=[
'six>=1.10.0',
'graphql-core==0.4.9',
'graphql-relay==0.3.3'
'graphql-core>=0.4.9',
'graphql-relay==0.3.3',
],
tests_require=[
'django-filter>=0.10.0',
'pytest>=2.7.2',
'pytest-django',
'mock',
],
extras_require={
'django': [
'Django>=1.6.0,<1.9',
'Django>=1.6.0',
'singledispatch>=3.4.0.3',
'graphql-django-view>=1.1.0',
],