mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-20 13:30:38 +03:00
Merge branch 'refs/heads/master' into features/django-debug-optimize
Conflicts: graphene/contrib/django/converter.py graphene/contrib/django/fields.py graphene/contrib/django/tests/test_converter.py
This commit is contained in:
commit
6e3fe9c40f
14
.editorconfig
Normal file
14
.editorconfig
Normal 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
6
.gitignore
vendored
|
@ -72,3 +72,9 @@ target/
|
||||||
/docs/static/playground/lib
|
/docs/static/playground/lib
|
||||||
|
|
||||||
/docs/static/playground
|
/docs/static/playground
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Databases
|
||||||
|
*.sqlite3
|
||||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -2,7 +2,6 @@ language: python
|
||||||
sudo: false
|
sudo: false
|
||||||
python:
|
python:
|
||||||
- 2.7
|
- 2.7
|
||||||
- 3.3
|
|
||||||
- 3.4
|
- 3.4
|
||||||
- 3.5
|
- 3.5
|
||||||
- pypy
|
- pypy
|
||||||
|
@ -24,8 +23,9 @@ before_install:
|
||||||
install:
|
install:
|
||||||
- |
|
- |
|
||||||
if [ "$TEST_TYPE" = build ]; then
|
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 --download-cache $HOME/.cache/pip/ -e .[django]
|
||||||
|
pip install django==$DJANGO_VERSION
|
||||||
python setup.py develop
|
python setup.py develop
|
||||||
elif [ "$TEST_TYPE" = build_website ]; then
|
elif [ "$TEST_TYPE" = build_website ]; then
|
||||||
pip install --download-cache $HOME/.cache/pip/ -e .
|
pip install --download-cache $HOME/.cache/pip/ -e .
|
||||||
|
@ -79,6 +79,14 @@ env:
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
include:
|
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'
|
- python: '2.7'
|
||||||
env: TEST_TYPE=build_website
|
env: TEST_TYPE=build_website
|
||||||
- python: '2.7'
|
- python: '2.7'
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#!/bin/bash
|
#!/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
|
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
|
autopep8 ./examples/ ./graphene/ -r --in-place --experimental --aggressive --max-line-length 120
|
||||||
isort -rc ./examples/ ./graphene/
|
isort -rc ./examples/ ./graphene/
|
||||||
|
|
|
@ -5,7 +5,6 @@ ga = "UA-12613282-7"
|
||||||
name = "Quickstart"
|
name = "Quickstart"
|
||||||
pages = [
|
pages = [
|
||||||
"/docs/quickstart/",
|
"/docs/quickstart/",
|
||||||
"/docs/quickstart-django/",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[docs.walkthrough]
|
[docs.walkthrough]
|
||||||
|
@ -17,3 +16,10 @@ ga = "UA-12613282-7"
|
||||||
"/docs/basic-types/",
|
"/docs/basic-types/",
|
||||||
"/docs/relay/",
|
"/docs/relay/",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[docs.django]
|
||||||
|
name = "Django"
|
||||||
|
pages = [
|
||||||
|
"/docs/django/tutorial/",
|
||||||
|
"/docs/django/filtering/",
|
||||||
|
]
|
||||||
|
|
|
@ -17,15 +17,17 @@
|
||||||
"copy-webpack-plugin": "^0.2.0",
|
"copy-webpack-plugin": "^0.2.0",
|
||||||
"es6-promise": "^3.0.2",
|
"es6-promise": "^3.0.2",
|
||||||
"extract-text-webpack-plugin": "^0.9.1",
|
"extract-text-webpack-plugin": "^0.9.1",
|
||||||
"gatsby": "^0.7.3",
|
"gatsby": "^0.7.7",
|
||||||
"graphiql": "^0.4.2",
|
"graphiql": "^0.4.2",
|
||||||
"graphql": "^0.4.13",
|
"graphql": "^0.4.13",
|
||||||
"jeet": "^6.1.2",
|
"jeet": "^6.1.2",
|
||||||
"lodash": "^3.10.1",
|
"lodash": "^3.10.1",
|
||||||
"nib": "^1.1.0",
|
"nib": "^1.1.0",
|
||||||
"react": "^0.14.3",
|
"react": "^0.14.6",
|
||||||
"react-burger-menu": "^1.4.2",
|
"radium": "0.14.2",
|
||||||
|
"react-burger-menu": "^1.4.12",
|
||||||
"react-document-title": "^2.0.1",
|
"react-document-title": "^2.0.1",
|
||||||
|
"react-dom": "^0.14.6",
|
||||||
"react-router": "^0.13.5",
|
"react-router": "^0.13.5",
|
||||||
"rupture": "^0.6.1",
|
"rupture": "^0.6.1",
|
||||||
"stylus-loader": "^1.4.2",
|
"stylus-loader": "^1.4.2",
|
||||||
|
|
|
@ -82,3 +82,21 @@ graphene.Field(graphene.String(), to=graphene.String())
|
||||||
# Is equivalent to:
|
# Is equivalent to:
|
||||||
graphene.Field(graphene.String(), to=graphene.Argument(graphene.String()))
|
graphene.Field(graphene.String(), to=graphene.Argument(graphene.String()))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Using custom object types as argument
|
||||||
|
|
||||||
|
To use a custom object type as an argument, you need to inherit `graphene.InputObjectType`, not `graphene.ObjectType`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CustomArgumentObjectType(graphene.InputObjectType):
|
||||||
|
field1 = graphene.String()
|
||||||
|
field2 = graphene.String()
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, when defining this in an argument, you need to wrap it in an `Argument` object.
|
||||||
|
|
||||||
|
```python
|
||||||
|
graphene.Field(graphene.String(), to=graphene.Argument(CustomArgumentObjectType))
|
||||||
|
```
|
||||||
|
|
159
docs/pages/docs/django/filtering.md
Normal file
159
docs/pages/docs/django/filtering.md
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
---
|
||||||
|
title: Filtering
|
||||||
|
description: Details of how to perform filtering in Graphene Django
|
||||||
|
---
|
||||||
|
|
||||||
|
# Filtering
|
||||||
|
|
||||||
|
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/master/examples/cookbook_django).**
|
||||||
|
|
||||||
|
## 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(name_Icontains: "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)
|
||||||
|
```
|
293
docs/pages/docs/django/tutorial.md
Normal file
293
docs/pages/docs/django/tutorial.md
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
---
|
||||||
|
title: Quickstart
|
||||||
|
description: A Quick guide to Graphene in Django
|
||||||
|
---
|
||||||
|
|
||||||
|
# Django Tutorial
|
||||||
|
|
||||||
|
Graphene has a number of additional features that are designed to make
|
||||||
|
working with Django *really simple*.
|
||||||
|
|
||||||
|
**Note: The code in this quickstart is pulled from the
|
||||||
|
[cookbook example app](https://github.com/graphql-python/graphene/tree/master/examples/cookbook_django)**.
|
||||||
|
|
||||||
|
|
||||||
|
## Setup the Django project
|
||||||
|
|
||||||
|
We will setup the project, create the following:
|
||||||
|
|
||||||
|
* A Django project called `cookbook`
|
||||||
|
* An app within `cookbook` called `ingredients`
|
||||||
|
|
||||||
|
```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]
|
||||||
|
pip install django-graphiql
|
||||||
|
|
||||||
|
# Set up a new project with a single application
|
||||||
|
django-admin.py startproject cookbook . # Note the trailing '.' character
|
||||||
|
django-admin.py startapp ingredients
|
||||||
|
```
|
||||||
|
|
||||||
|
Now sync your database for the first time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's create a few simple models...
|
||||||
|
|
||||||
|
|
||||||
|
## Defining our models
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
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.
|
||||||
|
In this example, we provide the ability to list all users via `all_users`, and the
|
||||||
|
ability to obtain a specific user via `get_user`.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
import cookbook.ingredients.schema
|
||||||
|
|
||||||
|
|
||||||
|
class Query(cookbook.ingredients.schema.Query):
|
||||||
|
# This class will inherit from multiple Queries
|
||||||
|
# as we begin to add more apps to our project
|
||||||
|
pass
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
...
|
||||||
|
'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 GraphQL is accessed.
|
||||||
|
Requests to this URL are handled by Graphene's `GraphQLView` view.
|
||||||
|
|
||||||
|
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 cookbook.schema import 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 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
|
||||||
|
|
||||||
|
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/graphiql](http://localhost:8000/graphiql) and type your first query!
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
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(name_Icontains: "e", categoryName: "Meat") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
50
docs/pages/docs/introspection-schema.md
Normal file
50
docs/pages/docs/introspection-schema.md
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
title: Introspection Schema
|
||||||
|
description: A guide to instrospection schema in Django
|
||||||
|
---
|
||||||
|
|
||||||
|
# Introspection Schema
|
||||||
|
|
||||||
|
Relay uses [Babel Relay Plugin](https://facebook.github.io/relay/docs/guides-babel-plugin.html)
|
||||||
|
that requires you to provide your GraphQL schema data.
|
||||||
|
|
||||||
|
Graphene comes with a management command for Django to dump your schema data to
|
||||||
|
`schema.json` that is compatible with babel-relay-plugin.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Include `graphene.django.contrib` to `INSTALLED_APPS` in you project settings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
INSTALLED_APPS += ('graphene.django.contrib')
|
||||||
|
```
|
||||||
|
|
||||||
|
Assuming your Graphene schema is at `tutorial.quickstart.schema`, run the command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
It dumps your full introspection schema to `schema.json` inside your project root
|
||||||
|
directory. Point `babel-relay-plugin` to this file and you're ready to use Relay
|
||||||
|
with Graphene GraphQL implementation.
|
||||||
|
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
To simplify the command to `./manage.py graphql_schema`, you can specify the
|
||||||
|
parameters in your settings.py:
|
||||||
|
|
||||||
|
```python
|
||||||
|
GRAPHENE_SCHEMA = 'tutorial.quickstart.schema'
|
||||||
|
GRAPHENE_SCHEMA_OUTPUT = 'data/schema.json' # defaults to schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Running `./manage.py graphql_schema` dumps your schema to
|
||||||
|
`<project root>/data/schema.json`.
|
||||||
|
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
Run `./manage.py graphql_schema -h` for command usage.
|
|
@ -22,7 +22,7 @@ class CreatePerson(graphene.Mutation):
|
||||||
person = graphene.Field('Person')
|
person = graphene.Field('Person')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mutate(cls, args, info):
|
def mutate(cls, instance, args, info):
|
||||||
person = Person(name=args.get('name'))
|
person = Person(name=args.get('name'))
|
||||||
ok = True
|
ok = True
|
||||||
return CreatePerson(person=person, ok=ok)
|
return CreatePerson(person=person, ok=ok)
|
||||||
|
|
|
@ -1,154 +0,0 @@
|
||||||
---
|
|
||||||
title: Django Tutorial
|
|
||||||
description: A Quick guide to Graphene in Django
|
|
||||||
---
|
|
||||||
|
|
||||||
# Django Tutorial
|
|
||||||
|
|
||||||
In our previous quickstart page we created a very simple schema.
|
|
||||||
|
|
||||||
Now we will adapt the schema to automatically map some Django models,
|
|
||||||
and expose this schema in a `/graphql` API endpoint.
|
|
||||||
|
|
||||||
## Project setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create the project directory
|
|
||||||
mkdir tutorial
|
|
||||||
cd tutorial
|
|
||||||
|
|
||||||
# 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]
|
|
||||||
pip install django-graphiql
|
|
||||||
|
|
||||||
# Set up a new project with a single application
|
|
||||||
django-admin.py startproject tutorial . # Note the trailing '.' character
|
|
||||||
django-admin.py startapp quickstart
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
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`.
|
|
||||||
|
|
||||||
Open `tutorial/quickstart/schema.py` and type the following:
|
|
||||||
|
|
||||||
```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')
|
|
||||||
|
|
||||||
|
|
||||||
class GroupType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Group
|
|
||||||
only_fields = ('name', )
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Adding GraphiQL
|
|
||||||
|
|
||||||
For having the GraphiQL static assets we need to append `django_graphiql` in `INSTALLED_APPS` in `tutorial/settings.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
# The other installed apps
|
|
||||||
'django_graphiql',
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Creating GraphQL and GraphiQL views
|
|
||||||
|
|
||||||
Unlike a RESTful API, there is only a single URL from which a 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. We’ll add a URL for this too.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from django.conf.urls import url, include
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from graphene.contrib.django.views import GraphQLView
|
|
||||||
from quickstart.schema import schema
|
|
||||||
|
|
||||||
|
|
||||||
# Wire up our GraphQL schema to /graphql.
|
|
||||||
# Additionally, we include GraphiQL view for querying easily our schema.
|
|
||||||
urlpatterns = [
|
|
||||||
url(r'^graphql', csrf_exempt(GraphQLView.as_view(schema=schema))),
|
|
||||||
url(r'^graphiql', include('django_graphiql.urls')),
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
```
|
|
||||||
|
|
||||||
Go to [localhost:8080/graphiql](http://localhost:8080/graphiql) and type your first query!
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
myQuery {
|
|
||||||
getUser(id:"1") {
|
|
||||||
username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
64
examples/cookbook_django/README.md
Normal file
64
examples/cookbook_django/README.md
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
Cookbook Example Django Project
|
||||||
|
===============================
|
||||||
|
|
||||||
|
This example project demos integration between Graphene and Django.
|
||||||
|
The project contains two apps, one named `ingredients` and another
|
||||||
|
named `recepies`.
|
||||||
|
|
||||||
|
Getting started
|
||||||
|
---------------
|
||||||
|
|
||||||
|
First you'll need to get the source of the project. Do this by cloning the
|
||||||
|
whole Graphene repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get the example project code
|
||||||
|
git clone https://github.com/graphql-python/graphene.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)
|
0
examples/cookbook_django/cookbook/__init__.py
Normal file
0
examples/cookbook_django/cookbook/__init__.py
Normal file
6
examples/cookbook_django/cookbook/ingredients/admin.py
Normal file
6
examples/cookbook_django/cookbook/ingredients/admin.py
Normal 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)
|
7
examples/cookbook_django/cookbook/ingredients/apps.py
Normal file
7
examples/cookbook_django/cookbook/ingredients/apps.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientsConfig(AppConfig):
|
||||||
|
name = 'cookbook.ingredients'
|
||||||
|
label = 'ingredients'
|
||||||
|
verbose_name = 'Ingredients'
|
|
@ -0,0 +1 @@
|
||||||
|
[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}]
|
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2015-12-04 18:15
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Ingredient',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('notes', models.TextField()),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
17
examples/cookbook_django/cookbook/ingredients/models.py
Normal file
17
examples/cookbook_django/cookbook/ingredients/models.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Category(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Ingredient(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
notes = models.TextField()
|
||||||
|
category = models.ForeignKey(Category, related_name='ingredients')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
39
examples/cookbook_django/cookbook/ingredients/schema.py
Normal file
39
examples/cookbook_django/cookbook/ingredients/schema.py
Normal 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
|
3
examples/cookbook_django/cookbook/ingredients/tests.py
Normal file
3
examples/cookbook_django/cookbook/ingredients/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
examples/cookbook_django/cookbook/ingredients/views.py
Normal file
3
examples/cookbook_django/cookbook/ingredients/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
6
examples/cookbook_django/cookbook/recipes/admin.py
Normal file
6
examples/cookbook_django/cookbook/recipes/admin.py
Normal 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)
|
7
examples/cookbook_django/cookbook/recipes/apps.py
Normal file
7
examples/cookbook_django/cookbook/recipes/apps.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RecipesConfig(AppConfig):
|
||||||
|
name = 'cookbook.recipes'
|
||||||
|
label = 'recipes'
|
||||||
|
verbose_name = 'Recipes'
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2015-12-04 18:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
19
examples/cookbook_django/cookbook/recipes/models.py
Normal file
19
examples/cookbook_django/cookbook/recipes/models.py
Normal 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'),
|
||||||
|
))
|
3
examples/cookbook_django/cookbook/recipes/tests.py
Normal file
3
examples/cookbook_django/cookbook/recipes/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
examples/cookbook_django/cookbook/recipes/views.py
Normal file
3
examples/cookbook_django/cookbook/recipes/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
9
examples/cookbook_django/cookbook/schema.py
Normal file
9
examples/cookbook_django/cookbook/schema.py
Normal 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
|
125
examples/cookbook_django/cookbook/settings.py
Normal file
125
examples/cookbook_django/cookbook/settings.py
Normal 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/'
|
12
examples/cookbook_django/cookbook/urls.py
Normal file
12
examples/cookbook_django/cookbook/urls.py
Normal 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')),
|
||||||
|
]
|
16
examples/cookbook_django/cookbook/wsgi.py
Normal file
16
examples/cookbook_django/cookbook/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for cookbook project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
10
examples/cookbook_django/manage.py
Executable file
10
examples/cookbook_django/manage.py
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings")
|
||||||
|
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
5
examples/cookbook_django/requirements.txt
Normal file
5
examples/cookbook_django/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
graphene[django]
|
||||||
|
django_graphiql
|
||||||
|
graphql-core
|
||||||
|
django==1.9
|
||||||
|
django-filter==0.11.0
|
|
@ -9,4 +9,4 @@ from graphene.contrib.django.fields import (
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection',
|
__all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection',
|
||||||
'DjangoConnectionField', 'DjangoModelField']
|
'DjangoModelField', 'DjangoConnectionField']
|
||||||
|
|
15
graphene/contrib/django/compat.py
Normal file
15
graphene/contrib/django/compat.py
Normal 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
|
|
@ -1,15 +1,11 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from singledispatch import singledispatch
|
|
||||||
|
|
||||||
from ...core.types.scalars import ID, Boolean, Float, Int, String
|
from ...core.types.scalars import ID, Boolean, Float, Int, String
|
||||||
from .fields import DjangoField, ConnectionOrListField, DjangoModelField
|
from .fields import DjangoField, ConnectionOrListField, DjangoModelField
|
||||||
|
from .compat import RelatedObject, UUIDField
|
||||||
|
from .utils import get_related_model, import_single_dispatch
|
||||||
|
|
||||||
try:
|
singledispatch = import_single_dispatch()
|
||||||
UUIDField = models.UUIDField
|
|
||||||
except AttributeError:
|
|
||||||
# Improved compatibility for Django 1.6
|
|
||||||
class UUIDField(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
@singledispatch
|
||||||
|
@ -33,6 +29,7 @@ def fetch_field(f):
|
||||||
@convert_django_field.register(models.EmailField)
|
@convert_django_field.register(models.EmailField)
|
||||||
@convert_django_field.register(models.SlugField)
|
@convert_django_field.register(models.SlugField)
|
||||||
@convert_django_field.register(models.URLField)
|
@convert_django_field.register(models.URLField)
|
||||||
|
@convert_django_field.register(models.GenericIPAddressField)
|
||||||
@convert_django_field.register(UUIDField)
|
@convert_django_field.register(UUIDField)
|
||||||
@fetch_field
|
@fetch_field
|
||||||
def convert_field_to_string(field):
|
def convert_field_to_string(field):
|
||||||
|
@ -76,12 +73,22 @@ def convert_field_to_float(field):
|
||||||
|
|
||||||
@convert_django_field.register(models.ManyToManyField)
|
@convert_django_field.register(models.ManyToManyField)
|
||||||
@convert_django_field.register(models.ManyToOneRel)
|
@convert_django_field.register(models.ManyToOneRel)
|
||||||
|
@convert_django_field.register(models.ManyToManyRel)
|
||||||
def convert_field_to_list_or_connection(field):
|
def convert_field_to_list_or_connection(field):
|
||||||
model_field = DjangoModelField(field.related_model)
|
model_field = DjangoModelField(get_related_model(field))
|
||||||
|
return ConnectionOrListField(model_field, _field=field)
|
||||||
|
|
||||||
|
|
||||||
|
# For Django 1.6
|
||||||
|
@convert_django_field.register(RelatedObject)
|
||||||
|
def convert_relatedfield_to_djangomodel(field):
|
||||||
|
model_field = DjangoModelField(field.model)
|
||||||
return ConnectionOrListField(model_field, _field=field)
|
return ConnectionOrListField(model_field, _field=field)
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(models.OneToOneField)
|
@convert_django_field.register(models.OneToOneField)
|
||||||
@convert_django_field.register(models.ForeignKey)
|
@convert_django_field.register(models.ForeignKey)
|
||||||
def convert_field_to_djangomodel(field):
|
def convert_field_to_djangomodel(field):
|
||||||
return DjangoField(DjangoModelField(field.related_model), description=field.help_text, _field=field)
|
related_model = get_related_model(field)
|
||||||
|
return DjangoField(DjangoModelField(related_model), description=field.help_text, _field=field)
|
||||||
|
# return DjangoModelField(get_related_model(field), description=field.help_text, _field=field)
|
||||||
|
|
|
@ -2,8 +2,8 @@ from contextlib import contextmanager
|
||||||
|
|
||||||
from django.db import connections
|
from django.db import connections
|
||||||
|
|
||||||
from ....core.types import Field
|
|
||||||
from ....core.schema import GraphQLSchema
|
from ....core.schema import GraphQLSchema
|
||||||
|
from ....core.types import Field
|
||||||
from ....plugins import Plugin
|
from ....plugins import Plugin
|
||||||
from .sql.tracking import unwrap_cursor, wrap_cursor
|
from .sql.tracking import unwrap_cursor, wrap_cursor
|
||||||
from .sql.types import DjangoDebugSQL
|
from .sql.types import DjangoDebugSQL
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .....core import Float, ObjectType, String, Boolean
|
from .....core import Boolean, Float, ObjectType, String
|
||||||
|
|
||||||
|
|
||||||
class DjangoDebugSQL(ObjectType):
|
class DjangoDebugSQL(ObjectType):
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from ...core.exceptions import SkipField
|
from ...core.exceptions import SkipField
|
||||||
from ...core.fields import Field
|
from ...core.fields import Field
|
||||||
from ...core.types.base import FieldType
|
from ...core.types.base import FieldType
|
||||||
from ...core.types.definitions import List
|
from ...core.types.definitions import List
|
||||||
from ...relay import ConnectionField
|
from ...relay import ConnectionField
|
||||||
from ...relay.utils import is_node
|
from ...relay.utils import is_node
|
||||||
from .utils import get_type_for_model
|
from .utils import DJANGO_FILTER_INSTALLED, get_type_for_model, maybe_queryset
|
||||||
|
|
||||||
|
|
||||||
class DjangoField(Field):
|
class DjangoField(Field):
|
||||||
|
@ -16,28 +14,51 @@ class DjangoField(Field):
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.field = kwargs.pop('_field')
|
self.field = kwargs.pop('_field', None)
|
||||||
return super(DjangoField, self).__init__(*args, **kwargs)
|
return super(DjangoField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class DjangoConnectionField(DjangoField, ConnectionField):
|
class DjangoConnectionField(DjangoField, ConnectionField):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
cls = self.__class__
|
self.on = kwargs.pop('on', False)
|
||||||
warnings.warn("Using {} will be not longer supported."
|
|
||||||
" Use relay.ConnectionField instead".format(cls.__name__),
|
|
||||||
FutureWarning)
|
|
||||||
return super(DjangoConnectionField, self).__init__(*args, **kwargs)
|
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(DjangoField):
|
class ConnectionOrListField(DjangoField):
|
||||||
|
|
||||||
def internal_type(self, schema):
|
def internal_type(self, schema):
|
||||||
|
if DJANGO_FILTER_INSTALLED:
|
||||||
|
from .filter.fields import DjangoFilterConnectionField
|
||||||
|
|
||||||
model_field = self.type
|
model_field = self.type
|
||||||
field_object_type = model_field.get_object_type(schema)
|
field_object_type = model_field.get_object_type(schema)
|
||||||
if not field_object_type:
|
if not field_object_type:
|
||||||
raise SkipField()
|
raise SkipField()
|
||||||
if is_node(field_object_type):
|
if is_node(field_object_type):
|
||||||
|
if field_object_type._meta.filter_fields:
|
||||||
|
field = DjangoFilterConnectionField(field_object_type, _field=self.field)
|
||||||
|
else:
|
||||||
field = DjangoConnectionField(field_object_type, _field=self.field)
|
field = DjangoConnectionField(field_object_type, _field=self.field)
|
||||||
else:
|
else:
|
||||||
field = DjangoField(List(field_object_type), _field=self.field)
|
field = DjangoField(List(field_object_type), _field=self.field)
|
||||||
|
|
14
graphene/contrib/django/filter/__init__.py
Normal file
14
graphene/contrib/django/filter/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import warnings
|
||||||
|
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
|
if not DJANGO_FILTER_INSTALLED:
|
||||||
|
warnings.warn(
|
||||||
|
"Use of django filtering requires the django-filter package "
|
||||||
|
"be installed. You can do so using `pip install django-filter`", ImportWarning
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from .fields import DjangoFilterConnectionField
|
||||||
|
from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter
|
||||||
|
|
||||||
|
__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet',
|
||||||
|
'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter']
|
36
graphene/contrib/django/filter/fields.py
Normal file
36
graphene/contrib/django/filter/fields.py
Normal 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)
|
116
graphene/contrib/django/filter/filterset.py
Normal file
116
graphene/contrib/django/filter/filterset.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import six
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.text import capfirst
|
||||||
|
from graphql_relay.node.node import from_global_id
|
||||||
|
|
||||||
|
from django_filters import Filter, MultipleChoiceFilter
|
||||||
|
from django_filters.filterset import FilterSet, FilterSetMetaclass
|
||||||
|
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
|
0
graphene/contrib/django/filter/tests/__init__.py
Normal file
0
graphene/contrib/django/filter/tests/__init__.py
Normal file
30
graphene/contrib/django/filter/tests/filters.py
Normal file
30
graphene/contrib/django/filter/tests/filters.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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
|
287
graphene/contrib/django/filter/tests/test_fields.py
Normal file
287
graphene/contrib/django/filter/tests/test_fields.py
Normal 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', 'headline_Icontains',
|
||||||
|
'pubDate', 'pubDate_Gt', 'pubDate_Lt',
|
||||||
|
'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', 'headline_Icontains',
|
||||||
|
'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
|
31
graphene/contrib/django/filter/utils.py
Normal file
31
graphene/contrib/django/filter/utils.py
Normal 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)
|
73
graphene/contrib/django/form_converter.py
Normal file
73
graphene/contrib/django/form_converter.py
Normal 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()
|
41
graphene/contrib/django/forms.py
Normal file
41
graphene/contrib/django/forms.py
Normal 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
|
0
graphene/contrib/django/management/__init__.py
Normal file
0
graphene/contrib/django/management/__init__.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
from distutils.version import StrictVersion
|
||||||
|
from optparse import make_option
|
||||||
|
|
||||||
|
from django import get_version as get_django_version
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
LT_DJANGO_1_8 = StrictVersion(get_django_version()) < StrictVersion('1.8')
|
||||||
|
|
||||||
|
if LT_DJANGO_1_8:
|
||||||
|
class CommandArguments(BaseCommand):
|
||||||
|
option_list = BaseCommand.option_list + (
|
||||||
|
make_option(
|
||||||
|
'--schema',
|
||||||
|
type=str,
|
||||||
|
dest='schema',
|
||||||
|
default='',
|
||||||
|
help='Django app containing schema to dump, e.g. myproject.core.schema',
|
||||||
|
),
|
||||||
|
make_option(
|
||||||
|
'--out',
|
||||||
|
type=str,
|
||||||
|
dest='out',
|
||||||
|
default='',
|
||||||
|
help='Output file (default: schema.json)'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
class CommandArguments(BaseCommand):
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
from django.conf import settings
|
||||||
|
parser.add_argument(
|
||||||
|
'--schema',
|
||||||
|
type=str,
|
||||||
|
dest='schema',
|
||||||
|
default=getattr(settings, 'GRAPHENE_SCHEMA', ''),
|
||||||
|
help='Django app containing schema to dump, e.g. myproject.core.schema')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--out',
|
||||||
|
type=str,
|
||||||
|
dest='out',
|
||||||
|
default=getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json'),
|
||||||
|
help='Output file (default: schema.json)')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(CommandArguments):
|
||||||
|
help = 'Dump Graphene schema JSON to file'
|
||||||
|
can_import_settings = True
|
||||||
|
|
||||||
|
def save_file(self, out, schema_dict):
|
||||||
|
with open(out, 'w') as outfile:
|
||||||
|
json.dump(schema_dict, outfile)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
from django.conf import settings
|
||||||
|
schema = options.get('schema') or getattr(settings, 'GRAPHENE_SCHEMA', '')
|
||||||
|
out = options.get('out') or getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json')
|
||||||
|
|
||||||
|
if schema == '':
|
||||||
|
raise CommandError('Specify schema on GRAPHENE_SCHEMA setting or by using --schema')
|
||||||
|
i = importlib.import_module(schema)
|
||||||
|
|
||||||
|
schema_dict = {'data': i.schema.introspect()}
|
||||||
|
self.save_file(out, schema_dict)
|
||||||
|
|
||||||
|
style = getattr(self, 'style', None)
|
||||||
|
SUCCESS = getattr(style, 'SUCCESS', lambda x: x)
|
||||||
|
|
||||||
|
self.stdout.write(SUCCESS('Successfully dumped GraphQL schema to %s' % out))
|
|
@ -1,9 +1,13 @@
|
||||||
from ...core.classtypes.objecttype import ObjectTypeOptions
|
from ...core.classtypes.objecttype import ObjectTypeOptions
|
||||||
from ...relay.types import Node
|
from ...relay.types import Node
|
||||||
from ...relay.utils import is_node
|
from ...relay.utils import is_node
|
||||||
|
from .utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
VALID_ATTRS = ('model', 'only_fields', 'exclude_fields')
|
VALID_ATTRS = ('model', 'only_fields', 'exclude_fields')
|
||||||
|
|
||||||
|
if DJANGO_FILTER_INSTALLED:
|
||||||
|
VALID_ATTRS += ('filter_fields', 'filter_order_by')
|
||||||
|
|
||||||
|
|
||||||
class DjangoOptions(ObjectTypeOptions):
|
class DjangoOptions(ObjectTypeOptions):
|
||||||
|
|
||||||
|
@ -13,6 +17,8 @@ class DjangoOptions(ObjectTypeOptions):
|
||||||
self.valid_attrs += VALID_ATTRS
|
self.valid_attrs += VALID_ATTRS
|
||||||
self.only_fields = None
|
self.only_fields = None
|
||||||
self.exclude_fields = []
|
self.exclude_fields = []
|
||||||
|
self.filter_fields = None
|
||||||
|
self.filter_order_by = None
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
super(DjangoOptions, self).contribute_to_class(cls, name)
|
super(DjangoOptions, self).contribute_to_class(cls, name)
|
||||||
|
|
|
@ -7,6 +7,11 @@ class Pet(models.Model):
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
|
|
||||||
|
|
||||||
|
class Film(models.Model):
|
||||||
|
reporters = models.ManyToManyField('Reporter',
|
||||||
|
related_name='films')
|
||||||
|
|
||||||
|
|
||||||
class Reporter(models.Model):
|
class Reporter(models.Model):
|
||||||
first_name = models.CharField(max_length=30)
|
first_name = models.CharField(max_length=30)
|
||||||
last_name = models.CharField(max_length=30)
|
last_name = models.CharField(max_length=30)
|
||||||
|
|
11
graphene/contrib/django/tests/test_command.py
Normal file
11
graphene/contrib/django/tests/test_command.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from django.core import management
|
||||||
|
from mock import patch
|
||||||
|
from six import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
@patch('graphene.contrib.django.management.commands.graphql_schema.Command.save_file')
|
||||||
|
def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
|
||||||
|
settings.GRAPHENE_SCHEMA = 'graphene.contrib.django.tests.test_urls'
|
||||||
|
out = StringIO()
|
||||||
|
management.call_command('graphql_schema', schema='', stdout=out)
|
||||||
|
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
|
|
@ -9,8 +9,8 @@ from graphene.contrib.django.fields import (ConnectionOrListField,
|
||||||
from .models import Article, Reporter
|
from .models import Article, Reporter
|
||||||
|
|
||||||
|
|
||||||
def assert_conversion(django_field, graphene_field, *args):
|
def assert_conversion(django_field, graphene_field, *args, **kwargs):
|
||||||
field = django_field(*args, help_text='Custom Help Text')
|
field = django_field(help_text='Custom Help Text', *args, **kwargs)
|
||||||
field = convert_django_field(field)
|
field = convert_django_field(field)
|
||||||
graphene_type = field.type
|
graphene_type = field.type
|
||||||
assert isinstance(graphene_type, graphene_field)
|
assert isinstance(graphene_type, graphene_field)
|
||||||
|
@ -48,8 +48,12 @@ def test_should_url_convert_string():
|
||||||
assert_conversion(models.URLField, graphene.String)
|
assert_conversion(models.URLField, graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_ipaddress_convert_string():
|
||||||
|
assert_conversion(models.GenericIPAddressField, graphene.String)
|
||||||
|
|
||||||
|
|
||||||
def test_should_auto_convert_id():
|
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():
|
def test_should_positive_integer_convert_int():
|
||||||
|
@ -94,7 +98,10 @@ def test_should_manytomany_convert_connectionorlist():
|
||||||
|
|
||||||
|
|
||||||
def test_should_manytoone_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, ConnectionOrListField)
|
||||||
assert isinstance(graphene_type.type, DjangoModelField)
|
assert isinstance(graphene_type.type, DjangoModelField)
|
||||||
assert graphene_type.type.model == Article
|
assert graphene_type.type.model == Article
|
||||||
|
|
103
graphene/contrib/django/tests/test_form_converter.py
Normal file
103
graphene/contrib/django/tests/test_form_converter.py
Normal 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)
|
36
graphene/contrib/django/tests/test_forms.py
Normal file
36
graphene/contrib/django/tests/test_forms.py
Normal 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==')
|
|
@ -1,3 +1,4 @@
|
||||||
|
import pytest
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
@ -6,6 +7,8 @@ from graphene.contrib.django import DjangoNode, DjangoObjectType
|
||||||
|
|
||||||
from .models import Article, Reporter
|
from .models import Article, Reporter
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_only_fields():
|
def test_should_query_only_fields():
|
||||||
with raises(Exception):
|
with raises(Exception):
|
||||||
|
|
|
@ -29,7 +29,7 @@ def test_should_map_fields_correctly():
|
||||||
model = Reporter
|
model = Reporter
|
||||||
assert_equal_lists(
|
assert_equal_lists(
|
||||||
ReporterType2._meta.fields_map.keys(),
|
ReporterType2._meta.fields_map.keys(),
|
||||||
['articles', 'first_name', 'last_name', 'email', 'pets', 'id']
|
['articles', 'first_name', 'last_name', 'email', 'pets', 'id', 'films']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta
|
||||||
from ...relay.types import Connection, Node, NodeMeta
|
from ...relay.types import Connection, Node, NodeMeta
|
||||||
from .converter import convert_django_field
|
from .converter import convert_django_field
|
||||||
from .options import DjangoOptions
|
from .options import DjangoOptions
|
||||||
from .utils import get_reverse_fields, maybe_queryset
|
from .utils import get_reverse_fields
|
||||||
|
|
||||||
|
|
||||||
class DjangoObjectTypeMeta(ObjectTypeMeta):
|
class DjangoObjectTypeMeta(ObjectTypeMeta):
|
||||||
|
@ -82,11 +82,7 @@ class DjangoObjectType(six.with_metaclass(
|
||||||
|
|
||||||
|
|
||||||
class DjangoConnection(Connection):
|
class DjangoConnection(Connection):
|
||||||
|
pass
|
||||||
@classmethod
|
|
||||||
def from_list(cls, iterable, *args, **kwargs):
|
|
||||||
iterable = maybe_queryset(iterable)
|
|
||||||
return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta):
|
class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta):
|
||||||
|
@ -112,5 +108,3 @@ class DjangoNode(six.with_metaclass(
|
||||||
return cls(instance)
|
return cls(instance)
|
||||||
except cls._meta.model.DoesNotExist:
|
except cls._meta.model.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
connection_type = DjangoConnection
|
|
||||||
|
|
|
@ -4,6 +4,15 @@ from django.db.models.query import QuerySet
|
||||||
|
|
||||||
from graphene.utils import LazyList
|
from graphene.utils import LazyList
|
||||||
|
|
||||||
|
from .compat import RelatedObject
|
||||||
|
|
||||||
|
try:
|
||||||
|
import django_filters # noqa
|
||||||
|
DJANGO_FILTER_INSTALLED = True
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
# AtributeError raised if DjangoFilters installed with a incompatible Django Version
|
||||||
|
DJANGO_FILTER_INSTALLED = False
|
||||||
|
|
||||||
|
|
||||||
def get_type_for_model(schema, model):
|
def get_type_for_model(schema, model):
|
||||||
schema = schema
|
schema = schema
|
||||||
|
@ -17,8 +26,17 @@ def get_type_for_model(schema, model):
|
||||||
|
|
||||||
def get_reverse_fields(model):
|
def get_reverse_fields(model):
|
||||||
for name, attr in model.__dict__.items():
|
for name, attr in model.__dict__.items():
|
||||||
related = getattr(attr, 'related', None)
|
# Django =>1.9 uses 'rel', django <1.9 uses 'related'
|
||||||
if isinstance(related, models.ManyToOneRel):
|
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
|
||||||
|
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
|
||||||
yield related
|
yield related
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,3 +55,33 @@ def maybe_queryset(value):
|
||||||
if isinstance(value, QuerySet):
|
if isinstance(value, QuerySet):
|
||||||
return WrappedQueryset(value)
|
return WrappedQueryset(value)
|
||||||
return 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
|
||||||
|
|
|
@ -8,8 +8,9 @@ from ..classtypes.base import FieldsClassType
|
||||||
from ..classtypes.inputobjecttype import InputObjectType
|
from ..classtypes.inputobjecttype import InputObjectType
|
||||||
from ..classtypes.mutation import Mutation
|
from ..classtypes.mutation import Mutation
|
||||||
from ..exceptions import SkipField
|
from ..exceptions import SkipField
|
||||||
from .argument import ArgumentsGroup, snake_case_args
|
from .argument import Argument, ArgumentsGroup, snake_case_args
|
||||||
from .base import GroupNamedType, LazyType, MountType, NamedType, OrderedType
|
from .base import (ArgumentType, GroupNamedType, LazyType, MountType,
|
||||||
|
NamedType, OrderedType)
|
||||||
from .definitions import NonNull
|
from .definitions import NonNull
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +20,9 @@ class Field(NamedType, OrderedType):
|
||||||
self, type, description=None, args=None, name=None, resolver=None,
|
self, type, description=None, args=None, name=None, resolver=None,
|
||||||
required=False, default=None, *args_list, **kwargs):
|
required=False, default=None, *args_list, **kwargs):
|
||||||
_creation_counter = kwargs.pop('_creation_counter', None)
|
_creation_counter = kwargs.pop('_creation_counter', None)
|
||||||
|
if isinstance(name, (Argument, ArgumentType)):
|
||||||
|
kwargs['name'] = name
|
||||||
|
name = None
|
||||||
super(Field, self).__init__(name=name, _creation_counter=_creation_counter)
|
super(Field, self).__init__(name=name, _creation_counter=_creation_counter)
|
||||||
if isinstance(type, six.string_types):
|
if isinstance(type, six.string_types):
|
||||||
type = LazyType(type)
|
type = LazyType(type)
|
||||||
|
@ -47,6 +51,16 @@ class Field(NamedType, OrderedType):
|
||||||
def resolver(self):
|
def resolver(self):
|
||||||
return self.resolver_fn or self.get_resolver_fn()
|
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):
|
def get_resolver_fn(self):
|
||||||
resolve_fn_name = 'resolve_%s' % self.attname
|
resolve_fn_name = 'resolve_%s' % self.attname
|
||||||
if hasattr(self.object_type, resolve_fn_name):
|
if hasattr(self.object_type, resolve_fn_name):
|
||||||
|
|
|
@ -48,6 +48,7 @@ def test_to_arguments_wrong_type():
|
||||||
|
|
||||||
|
|
||||||
def test_snake_case_args():
|
def test_snake_case_args():
|
||||||
resolver = lambda instance, args, info: args['my_arg']['inner_arg']
|
def resolver(instance, args, info):
|
||||||
|
return args['my_arg']['inner_arg']
|
||||||
r = snake_case_args(resolver)
|
r = snake_case_args(resolver)
|
||||||
assert r(None, {'myArg': {'innerArg': 3}}, None) == 3
|
assert r(None, {'myArg': {'innerArg': 3}}, None) == 3
|
||||||
|
|
|
@ -25,7 +25,9 @@ def test_orderedtype_different():
|
||||||
|
|
||||||
@patch('graphene.core.types.field.Field')
|
@patch('graphene.core.types.field.Field')
|
||||||
def test_type_as_field_called(Field):
|
def test_type_as_field_called(Field):
|
||||||
resolver = lambda x: x
|
def resolver(x):
|
||||||
|
return x
|
||||||
|
|
||||||
a = MountedType(2, description='A', resolver=resolver)
|
a = MountedType(2, description='A', resolver=resolver)
|
||||||
a.as_field()
|
a.as_field()
|
||||||
Field.assert_called_with(
|
Field.assert_called_with(
|
||||||
|
@ -45,7 +47,8 @@ def test_type_as_argument_called(Argument):
|
||||||
|
|
||||||
|
|
||||||
def test_type_as_field():
|
def test_type_as_field():
|
||||||
resolver = lambda x: x
|
def resolver(x):
|
||||||
|
return x
|
||||||
|
|
||||||
class MyObjectType(ObjectType):
|
class MyObjectType(ObjectType):
|
||||||
t = MountedType(description='A', resolver=resolver)
|
t = MountedType(description='A', resolver=resolver)
|
||||||
|
|
|
@ -11,7 +11,8 @@ from ..scalars import String
|
||||||
|
|
||||||
|
|
||||||
def test_field_internal_type():
|
def test_field_internal_type():
|
||||||
resolver = lambda *args: 'RESOLVED'
|
def resolver(*args):
|
||||||
|
return 'RESOLVED'
|
||||||
|
|
||||||
field = Field(String(), description='My argument', resolver=resolver)
|
field = Field(String(), description='My argument', resolver=resolver)
|
||||||
|
|
||||||
|
@ -104,6 +105,14 @@ def test_field_custom_arguments():
|
||||||
assert 'p' in schema.T(args)
|
assert 'p' in schema.T(args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_name_as_argument():
|
||||||
|
field = Field(None, name=String())
|
||||||
|
schema = Schema()
|
||||||
|
|
||||||
|
args = field.arguments
|
||||||
|
assert 'name' in schema.T(args)
|
||||||
|
|
||||||
|
|
||||||
def test_inputfield_internal_type():
|
def test_inputfield_internal_type():
|
||||||
field = InputField(String, description='My input field', default='3')
|
field = InputField(String, description='My input field', default='3')
|
||||||
|
|
||||||
|
@ -121,3 +130,39 @@ def test_inputfield_internal_type():
|
||||||
assert isinstance(type, GraphQLInputObjectField)
|
assert isinstance(type, GraphQLInputObjectField)
|
||||||
assert type.description == 'My input field'
|
assert type.description == 'My input field'
|
||||||
assert type.default_value == '3'
|
assert type.default_value == '3'
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_resolve_argument():
|
||||||
|
def resolver(instance, args, info):
|
||||||
|
return args.get('first_name')
|
||||||
|
|
||||||
|
field = Field(String(), first_name=String(), description='My argument', resolver=resolver)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
my_field = field
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
type = schema.T(field)
|
||||||
|
assert type.resolver(None, {'firstName': 'Peter'}, None) == 'Peter'
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_resolve_vars():
|
||||||
|
class Query(ObjectType):
|
||||||
|
hello = String(first_name=String())
|
||||||
|
|
||||||
|
def resolve_hello(self, args, info):
|
||||||
|
return 'Hello ' + args.get('first_name')
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
result = schema.execute("""
|
||||||
|
query foo($firstName:String)
|
||||||
|
{
|
||||||
|
hello(firstName:$firstName)
|
||||||
|
}
|
||||||
|
""", args={"firstName": "Serkan"})
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'hello': 'Hello Serkan'
|
||||||
|
}
|
||||||
|
assert result.data == expected
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import six
|
||||||
from graphql_relay.node.node import from_global_id
|
from graphql_relay.node.node import from_global_id
|
||||||
|
|
||||||
from ..core.fields import Field
|
from ..core.fields import Field
|
||||||
|
@ -23,15 +24,15 @@ class ConnectionField(Field):
|
||||||
self.connection_type = connection_type
|
self.connection_type = connection_type
|
||||||
self.edge_type = edge_type
|
self.edge_type = edge_type
|
||||||
|
|
||||||
def wrap_resolved(self, value, instance, args, info):
|
|
||||||
return value
|
|
||||||
|
|
||||||
def resolver(self, instance, args, info):
|
def resolver(self, instance, args, info):
|
||||||
schema = info.schema.graphene_schema
|
schema = info.schema.graphene_schema
|
||||||
connection_type = self.get_type(schema)
|
connection_type = self.get_type(schema)
|
||||||
resolved = super(ConnectionField, self).resolver(instance, args, info)
|
resolved = super(ConnectionField, self).resolver(instance, args, info)
|
||||||
if isinstance(resolved, connection_type):
|
if isinstance(resolved, connection_type):
|
||||||
return resolved
|
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)
|
return connection_type.from_list(resolved, args, info)
|
||||||
|
|
||||||
def get_connection_type(self, node):
|
def get_connection_type(self, node):
|
||||||
|
@ -73,8 +74,11 @@ class NodeField(Field):
|
||||||
return None
|
return None
|
||||||
_type, _id = resolved_global_id.type, resolved_global_id.id
|
_type, _id = resolved_global_id.type, resolved_global_id.id
|
||||||
object_type = schema.get_type(_type)
|
object_type = schema.get_type(_type)
|
||||||
if not is_node(object_type) or (self.field_object_type and
|
if isinstance(self.field_object_type, six.string_types):
|
||||||
object_type != self.field_object_type):
|
field_object_type = schema.get_type(self.field_object_type)
|
||||||
|
else:
|
||||||
|
field_object_type = self.field_object_type
|
||||||
|
if not is_node(object_type) or (self.field_object_type and object_type != field_object_type):
|
||||||
return
|
return
|
||||||
|
|
||||||
return object_type.get_node(_id, info)
|
return object_type.get_node(_id, info)
|
||||||
|
|
|
@ -31,6 +31,7 @@ class SpecialNode(relay.Node):
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
my_node = relay.NodeField(MyNode)
|
my_node = relay.NodeField(MyNode)
|
||||||
|
my_node_lazy = relay.NodeField('MyNode')
|
||||||
special_node = relay.NodeField(SpecialNode)
|
special_node = relay.NodeField(SpecialNode)
|
||||||
all_my_nodes = relay.ConnectionField(
|
all_my_nodes = relay.ConnectionField(
|
||||||
MyNode, connection_type=MyConnection, customArg=graphene.String())
|
MyNode, connection_type=MyConnection, customArg=graphene.String())
|
||||||
|
@ -117,3 +118,23 @@ def test_nodeidfield():
|
||||||
id_field_type = schema.T(id_field)
|
id_field_type = schema.T(id_field)
|
||||||
assert isinstance(id_field_type.type, GraphQLNonNull)
|
assert isinstance(id_field_type.type, GraphQLNonNull)
|
||||||
assert id_field_type.type.of_type == GraphQLID
|
assert id_field_type.type.of_type == GraphQLID
|
||||||
|
|
||||||
|
|
||||||
|
def test_nodefield_lazy_query():
|
||||||
|
query = '''
|
||||||
|
query RebelsShipsQuery {
|
||||||
|
myNode(id:"TXlOb2RlOjE=") {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
},
|
||||||
|
myNodeLazy(id:"TXlOb2RlOjE=") {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data['myNode'] == result.data['myNodeLazy'], \
|
||||||
|
"NodeField with object_type direct reference and with object_type string name should not differ."
|
||||||
|
|
|
@ -4,7 +4,6 @@ from collections import Iterable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from graphql_relay.connection.arrayconnection import connection_from_list
|
from graphql_relay.connection.arrayconnection import connection_from_list
|
||||||
from graphql_relay.node.node import to_global_id
|
from graphql_relay.node.node import to_global_id
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ def to_camel_case(snake_str):
|
||||||
components = snake_str.split('_')
|
components = snake_str.split('_')
|
||||||
# We capitalize the first letter of each component except the first one
|
# We capitalize the first letter of each component except the first one
|
||||||
# with the 'title' method and join them together.
|
# with the 'title' method and join them together.
|
||||||
return components[0] + "".join(x.title() for x in components[1:])
|
return components[0] + "".join(x.title() if x else '_' for x in components[1:])
|
||||||
|
|
||||||
|
|
||||||
# From this response in Stackoverflow
|
# From this response in Stackoverflow
|
||||||
|
|
|
@ -2,7 +2,11 @@ from ..resolve_only_args import resolve_only_args
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_only_args():
|
def test_resolve_only_args():
|
||||||
|
|
||||||
|
def resolver(*args, **kwargs):
|
||||||
|
return kwargs
|
||||||
|
|
||||||
my_data = {'one': 1, 'two': 2}
|
my_data = {'one': 1, 'two': 2}
|
||||||
resolver = lambda *args, **kwargs: kwargs
|
|
||||||
wrapped = resolve_only_args(resolver)
|
wrapped = resolve_only_args(resolver)
|
||||||
assert wrapped(None, my_data, None) == my_data
|
assert wrapped(None, my_data, None) == my_data
|
||||||
|
|
|
@ -4,11 +4,14 @@ from ..str_converters import to_camel_case, to_snake_case
|
||||||
def test_snake_case():
|
def test_snake_case():
|
||||||
assert to_snake_case('snakesOnAPlane') == 'snakes_on_a_plane'
|
assert to_snake_case('snakesOnAPlane') == 'snakes_on_a_plane'
|
||||||
assert to_snake_case('SnakesOnAPlane') == 'snakes_on_a_plane'
|
assert to_snake_case('SnakesOnAPlane') == 'snakes_on_a_plane'
|
||||||
|
assert to_snake_case('SnakesOnA_Plane') == 'snakes_on_a__plane'
|
||||||
assert to_snake_case('snakes_on_a_plane') == 'snakes_on_a_plane'
|
assert to_snake_case('snakes_on_a_plane') == 'snakes_on_a_plane'
|
||||||
|
assert to_snake_case('snakes_on_a__plane') == 'snakes_on_a__plane'
|
||||||
assert to_snake_case('IPhoneHysteria') == 'i_phone_hysteria'
|
assert to_snake_case('IPhoneHysteria') == 'i_phone_hysteria'
|
||||||
assert to_snake_case('iPhoneHysteria') == 'i_phone_hysteria'
|
assert to_snake_case('iPhoneHysteria') == 'i_phone_hysteria'
|
||||||
|
|
||||||
|
|
||||||
def test_camel_case():
|
def test_camel_case():
|
||||||
assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane'
|
assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane'
|
||||||
|
assert to_camel_case('snakes_on_a__plane') == 'snakesOnA_Plane'
|
||||||
assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria'
|
assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = setup.py,docs/*
|
exclude = setup.py,docs/*,examples/cookbook_django/*
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
|
||||||
[coverage:run]
|
[coverage:run]
|
||||||
|
|
9
setup.py
9
setup.py
|
@ -24,7 +24,7 @@ class PyTest(TestCommand):
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='graphene',
|
name='graphene',
|
||||||
version='0.5.0',
|
version='0.6.0',
|
||||||
|
|
||||||
description='GraphQL Framework for Python',
|
description='GraphQL Framework for Python',
|
||||||
long_description=open('README.rst').read(),
|
long_description=open('README.rst').read(),
|
||||||
|
@ -55,17 +55,18 @@ setup(
|
||||||
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'six>=1.10.0',
|
'six>=1.10.0',
|
||||||
'graphql-core==0.4.9',
|
'graphql-core>=0.4.9',
|
||||||
'graphql-relay==0.3.3'
|
'graphql-relay==0.3.3',
|
||||||
],
|
],
|
||||||
tests_require=[
|
tests_require=[
|
||||||
|
'django-filter>=0.10.0',
|
||||||
'pytest>=2.7.2',
|
'pytest>=2.7.2',
|
||||||
'pytest-django',
|
'pytest-django',
|
||||||
'mock',
|
'mock',
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'django': [
|
'django': [
|
||||||
'Django>=1.6.0,<1.9',
|
'Django>=1.6.0',
|
||||||
'singledispatch>=3.4.0.3',
|
'singledispatch>=3.4.0.3',
|
||||||
'graphql-django-view>=1.1.0',
|
'graphql-django-view>=1.1.0',
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
SECRET_KEY = 1
|
SECRET_KEY = 1
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'graphene.contrib.django',
|
||||||
'graphene.contrib.django.tests',
|
'graphene.contrib.django.tests',
|
||||||
'examples.starwars_django',
|
'examples.starwars_django',
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user