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