From f88590343b84125715aa347ef72f9ce1e07126b6 Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Wed, 16 Nov 2016 09:56:55 -0800 Subject: [PATCH 01/27] create pytest.ini to contain DJANGO_SETTINGS_MODULE --- pytest.ini | 2 ++ setup.cfg | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 pytest.ini 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 From b26f914b54602a8476654a397cf2b08ebe81aa8f Mon Sep 17 00:00:00 2001 From: Pablo Chinea Date: Thu, 29 Dec 2016 15:40:12 +0000 Subject: [PATCH 02/27] Fixes filtering in nested nodes. --- graphene_django/fields.py | 1 + 1 file changed, 1 insertion(+) 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) From dfb55cda6b456f5ee7fd8cee5db642a8e2f73de0 Mon Sep 17 00:00:00 2001 From: Pablo Chinea Date: Fri, 30 Dec 2016 09:34:59 +0000 Subject: [PATCH 03/27] Adds test for nested node filtering. --- graphene_django/tests/test_query.py | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 9c31243..fdf5bb0 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -281,3 +281,87 @@ def test_should_query_connectionfields(): }] } } + +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 From b5a450cb6de3d4877a120ea72b713c54811ecef4 Mon Sep 17 00:00:00 2001 From: Pablo Chinea Date: Fri, 30 Dec 2016 09:57:50 +0000 Subject: [PATCH 04/27] Fixes nested node test when django-filter is not installed. --- graphene_django/tests/test_query.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index fdf5bb0..23b8511 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 @@ -282,6 +283,9 @@ 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): @@ -320,7 +324,6 @@ def test_should_query_node_filtering(): lang='en' ) - schema = graphene.Schema(query=Query) query = ''' query NodeFilteringQuery { From 16a0d9ce58b7ae2b16a5faea0e02c2244b324626 Mon Sep 17 00:00:00 2001 From: Pablo Chinea Date: Fri, 30 Dec 2016 12:27:45 +0000 Subject: [PATCH 05/27] Fixes identation in queries test file. --- graphene_django/tests/test_query.py | 34 +++++++++++++---------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 23b8511..750a421 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -345,25 +345,21 @@ def test_should_query_node_filtering(): ''' expected = { - "allReporters": { - "edges": [ - { - "node": { - "id": "UmVwb3J0ZXJUeXBlOjE=", - "articles": { - "edges": [ - { - "node": { - "id": "QXJ0aWNsZVR5cGU6MQ==" - } - } - ] - } - } - } - ] - } - } + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=', + 'articles': { + 'edges': [{ + 'node': { + 'id': 'QXJ0aWNsZVR5cGU6MQ==' + } + }] + } + } + }] + } + } result = schema.execute(query) assert not result.errors From 2c26774c625da91f907804ee9bd7f5d9d2a06659 Mon Sep 17 00:00:00 2001 From: Pablo Chinea Date: Wed, 4 Jan 2017 16:23:17 +0000 Subject: [PATCH 06/27] Avoid collisions in choices names conversion. --- graphene_django/converter.py | 4 ++++ graphene_django/tests/test_converter.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 706d968..addaec3 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) + if 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/tests/test_converter.py b/graphene_django/tests/test_converter.py index 69be406..b3ddf40 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -176,6 +176,21 @@ 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', '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) From 89d0f0ca926a1786cb84175f3c87203a736a9069 Mon Sep 17 00:00:00 2001 From: Pablo Chinea Date: Thu, 5 Jan 2017 09:49:26 +0000 Subject: [PATCH 07/27] Handles multiple collisions with the same key. --- graphene_django/converter.py | 2 +- graphene_django/tests/test_converter.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index addaec3..92812d1 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -34,7 +34,7 @@ def get_choices(choices): yield choice else: name = convert_choice_name(value) - if name in converted_names: + while name in converted_names: name += '_' + str(len(converted_names)) converted_names.append(name) description = help_text diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index b3ddf40..997b03c 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -178,6 +178,7 @@ def test_field_with_choices_gettext(): 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'), )) From 15664bdc0b76d9118d41204922a41b808d5848ad Mon Sep 17 00:00:00 2001 From: Nick Hudkins Date: Fri, 27 Jan 2017 10:47:56 -0500 Subject: [PATCH 08/27] accept connections for DjangoFilterConnectionField --- graphene_django/filter/fields.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index defcfc1..bdccd4a 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -1,6 +1,7 @@ from functools import partial from ..fields import DjangoConnectionField +from graphene.relay import is_node from .utils import get_filtering_args_from_filterset, get_filterset_class @@ -9,9 +10,18 @@ class DjangoFilterConnectionField(DjangoConnectionField): def __init__(self, type, fields=None, extra_filter_meta=None, filterset_class=None, *args, **kwargs): - self.fields = fields or type._meta.filter_fields - meta = dict(model=type._meta.model, - fields=self.fields) + if is_node(type): + _fields = type._meta.filter_fields + _model = 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. + _fields = type._meta.node._meta.filter_fields + _model = type._meta.node._meta.model + + self.fields = fields or _fields + meta = dict(model=_model, fields=self.fields) if extra_filter_meta: meta.update(extra_filter_meta) self.filterset_class = get_filterset_class(filterset_class, **meta) From aed7229baee9f22e8a0857df773380a465020544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=ADas=20P=C3=A1ll=20Gissurarson?= Date: Thu, 2 Feb 2017 20:12:51 +0100 Subject: [PATCH 09/27] Allow nodes to skip the registry --- graphene_django/registry.py | 3 ++- graphene_django/types.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) 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/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: From 602e1f9de1d5636fb118b92811761447daed21cc Mon Sep 17 00:00:00 2001 From: BossGrand Date: Mon, 6 Feb 2017 16:59:13 -0800 Subject: [PATCH 10/27] Added documentation for exclude_fields as a method of limiting field access DjangoObjectType --- docs/authorization.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/authorization.rst b/docs/authorization.rst index 9ee7d0a..31bfdcf 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -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 --------------------------- From 84a074250bb5642d61aeb8ed68f4c2338beff0d9 Mon Sep 17 00:00:00 2001 From: BossGrand Date: Mon, 6 Feb 2017 17:00:35 -0800 Subject: [PATCH 11/27] missed a period --- docs/authorization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index 31bfdcf..7fa5213 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -34,7 +34,7 @@ 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 +conversely you can use ``exclude_fields`` meta atrribute. .. code:: python From b63cb772e46364205b0c2c932399abd022df6e4e Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Thu, 9 Feb 2017 18:18:50 +0200 Subject: [PATCH 12/27] Add `Adding login required` section to authorization --- docs/authorization.rst | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index 9ee7d0a..f003a01 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. @@ -108,3 +108,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 From 61dd8c4277d3962fa980bd55248f35d6289c1520 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Sun, 12 Feb 2017 16:44:39 +0200 Subject: [PATCH 13/27] Add basic structure for plain tutorial - `tutorial.rst` is now `tutorial-relay.rst` - Added `tutorial-plain.rst` --- docs/index.rst | 3 +- docs/tutorial-plain.rst | 504 ++++++++++++++++++++++ docs/{tutorial.rst => tutorial-relay.rst} | 4 +- 3 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 docs/tutorial-plain.rst rename docs/{tutorial.rst => tutorial-relay.rst} (99%) 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..6653b52 --- /dev/null +++ b/docs/tutorial-plain.rst @@ -0,0 +1,504 @@ +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 +------------------------ + +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 params 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/graphiql `__ 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 happeing. + + +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 excercise, 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 `__ diff --git a/docs/tutorial.rst b/docs/tutorial-relay.rst similarity index 99% rename from docs/tutorial.rst rename to docs/tutorial-relay.rst index 56c492d..d544bac 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial-relay.rst @@ -1,5 +1,5 @@ -Graphene-Django Tutorial -======================== +Graphene-Django Tutorial using Relay +==================================== Graphene has a number of additional features that are designed to make working with Django *really simple*. From 1304183893f3c31d09cabcc5cbd845bf1189cbaf Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Sun, 12 Feb 2017 16:48:48 +0200 Subject: [PATCH 14/27] Spellcheck `tutorial-plain.rst` --- docs/tutorial-plain.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 6653b52..2a76a25 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -218,7 +218,7 @@ 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 params with ``graphiql=True``. +aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``. .. code:: python @@ -308,7 +308,7 @@ If you are using the provided fixtures, you will see the following response: You can experiment with ``allCategories`` too. -Something to have in mind is the `auto camelcasing `__ that is happeing. +Something to have in mind is the `auto camelcasing `__ that is happening. Getting relations From 950cab641b9addbcf722226b7bcb3b36b10e85ab Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Sun, 12 Feb 2017 16:48:59 +0200 Subject: [PATCH 15/27] Bump year to 2017 --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 99e23398773877da561ce8f4aad32741e0dcb778 Mon Sep 17 00:00:00 2001 From: Craig Anderson Date: Tue, 14 Feb 2017 17:31:23 +0000 Subject: [PATCH 16/27] Correct imports in tutorial --- docs/tutorial.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 56c492d..955a3f0 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -90,7 +90,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 +145,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 From e063ef3cfc4ea2575b72a826a8ef512d62ecf5b6 Mon Sep 17 00:00:00 2001 From: Craig Anderson Date: Tue, 14 Feb 2017 17:37:46 +0000 Subject: [PATCH 17/27] Fix GraphiQL URL which is configured in the tutorial. --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 955a3f0..e3113a1 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -276,7 +276,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:: From b55e988205b6f14c79c177024b4a9bcca7c1d1aa Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 14 Feb 2017 19:55:38 +0200 Subject: [PATCH 18/27] Add `Summary` section with some paragraphs --- docs/tutorial-plain.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 2a76a25..f1c16ee 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -499,6 +499,14 @@ This will give us the following results: } } -As an excercise, you can try making some queries to ``ingredient``. +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``. + +In the next tutorial, we are going to use the **Relay specificiation** combined with ``django-filter`` From 4fc3dd66a197cd5df1d430cf34aacc31fc63e4c0 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 14 Feb 2017 20:22:51 +0200 Subject: [PATCH 19/27] Add `Vim.gitignore` to `.gitignore` - Taken from --- .gitignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 From 531a5348e6c88ea313a97649d35375b1923d9d54 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 14 Feb 2017 20:23:45 +0200 Subject: [PATCH 20/27] Add `examples/cookbook-plain` to follow the plain tutorial --- examples/cookbook-plain/README.md | 64 ++++++++ examples/cookbook-plain/cookbook/__init__.py | 0 .../cookbook/ingredients/__init__.py | 0 .../cookbook/ingredients/admin.py | 12 ++ .../cookbook/ingredients/apps.py | 7 + .../ingredients/fixtures/ingredients.json | 1 + .../ingredients/migrations/0001_initial.py | 33 +++++ .../migrations/0002_auto_20161104_0050.py | 20 +++ .../ingredients/migrations/__init__.py | 0 .../cookbook/ingredients/models.py | 17 +++ .../cookbook/ingredients/schema.py | 57 ++++++++ .../cookbook/recipes/__init__.py | 0 .../cookbook-plain/cookbook/recipes/admin.py | 12 ++ .../cookbook-plain/cookbook/recipes/apps.py | 7 + .../recipes/migrations/0001_initial.py | 36 +++++ .../migrations/0002_auto_20161104_0106.py | 25 ++++ .../cookbook/recipes/migrations/__init__.py | 0 .../cookbook-plain/cookbook/recipes/models.py | 20 +++ .../cookbook-plain/cookbook/recipes/schema.py | 52 +++++++ examples/cookbook-plain/cookbook/schema.py | 14 ++ examples/cookbook-plain/cookbook/settings.py | 138 ++++++++++++++++++ examples/cookbook-plain/cookbook/urls.py | 10 ++ examples/cookbook-plain/cookbook/wsgi.py | 16 ++ examples/cookbook-plain/manage.py | 10 ++ examples/cookbook-plain/requirements.txt | 5 + examples/cookbook-plain/setup.cfg | 2 + 26 files changed, 558 insertions(+) create mode 100644 examples/cookbook-plain/README.md create mode 100644 examples/cookbook-plain/cookbook/__init__.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/__init__.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/admin.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/apps.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json create mode 100644 examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/migrations/__init__.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/models.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/schema.py create mode 100644 examples/cookbook-plain/cookbook/recipes/__init__.py create mode 100644 examples/cookbook-plain/cookbook/recipes/admin.py create mode 100644 examples/cookbook-plain/cookbook/recipes/apps.py create mode 100644 examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py create mode 100644 examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py create mode 100644 examples/cookbook-plain/cookbook/recipes/migrations/__init__.py create mode 100644 examples/cookbook-plain/cookbook/recipes/models.py create mode 100644 examples/cookbook-plain/cookbook/recipes/schema.py create mode 100644 examples/cookbook-plain/cookbook/schema.py create mode 100644 examples/cookbook-plain/cookbook/settings.py create mode 100644 examples/cookbook-plain/cookbook/urls.py create mode 100644 examples/cookbook-plain/cookbook/wsgi.py create mode 100755 examples/cookbook-plain/manage.py create mode 100644 examples/cookbook-plain/requirements.txt create mode 100644 examples/cookbook-plain/setup.cfg 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__ From 9621054852fe7449a5d64f2c1624e1c5ea6ccad1 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 14 Feb 2017 20:28:49 +0200 Subject: [PATCH 21/27] Add reference to `cookbook-plain` in `tutorial-plain.rst` --- docs/tutorial-plain.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index f1c16ee..ac11430 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -11,6 +11,10 @@ A good idea is to check the `graphene Date: Mon, 20 Feb 2017 01:08:42 -0800 Subject: [PATCH 22/27] Improved GraphQL batch view errors. --- graphene_django/tests/test_views.py | 9 +++++++++ graphene_django/views.py | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index e7ec187..fa3aaed 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(), j([]), 'application/json') + + assert response.status_code == 200 + 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') 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.')) From 2660de969ffae0ec0558a0d41ad36c2827630969 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 01:15:13 -0800 Subject: [PATCH 23/27] Improved batch view tests --- graphene_django/tests/test_views.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index fa3aaed..da7ad04 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -184,9 +184,9 @@ def test_batch_allows_post_with_json_encoding(client): def test_batch_fails_if_is_empty(client): - response = client.post(batch_url_string(), j([]), 'application/json') + response = client.post(batch_url_string(), '[]', 'application/json') - assert response.status_code == 200 + assert response.status_code == 400 assert response_json(response) == { 'errors': [{'message': 'Received an empty list in the batch request.'}] } @@ -441,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.'}] From 0b4db6a42d50b21ec2fa17eeefceedc4e916fe88 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 21 Feb 2017 21:15:43 +0200 Subject: [PATCH 24/27] Improve summary section - Mention automatic pagination --- docs/tutorial-plain.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index ac11430..b91f886 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -513,4 +513,4 @@ 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``. -In the next tutorial, we are going to use the **Relay specificiation** combined with ``django-filter`` +If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.** From f15d2f67621f4857a8b37aef133087d1c17fe8d1 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 21 Feb 2017 21:32:45 +0200 Subject: [PATCH 25/27] Update `tutorial-relay.rst` structure to match plain --- docs/tutorial-relay.rst | 77 ++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index d544bac..45d2207 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -1,5 +1,5 @@ -Graphene-Django Tutorial using Relay -==================================== +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 ------ @@ -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. From 5d7794815f3ee92528b1b7657bacbb246bcc5dda Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 21 Feb 2017 21:39:25 +0200 Subject: [PATCH 26/27] Fix GraphiQL URL which is configured in the tutorial. - Based on --- docs/tutorial-plain.rst | 2 +- docs/tutorial-relay.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index b91f886..a4c98a9 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -271,7 +271,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/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index 45d2207..895dc44 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -281,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:: From 755bd4e18759ebe0cda4120f435deefc573545a3 Mon Sep 17 00:00:00 2001 From: thatneat Date: Thu, 23 Feb 2017 09:08:05 -0800 Subject: [PATCH 27/27] spelling --- examples/cookbook/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ---------------