From 7882491e04ffbdd7466a096068077f7aeffc23f4 Mon Sep 17 00:00:00 2001 From: Niall Date: Sun, 5 Mar 2017 17:13:09 +0000 Subject: [PATCH 01/18] Fix filtering with a resolver and DjangoFilter filter. --- graphene_django/fields.py | 2 +- graphene_django/tests/test_query.py | 109 +++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 2519562..3e7c378 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -54,7 +54,7 @@ class DjangoConnectionField(ConnectionField): iterable = maybe_queryset(iterable) if isinstance(iterable, QuerySet): if iterable is not default_manager: - iterable &= maybe_queryset(default_manager) + iterable = maybe_queryset(default_manager) _len = iterable.count() else: _len = len(iterable) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 06b2bb3..486e5ff 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -5,12 +5,15 @@ from django.db import models from django.utils.functional import SimpleLazyObject from py.test import raises +from django_filters import FilterSet, NumberFilter + import graphene from graphene.relay import Node from ..utils import DJANGO_FILTER_INSTALLED from ..compat import MissingType, JSONField from ..fields import DjangoConnectionField +from ..filter.fields import DjangoFilterConnectionField from ..types import DjangoObjectType from .models import Article, Reporter @@ -42,7 +45,6 @@ def test_should_query_simplelazy_objects(): model = Reporter only_fields = ('id', ) - class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) @@ -360,7 +362,110 @@ def test_should_query_node_filtering(): }] } } - + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_should_query_filter_node_limit(): + class ReporterFilter(FilterSet): + limit = NumberFilter(method='filter_limit') + + def filter_limit(self, queryset, name, value): + return queryset[:value] + + class Meta: + model = Reporter + fields = ['first_name', ] + + 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 = DjangoFilterConnectionField( + ReporterType, + filterset_class=ReporterFilter + ) + + def resolve_all_reporters(self, args, context, info): + return Reporter.objects.all() + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + Reporter.objects.create( + first_name='Bob', + last_name='Doe', + email='bobdoe@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(limit: 1) { + 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 a070b291a24c06f7f06a4d81e2cd60f1f5fcf33c Mon Sep 17 00:00:00 2001 From: Niall Date: Sun, 5 Mar 2017 19:17:00 +0000 Subject: [PATCH 02/18] Add broken test --- .gitignore | 1 + examples/starwars/tests/test_mutation.py | 1 + graphene_django/tests/models.py | 1 + graphene_django/tests/test_query.py | 92 ++++++++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/.gitignore b/.gitignore index 0b25625..4e81c34 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +.virtualenv # C extensions *.so diff --git a/examples/starwars/tests/test_mutation.py b/examples/starwars/tests/test_mutation.py index aa312ff..7c8019a 100644 --- a/examples/starwars/tests/test_mutation.py +++ b/examples/starwars/tests/test_mutation.py @@ -75,5 +75,6 @@ def test_mutations(): } } result = schema.execute(query) + print(result.data) assert not result.errors assert result.data == expected diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 0c62f28..03ca59d 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -45,6 +45,7 @@ class Article(models.Model): ], default='es') importance = models.IntegerField('Importance', null=True, blank=True, choices=[(1, u'Very important'), (2, u'Not as important')]) + tag = models.CharField(max_length=100) def __str__(self): # __unicode__ on Python 2 return self.headline diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 486e5ff..1f4809b 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -368,6 +368,98 @@ def test_should_query_node_filtering(): assert result.data == expected +@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED, + reason="django-filter should be installed") +def test_should_query_node_multiple_filtering(): + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class ArticleType(DjangoObjectType): + + class Meta: + model = Article + interfaces = (Node, ) + filter_fields = ('lang', 'tag') + + 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', + tag='one' + ) + Article.objects.create( + headline='Article Node 2', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='en', + tag='two' + ) + Article.objects.create( + headline='Article Node 3', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='en', + tag='three' + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters { + edges { + node { + id + articles(lang: "es", tag: "two") { + 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 + + def test_should_query_filter_node_limit(): class ReporterFilter(FilterSet): limit = NumberFilter(method='filter_limit') From 27997276a4cc2df034a139af83e334370e826918 Mon Sep 17 00:00:00 2001 From: Niall Date: Mon, 6 Mar 2017 18:13:40 +0000 Subject: [PATCH 03/18] Attempt fix. Breaks tests --- graphene_django/fields.py | 4 ++-- graphene_django/filter/fields.py | 27 ++++++++++++++++++++++----- graphene_django/tests/test_query.py | 13 +++++-------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 3e7c378..9ba999c 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -53,8 +53,8 @@ class DjangoConnectionField(ConnectionField): iterable = default_manager iterable = maybe_queryset(iterable) if isinstance(iterable, QuerySet): - if iterable is not default_manager: - iterable = maybe_queryset(default_manager) + if default_manager is not None and iterable is not default_manager: + 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 363e1d9..1b2c1c8 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -1,6 +1,8 @@ from collections import OrderedDict from functools import partial +from django.db.models.query import QuerySet + # from graphene.relay import is_node from graphene.types.argument import to_arguments from ..fields import DjangoConnectionField @@ -44,15 +46,30 @@ class DjangoFilterConnectionField(DjangoConnectionField): def filtering_args(self): return get_filtering_args_from_filterset(self.filterset_class, self.node_type) + # @staticmethod + # def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args, + # root, args, context, info): + # filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} + # qs = filterset_class( + # data=filter_kwargs, + # queryset=default_manager.get_queryset() + # ).qs + # return DjangoConnectionField.connection_resolver(resolver, connection, qs, root, args, context, info) + @staticmethod def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args, root, args, context, info): filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} - qs = filterset_class( - data=filter_kwargs, - queryset=default_manager.get_queryset() - ).qs - return DjangoConnectionField.connection_resolver(resolver, connection, qs, root, args, context, info) + + def new_resolver(root, args, context, info): + qs = resolver(root, args, context, info) + if qs is None or not isinstance(qs, QuerySet): + qs = default_manager.get_queryset() + qs = filterset_class(data=filter_kwargs, queryset=qs).qs + + return qs + + return DjangoConnectionField.connection_resolver(new_resolver, connection, None, root, args, context, info) def get_resolver(self, parent_resolver): return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager(), diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 1f4809b..6d2f8c8 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -382,7 +382,7 @@ def test_should_query_node_multiple_filtering(): class Meta: model = Article interfaces = (Node, ) - filter_fields = ('lang', 'tag') + filter_fields = ('lang', 'headline') class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) @@ -398,24 +398,21 @@ def test_should_query_node_multiple_filtering(): pub_date=datetime.date.today(), reporter=r, editor=r, - lang='es', - tag='one' + lang='es' ) Article.objects.create( headline='Article Node 2', pub_date=datetime.date.today(), reporter=r, editor=r, - lang='en', - tag='two' + lang='en' ) Article.objects.create( headline='Article Node 3', pub_date=datetime.date.today(), reporter=r, editor=r, - lang='en', - tag='three' + lang='en' ) schema = graphene.Schema(query=Query) @@ -425,7 +422,7 @@ def test_should_query_node_multiple_filtering(): edges { node { id - articles(lang: "es", tag: "two") { + articles(lang: "es", headline: "Article Node 2") { edges { node { id From f08dbdc7c3996aa8d66a0ca1445b0bf68268225f Mon Sep 17 00:00:00 2001 From: Niall Date: Mon, 6 Mar 2017 18:20:31 +0000 Subject: [PATCH 04/18] Clean up --- .gitignore | 1 - examples/starwars/tests/test_mutation.py | 1 - graphene_django/tests/models.py | 1 - 3 files changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4e81c34..0b25625 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -.virtualenv # C extensions *.so diff --git a/examples/starwars/tests/test_mutation.py b/examples/starwars/tests/test_mutation.py index 7c8019a..aa312ff 100644 --- a/examples/starwars/tests/test_mutation.py +++ b/examples/starwars/tests/test_mutation.py @@ -75,6 +75,5 @@ def test_mutations(): } } result = schema.execute(query) - print(result.data) assert not result.errors assert result.data == expected diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 03ca59d..0c62f28 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -45,7 +45,6 @@ class Article(models.Model): ], default='es') importance = models.IntegerField('Importance', null=True, blank=True, choices=[(1, u'Very important'), (2, u'Not as important')]) - tag = models.CharField(max_length=100) def __str__(self): # __unicode__ on Python 2 return self.headline From 44530c33d9e4fd81cbfb3444da1c152a345a4d3d Mon Sep 17 00:00:00 2001 From: Niall Date: Mon, 6 Mar 2017 19:41:04 +0000 Subject: [PATCH 05/18] Long-winded intersection using sets --- graphene_django/fields.py | 8 +++++--- graphene_django/filter/fields.py | 27 +++++---------------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 9ba999c..c7d9968 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -53,9 +53,11 @@ class DjangoConnectionField(ConnectionField): iterable = default_manager iterable = maybe_queryset(iterable) if isinstance(iterable, QuerySet): - if default_manager is not None and iterable is not default_manager: - iterable &= maybe_queryset(default_manager) - _len = iterable.count() + if iterable is not default_manager: + iterable = list(set(iterable).intersection(maybe_queryset(default_manager))) + _len = len(iterable) + else: + _len = iterable.count() else: _len = len(iterable) connection = connection_from_list_slice( diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 1b2c1c8..363e1d9 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -1,8 +1,6 @@ from collections import OrderedDict from functools import partial -from django.db.models.query import QuerySet - # from graphene.relay import is_node from graphene.types.argument import to_arguments from ..fields import DjangoConnectionField @@ -46,30 +44,15 @@ class DjangoFilterConnectionField(DjangoConnectionField): def filtering_args(self): return get_filtering_args_from_filterset(self.filterset_class, self.node_type) - # @staticmethod - # def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args, - # root, args, context, info): - # filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} - # qs = filterset_class( - # data=filter_kwargs, - # queryset=default_manager.get_queryset() - # ).qs - # return DjangoConnectionField.connection_resolver(resolver, connection, qs, root, args, context, info) - @staticmethod def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args, root, args, context, info): filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} - - def new_resolver(root, args, context, info): - qs = resolver(root, args, context, info) - if qs is None or not isinstance(qs, QuerySet): - qs = default_manager.get_queryset() - qs = filterset_class(data=filter_kwargs, queryset=qs).qs - - return qs - - return DjangoConnectionField.connection_resolver(new_resolver, connection, None, root, args, context, info) + qs = filterset_class( + data=filter_kwargs, + queryset=default_manager.get_queryset() + ).qs + return DjangoConnectionField.connection_resolver(resolver, connection, qs, root, args, context, info) def get_resolver(self, parent_resolver): return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager(), From 6a3a2fc152fb332d179f6e986a0858087c51e441 Mon Sep 17 00:00:00 2001 From: Niall Date: Mon, 6 Mar 2017 20:00:01 +0000 Subject: [PATCH 06/18] Fix test --- graphene_django/tests/test_query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 6d2f8c8..4553259 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -405,7 +405,7 @@ def test_should_query_node_multiple_filtering(): pub_date=datetime.date.today(), reporter=r, editor=r, - lang='en' + lang='es' ) Article.objects.create( headline='Article Node 3', @@ -422,7 +422,7 @@ def test_should_query_node_multiple_filtering(): edges { node { id - articles(lang: "es", headline: "Article Node 2") { + articles(lang: "es", headline: "Article Node 1") { edges { node { id From 54de562eac98c5237752b9e57fd376679b614981 Mon Sep 17 00:00:00 2001 From: Niall Date: Mon, 6 Mar 2017 20:19:39 +0000 Subject: [PATCH 07/18] Example for order_by being ignored --- graphene_django/tests/test_query.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 4553259..75eb8c8 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -488,18 +488,18 @@ def test_should_query_filter_node_limit(): ) def resolve_all_reporters(self, args, context, info): - return Reporter.objects.all() + return Reporter.objects.order_by('a_choice') - r = Reporter.objects.create( - first_name='John', - last_name='Doe', - email='johndoe@example.com', - a_choice=1 - ) Reporter.objects.create( first_name='Bob', last_name='Doe', email='bobdoe@example.com', + a_choice=2 + ) + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', a_choice=1 ) From afbd6c6e8fb05b16b18df0a8b4227afe592de54a Mon Sep 17 00:00:00 2001 From: Ed Morley Date: Wed, 22 Mar 2017 01:59:52 +0000 Subject: [PATCH 08/18] Remove django_graphiql from cookbook requirements.txt. The package is not required since support for graphiql is now built-in. Fixes #135. --- examples/cookbook-plain/requirements.txt | 1 - examples/cookbook/requirements.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index 6931cf4..a693bd1 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,5 +1,4 @@ graphene graphene-django -django_graphiql graphql-core django==1.9 diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index 78754fd..66fa629 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,6 +1,5 @@ graphene graphene-django -django_graphiql graphql-core django==1.9 django-filter==0.11.0 From 69261ba86aacd49b1af8d33ffc9d54ba3fbfcf00 Mon Sep 17 00:00:00 2001 From: Kuan Date: Tue, 11 Apr 2017 09:07:28 -0700 Subject: [PATCH 09/18] Add hyperlink on documentation in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3927a8..32ebe92 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode ### Documentation -The documentation is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme. +The [documentation](http://docs.graphene-python.org/projects/django/en/latest) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme. The documentation dependencies are installed by running: From f15483509653bb32dc8dcdabf5c21cf0cd9f018b Mon Sep 17 00:00:00 2001 From: Kuan Date: Tue, 11 Apr 2017 09:09:08 -0700 Subject: [PATCH 10/18] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 21b222b..934bef7 100644 --- a/README.rst +++ b/README.rst @@ -120,7 +120,7 @@ After developing, the full test suite can be evaluated by running: Documentation ~~~~~~~~~~~~~ -The documentation is generated using the excellent +The `documentation `__ is generated using the excellent `Sphinx `__ and a custom theme. The documentation dependencies are installed by running: From b10dba8cc6b4dc296b23d019d681117711a5f7fc Mon Sep 17 00:00:00 2001 From: Kuan Date: Tue, 11 Apr 2017 09:10:01 -0700 Subject: [PATCH 11/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32ebe92..c3f7341 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode ### Documentation -The [documentation](http://docs.graphene-python.org/projects/django/en/latest) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme. +The [documentation](http://docs.graphene-python.org/projects/django/en/latest/) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme. The documentation dependencies are installed by running: From 03ba301ccf5d40ed551399625887095c28a19deb Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 15 Apr 2017 01:00:02 -0700 Subject: [PATCH 12/18] Fixed filterset limit issue --- graphene_django/fields.py | 15 ++++--- graphene_django/filter/fields.py | 27 +++++++++++- graphene_django/tests/test_query.py | 68 ++++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 10 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index c7d9968..367ad63 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -46,18 +46,21 @@ class DjangoConnectionField(ConnectionField): else: return self.model._default_manager - @staticmethod - def connection_resolver(resolver, connection, default_manager, root, args, context, info): + @classmethod + def merge_querysets(cls, default_queryset, queryset): + return default_queryset & queryset + + @classmethod + def connection_resolver(cls, resolver, connection, default_manager, root, args, context, info): iterable = resolver(root, args, context, info) if iterable is None: iterable = default_manager iterable = maybe_queryset(iterable) if isinstance(iterable, QuerySet): if iterable is not default_manager: - iterable = list(set(iterable).intersection(maybe_queryset(default_manager))) - _len = len(iterable) - else: - _len = iterable.count() + default_queryset = maybe_queryset(default_manager) + iterable = cls.merge_querysets(default_queryset, iterable) + _len = iterable.count() else: _len = len(iterable) connection = connection_from_list_slice( diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 363e1d9..061b2c6 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -45,14 +45,37 @@ class DjangoFilterConnectionField(DjangoConnectionField): return get_filtering_args_from_filterset(self.filterset_class, self.node_type) @staticmethod - def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args, + def merge_querysets(default_queryset, queryset): + # There could be the case where the default queryset (returned from the filterclass) + # and the resolver queryset have some limits on it. + # We only would be able to apply one of those, but not both + # at the same time. + + # See related PR: https://github.com/graphql-python/graphene-django/pull/126 + + assert not (default_queryset.query.low_mark and queryset.query.low_mark), ( + 'Received two sliced querysets (low mark) in the connection, please slice only in one.' + ) + assert not (default_queryset.query.high_mark and queryset.query.high_mark), ( + 'Received two sliced querysets (high mark) in the connection, please slice only in one.' + ) + low = default_queryset.query.low_mark or queryset.query.low_mark + high = default_queryset.query.high_mark or queryset.query.high_mark + default_queryset.query.clear_limits() + queryset = default_queryset & queryset + queryset.query.set_limits(low, high) + return queryset + + @classmethod + def connection_resolver(cls, resolver, connection, default_manager, filterset_class, filtering_args, root, args, context, info): filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} qs = filterset_class( data=filter_kwargs, queryset=default_manager.get_queryset() ).qs - return DjangoConnectionField.connection_resolver(resolver, connection, qs, root, args, context, info) + return super(DjangoFilterConnectionField, cls).connection_resolver( + resolver, connection, qs, root, args, context, info) def get_resolver(self, parent_resolver): return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager(), diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 75eb8c8..ae765e2 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -525,10 +525,12 @@ def test_should_query_filter_node_limit(): edges { node { id + firstName articles(lang: "es") { edges { node { id + lang } } } @@ -542,11 +544,13 @@ def test_should_query_filter_node_limit(): 'allReporters': { 'edges': [{ 'node': { - 'id': 'UmVwb3J0ZXJUeXBlOjE=', + 'id': 'UmVwb3J0ZXJUeXBlOjI=', + 'firstName': 'John', 'articles': { 'edges': [{ 'node': { - 'id': 'QXJ0aWNsZVR5cGU6MQ==' + 'id': 'QXJ0aWNsZVR5cGU6MQ==', + 'lang': 'ES' } }] } @@ -558,3 +562,63 @@ def test_should_query_filter_node_limit(): result = schema.execute(query) assert not result.errors assert result.data == expected + + +def test_should_query_filter_node_double_limit_raises(): + class ReporterFilter(FilterSet): + limit = NumberFilter(method='filter_limit') + + def filter_limit(self, queryset, name, value): + return queryset[:value] + + class Meta: + model = Reporter + fields = ['first_name', ] + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class Query(graphene.ObjectType): + all_reporters = DjangoFilterConnectionField( + ReporterType, + filterset_class=ReporterFilter + ) + + def resolve_all_reporters(self, args, context, info): + return Reporter.objects.order_by('a_choice')[:2] + + Reporter.objects.create( + first_name='Bob', + last_name='Doe', + email='bobdoe@example.com', + a_choice=2 + ) + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(limit: 1) { + edges { + node { + id + firstName + } + } + } + } + ''' + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == ( + 'Received two sliced querysets (high mark) in the connection, please slice only in one.' + ) From 40bec33ac9cd7fc1b4a644ea681b0885ee4e99d1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 15 Apr 2017 01:11:36 -0700 Subject: [PATCH 13/18] Improved travis tests. Added Django==1.11 tests --- .travis.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index eb4799b..e18db52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,7 @@ install: pip install -e .[test] pip install psycopg2 # Required for Django postgres fields testing pip install django==$DJANGO_VERSION + pip install django-filter==$DJANGO_FILTER_VERSION python setup.py develop elif [ "$TEST_TYPE" = lint ]; then pip install flake8 @@ -45,18 +46,20 @@ after_success: fi env: matrix: - - TEST_TYPE=build DJANGO_VERSION=1.10 + - TEST_TYPE=build DJANGO_VERSION=1.11 DJANGO_FILTER_VERSION=1.0.2 matrix: fast_finish: true include: - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.6 + env: TEST_TYPE=build DJANGO_VERSION=1.6 DJANGO_FILTER_VERSION=1.0.0 - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.7 + env: TEST_TYPE=build DJANGO_VERSION=1.7 DJANGO_FILTER_VERSION=1.0.0 - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.8 + env: TEST_TYPE=build DJANGO_VERSION=1.8 DJANGO_FILTER_VERSION=1.0.2 - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.9 + env: TEST_TYPE=build DJANGO_VERSION=1.9 DJANGO_FILTER_VERSION=1.0.2 + - python: '2.7' + env: TEST_TYPE=build DJANGO_VERSION=1.10 DJANGO_FILTER_VERSION=1.0.2 - python: '2.7' env: TEST_TYPE=lint deploy: From 073ed8af5d5dcfe4537baafe79726adba77382d7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 15 Apr 2017 01:16:11 -0700 Subject: [PATCH 14/18] Moved tests to filter field tests --- .travis.yml | 12 +- graphene_django/filter/tests/test_fields.py | 171 +++++++++++++++++++- graphene_django/tests/test_query.py | 170 ------------------- 3 files changed, 176 insertions(+), 177 deletions(-) diff --git a/.travis.yml b/.travis.yml index e18db52..3c9b44b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,20 +46,20 @@ after_success: fi env: matrix: - - TEST_TYPE=build DJANGO_VERSION=1.11 DJANGO_FILTER_VERSION=1.0.2 + - TEST_TYPE=build DJANGO_VERSION=1.11 matrix: fast_finish: true include: - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.6 DJANGO_FILTER_VERSION=1.0.0 + env: TEST_TYPE=build DJANGO_VERSION=1.6 - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.7 DJANGO_FILTER_VERSION=1.0.0 + env: TEST_TYPE=build DJANGO_VERSION=1.7 - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.8 DJANGO_FILTER_VERSION=1.0.2 + env: TEST_TYPE=build DJANGO_VERSION=1.8 - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.9 DJANGO_FILTER_VERSION=1.0.2 + env: TEST_TYPE=build DJANGO_VERSION=1.9 - python: '2.7' - env: TEST_TYPE=build DJANGO_VERSION=1.10 DJANGO_FILTER_VERSION=1.0.2 + env: TEST_TYPE=build DJANGO_VERSION=1.10 - python: '2.7' env: TEST_TYPE=lint deploy: diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index c95e2d7..1b24ff2 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -14,11 +14,13 @@ pytestmark = [] if DJANGO_FILTER_INSTALLED: import django_filters + from django_filters import FilterSet, NumberFilter + from graphene_django.filter import (GlobalIDFilter, DjangoFilterConnectionField, GlobalIDMultipleChoiceFilter) from graphene_django.filter.tests.filters import ArticleFilter, PetFilter, ReporterFilter else: - pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed')) + pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed or not compatible')) pytestmark.append(pytest.mark.django_db) @@ -365,3 +367,170 @@ def test_recursive_filter_connection(): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) assert ReporterFilterNode._meta.fields['child_reporters'].node_type == ReporterFilterNode + + +def test_should_query_filter_node_limit(): + class ReporterFilter(FilterSet): + limit = NumberFilter(method='filter_limit') + + def filter_limit(self, queryset, name, value): + return queryset[:value] + + class Meta: + model = Reporter + fields = ['first_name', ] + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class ArticleType(DjangoObjectType): + + class Meta: + model = Article + interfaces = (Node, ) + filter_fields = ('lang', ) + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField( + ReporterType, + filterset_class=ReporterFilter + ) + + def resolve_all_reporters(self, args, context, info): + return Reporter.objects.order_by('a_choice') + + Reporter.objects.create( + first_name='Bob', + last_name='Doe', + email='bobdoe@example.com', + a_choice=2 + ) + 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.now(), + reporter=r, + editor=r, + lang='es' + ) + Article.objects.create( + headline='Article Node 2', + pub_date=datetime.now(), + reporter=r, + editor=r, + lang='en' + ) + + schema = Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(limit: 1) { + edges { + node { + id + firstName + articles(lang: "es") { + edges { + node { + id + lang + } + } + } + } + } + } + } + ''' + + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjI=', + 'firstName': 'John', + 'articles': { + 'edges': [{ + 'node': { + 'id': 'QXJ0aWNsZVR5cGU6MQ==', + 'lang': 'ES' + } + }] + } + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_should_query_filter_node_double_limit_raises(): + class ReporterFilter(FilterSet): + limit = NumberFilter(method='filter_limit') + + def filter_limit(self, queryset, name, value): + return queryset[:value] + + class Meta: + model = Reporter + fields = ['first_name', ] + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField( + ReporterType, + filterset_class=ReporterFilter + ) + + def resolve_all_reporters(self, args, context, info): + return Reporter.objects.order_by('a_choice')[:2] + + Reporter.objects.create( + first_name='Bob', + last_name='Doe', + email='bobdoe@example.com', + a_choice=2 + ) + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + + schema = Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(limit: 1) { + edges { + node { + id + firstName + } + } + } + } + ''' + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == ( + 'Received two sliced querysets (high mark) in the connection, please slice only in one.' + ) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index ae765e2..3594000 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -5,15 +5,12 @@ from django.db import models from django.utils.functional import SimpleLazyObject from py.test import raises -from django_filters import FilterSet, NumberFilter - import graphene from graphene.relay import Node from ..utils import DJANGO_FILTER_INSTALLED from ..compat import MissingType, JSONField from ..fields import DjangoConnectionField -from ..filter.fields import DjangoFilterConnectionField from ..types import DjangoObjectType from .models import Article, Reporter @@ -455,170 +452,3 @@ def test_should_query_node_multiple_filtering(): result = schema.execute(query) assert not result.errors assert result.data == expected - - -def test_should_query_filter_node_limit(): - class ReporterFilter(FilterSet): - limit = NumberFilter(method='filter_limit') - - def filter_limit(self, queryset, name, value): - return queryset[:value] - - class Meta: - model = Reporter - fields = ['first_name', ] - - 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 = DjangoFilterConnectionField( - ReporterType, - filterset_class=ReporterFilter - ) - - def resolve_all_reporters(self, args, context, info): - return Reporter.objects.order_by('a_choice') - - Reporter.objects.create( - first_name='Bob', - last_name='Doe', - email='bobdoe@example.com', - a_choice=2 - ) - 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(limit: 1) { - edges { - node { - id - firstName - articles(lang: "es") { - edges { - node { - id - lang - } - } - } - } - } - } - } - ''' - - expected = { - 'allReporters': { - 'edges': [{ - 'node': { - 'id': 'UmVwb3J0ZXJUeXBlOjI=', - 'firstName': 'John', - 'articles': { - 'edges': [{ - 'node': { - 'id': 'QXJ0aWNsZVR5cGU6MQ==', - 'lang': 'ES' - } - }] - } - } - }] - } - } - - result = schema.execute(query) - assert not result.errors - assert result.data == expected - - -def test_should_query_filter_node_double_limit_raises(): - class ReporterFilter(FilterSet): - limit = NumberFilter(method='filter_limit') - - def filter_limit(self, queryset, name, value): - return queryset[:value] - - class Meta: - model = Reporter - fields = ['first_name', ] - - class ReporterType(DjangoObjectType): - - class Meta: - model = Reporter - interfaces = (Node, ) - - class Query(graphene.ObjectType): - all_reporters = DjangoFilterConnectionField( - ReporterType, - filterset_class=ReporterFilter - ) - - def resolve_all_reporters(self, args, context, info): - return Reporter.objects.order_by('a_choice')[:2] - - Reporter.objects.create( - first_name='Bob', - last_name='Doe', - email='bobdoe@example.com', - a_choice=2 - ) - r = Reporter.objects.create( - first_name='John', - last_name='Doe', - email='johndoe@example.com', - a_choice=1 - ) - - schema = graphene.Schema(query=Query) - query = ''' - query NodeFilteringQuery { - allReporters(limit: 1) { - edges { - node { - id - firstName - } - } - } - } - ''' - - result = schema.execute(query) - assert len(result.errors) == 1 - assert str(result.errors[0]) == ( - 'Received two sliced querysets (high mark) in the connection, please slice only in one.' - ) From 62ef9725d9bf0e553c32282a0db89584a4029473 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 15 Apr 2017 01:22:32 -0700 Subject: [PATCH 15/18] Removed unnecesary Django filter installation --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3c9b44b..6450bd2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,6 @@ install: pip install -e .[test] pip install psycopg2 # Required for Django postgres fields testing pip install django==$DJANGO_VERSION - pip install django-filter==$DJANGO_FILTER_VERSION python setup.py develop elif [ "$TEST_TYPE" = lint ]; then pip install flake8 From a6ca14405c2cfd8ef4251dcbabed87a83717bfb0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 15 Apr 2017 02:09:05 -0700 Subject: [PATCH 16/18] Added RELAY_CONNECTION_MAX_LIMIT and RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST settings Relay connections will be limited to 100 records by default. --- graphene_django/fields.py | 42 ++++++++++++- graphene_django/filter/fields.py | 27 +++++++-- graphene_django/settings.py | 5 ++ graphene_django/tests/test_query.py | 93 +++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 6 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 367ad63..c2a2a8f 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -6,6 +6,7 @@ from graphene.types import Field, List from graphene.relay import ConnectionField, PageInfo from graphql_relay.connection.arrayconnection import connection_from_list_slice +from .settings import graphene_settings from .utils import DJANGO_FILTER_INSTALLED, maybe_queryset @@ -30,6 +31,14 @@ class DjangoConnectionField(ConnectionField): def __init__(self, *args, **kwargs): self.on = kwargs.pop('on', False) + self.max_limit = kwargs.pop( + 'max_limit', + graphene_settings.RELAY_CONNECTION_MAX_LIMIT + ) + self.enforce_first_or_last = kwargs.pop( + 'enforce_first_or_last', + graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST + ) super(DjangoConnectionField, self).__init__(*args, **kwargs) @property @@ -51,7 +60,29 @@ class DjangoConnectionField(ConnectionField): return default_queryset & queryset @classmethod - def connection_resolver(cls, resolver, connection, default_manager, root, args, context, info): + def connection_resolver(cls, resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, args, context, info): + first = args.get('first') + last = args.get('last') + + if enforce_first_or_last: + assert first or last, ( + 'You must provide a `first` or `last` value to properly paginate the `{}` connection.' + ).format(info.field_name) + + if max_limit: + if first: + assert first <= max_limit, ( + 'Requesting {} records on the `{}` connection exceeds the `first` limit of {} records.' + ).format(first, info.field_name, max_limit) + args['first'] = min(first, max_limit) + + if last: + assert last <= max_limit, ( + 'Requesting {} records on the `{}` connection exceeds the `last` limit of {} records.' + ).format(first, info.field_name, max_limit) + args['last'] = min(last, max_limit) + iterable = resolver(root, args, context, info) if iterable is None: iterable = default_manager @@ -78,7 +109,14 @@ class DjangoConnectionField(ConnectionField): return connection def get_resolver(self, parent_resolver): - return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager()) + return partial( + self.connection_resolver, + parent_resolver, + self.type, + self.get_manager(), + self.max_limit, + self.enforce_first_or_last + ) def get_connection_field(*args, **kwargs): diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 061b2c6..fc414bf 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -67,16 +67,35 @@ class DjangoFilterConnectionField(DjangoConnectionField): return queryset @classmethod - def connection_resolver(cls, resolver, connection, default_manager, filterset_class, filtering_args, + def connection_resolver(cls, resolver, connection, default_manager, max_limit, + enforce_first_or_last, filterset_class, filtering_args, root, args, context, info): filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} qs = filterset_class( data=filter_kwargs, queryset=default_manager.get_queryset() ).qs + return super(DjangoFilterConnectionField, cls).connection_resolver( - resolver, connection, qs, root, args, context, info) + resolver, + connection, + qs, + max_limit, + enforce_first_or_last, + root, + args, + context, + info + ) def get_resolver(self, parent_resolver): - return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager(), - self.filterset_class, self.filtering_args) + return partial( + self.connection_resolver, + parent_resolver, + self.type, + self.get_manager(), + self.max_limit, + self.enforce_first_or_last, + self.filterset_class, + self.filtering_args + ) diff --git a/graphene_django/settings.py b/graphene_django/settings.py index d83642a..46d70ee 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -30,6 +30,11 @@ DEFAULTS = { 'SCHEMA_OUTPUT': 'schema.json', 'SCHEMA_INDENT': None, 'MIDDLEWARE': (), + # Set to True if the connection fields must have + # either the first or last argument + 'RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST': False, + # Max items returned in ConnectionFields / FilterConnectionFields + 'RELAY_CONNECTION_MAX_LIMIT': 100, } if settings.DEBUG: diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 3594000..c1deebb 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -12,6 +12,7 @@ from ..utils import DJANGO_FILTER_INSTALLED from ..compat import MissingType, JSONField from ..fields import DjangoConnectionField from ..types import DjangoObjectType +from ..settings import graphene_settings from .models import Article, Reporter pytestmark = pytest.mark.django_db @@ -452,3 +453,95 @@ def test_should_query_node_multiple_filtering(): result = schema.execute(query) assert not result.errors assert result.data == expected + + +def test_should_enforce_first_or_last(): + graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + 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 + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters { + edges { + node { + id + } + } + } + } + ''' + + expected = { + 'allReporters': None + } + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == ( + 'You must provide a `first` or `last` value to properly ' + 'paginate the `allReporters` connection.' + ) + assert result.data == expected + + +def test_should_error_if_first_is_greater_than_max(): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + 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 + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(first: 101) { + edges { + node { + id + } + } + } + } + ''' + + expected = { + 'allReporters': None + } + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == ( + 'Requesting 101 records on the `allReporters` connection ' + 'exceeds the `first` limit of 100 records.' + ) + assert result.data == expected + + graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False From 3da777c8679065934138b7af2dc02e7d0c719472 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 20 Apr 2017 01:15:05 -0700 Subject: [PATCH 17/18] Updated version to 1.3 - Require graphene 1.4 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 38a16c5..2d2e578 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ tests_require = [ setup( name='graphene-django', - version='1.2.1', + version='1.3', description='Graphene Django integration', long_description=open('README.rst').read(), @@ -43,7 +43,7 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphene>=1.1.3', + 'graphene>=1.4', 'Django>=1.6.0', 'iso8601', 'singledispatch>=3.4.0.3', From edc286f49cfe17ad9f8e29f4160f91deac765a6e Mon Sep 17 00:00:00 2001 From: Bryan Kimani Date: Mon, 1 May 2017 21:08:38 +0300 Subject: [PATCH 18/18] Update tutorial-plain.rst If your run ``$ python ./manage.py loaddata ingredients`` without installing ``ingredients`` app in the project ``settings.py`` you will get the following error ``CommandError: No fixture named 'ingredients' found``. So make sure ``ingredients`` app has been put on the ``settings.py`` INSTALLED_APPS section before running ``$ python ./manage.py loaddata ingredients``. --- docs/tutorial-plain.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index a4c98a9..50f0750 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -95,6 +95,9 @@ following: $ python ./manage.py loaddata ingredients Installed 6 object(s) from 1 fixture(s) + +Note: +If your run ``$ python ./manage.py loaddata ingredients`` without installing ``ingredients`` app in the project ``settings.py`` you will get the following error ``CommandError: No fixture named 'ingredients' found``. So make sure ``ingredients`` app has been put on the ``settings.py`` INSTALLED_APPS section before running ``$ python ./manage.py loaddata ingredients``. Alternatively you can use the Django admin interface to create some data yourself. You'll need to run the development server (see below), and