From 4bdcf054ebdb29e84ea190bb6becc8dc27ca286d Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Fri, 28 Jul 2017 16:46:39 +0200 Subject: [PATCH 01/11] Pass context object to FilterSet instance to support request-baed filtering (fixes #203). --- docs/filtering.rst | 20 ++++++++++ graphene_django/filter/fields.py | 3 +- graphene_django/filter/tests/test_fields.py | 42 +++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index f6ad882..2e6b87f 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -126,3 +126,23 @@ create your own ``Filterset`` as follows: # We specify our custom AnimalFilter using the filterset_class param all_animals = DjangoFilterConnectionField(AnimalNode, filterset_class=AnimalFilter) + +The context argument is passed on as the `request argument `__ +in a ``django_filters.FilterSet`` instance. You can use this to customize your +filters to be context-dependent. We could modify the ``AnimalFilter`` above to +pre-filter animals owned by the authenticated user (set in ``context.user``). + +.. code:: python + + class AnimalFilter(django_filters.FilterSet): + # Do case-insensitive lookups on 'name' + name = django_filters.CharFilter(lookup_type='iexact') + + class Meta: + model = Animal + fields = ['name', 'genus', 'is_domesticated'] + + @property + def qs(self): + # The query context can be found in self.request. + return super(AnimalFilter, self).filter(owner=self.request.user) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index fc414bf..68a9072 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -73,7 +73,8 @@ class DjangoFilterConnectionField(DjangoConnectionField): 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() + queryset=default_manager.get_queryset(), + request=context ).qs return super(DjangoFilterConnectionField, cls).connection_resolver( diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 1b24ff2..565e4f2 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -136,6 +136,48 @@ def test_filter_shortcut_filterset_extra_meta(): assert 'headline' not in field.filterset_class.get_fields() +def test_filter_shortcut_filterset_context(): + class ArticleContextFilter(django_filters.FilterSet): + + class Meta: + model = Article + exclude = set() + + @property + def qs(self): + qs = super(ArticleContextFilter, self).qs + return qs.filter(reporter=self.request.reporter) + + class Query(ObjectType): + context_articles = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleContextFilter) + + r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') + r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') + Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1, editor=r1) + Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2, editor=r2) + + class context(object): + reporter = r2 + + query = ''' + query { + contextArticles { + edges { + node { + headline + } + } + } + } + ''' + schema = Schema(query=Query) + result = schema.execute(query, context_value=context()) + assert not result.errors + + assert len(result.data['contextArticles']['edges']) == 1 + assert result.data['contextArticles']['edges'][0]['node']['headline'] == 'a2' + + def test_filter_filterset_information_on_meta(): class ReporterFilterNode(DjangoObjectType): From 73905547c8391c009228f3ecfda7750af784bbea Mon Sep 17 00:00:00 2001 From: Miguel Date: Sat, 9 Sep 2017 15:21:34 -0400 Subject: [PATCH 02/11] Fix tutorial-plain.rst typo in " Getting single objects" code example --- docs/tutorial-plain.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 6aa4294..93e55a9 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return Ingredient.objects.all() def resolve_category(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Category.objects.get(pk=id) @@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return None def resolve_ingredient(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Ingredient.objects.get(pk=id) From 69cec060d8d5b9bc5e6403e608079eadf77d1655 Mon Sep 17 00:00:00 2001 From: Cameron Dawson Date: Fri, 21 Jul 2017 11:23:16 -0700 Subject: [PATCH 03/11] Fix select_related with filtering --- graphene_django/filter/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index a80d8d7..b93977b 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -61,7 +61,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): 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 = queryset & default_queryset queryset.query.set_limits(low, high) return queryset From 1d76db8164a2a3723e41141209a8923bad5044bd Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 9 Oct 2017 23:00:09 -0500 Subject: [PATCH 04/11] Use super for base queryset logic --- graphene_django/filter/fields.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index b93977b..62c50d9 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -43,8 +43,8 @@ class DjangoFilterConnectionField(DjangoConnectionField): def filtering_args(self): return get_filtering_args_from_filterset(self.filterset_class, self.node_type) - @staticmethod - def merge_querysets(default_queryset, queryset): + @classmethod + def merge_querysets(cls, 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 @@ -61,7 +61,9 @@ class DjangoFilterConnectionField(DjangoConnectionField): 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 = queryset & default_queryset + + queryset = super(cls, cls).merge_querysets(default_queryset, queryset) + queryset.query.set_limits(low, high) return queryset From 00b5a176d3e591cb5f007a30e8f43e0654b3f62a Mon Sep 17 00:00:00 2001 From: Vincent Poulailleau Date: Mon, 30 Oct 2017 09:54:09 +0100 Subject: [PATCH 05/11] typo in authorization.rst fix a small typo error in the documentation --- docs/authorization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index 1e2ec81..707dbf6 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 attribute. .. code:: python From 2a39f5d8eaba3f7772c63b012a974bb9a841fb9f Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Mon, 30 Oct 2017 14:35:29 -0700 Subject: [PATCH 06/11] Allow abstract Connection Class to DjangoObjectType referred to as connection_class, it will instantiate the connection from the provided class or default to graphene.Connection if not supplied --- graphene_django/tests/test_types.py | 15 ++++++++++++++- graphene_django/types.py | 8 ++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index f0185d4..83d9b40 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,6 +1,6 @@ from mock import patch -from graphene import Interface, ObjectType, Schema +from graphene import Interface, ObjectType, Schema, Connection, String from graphene.relay import Node from .. import registry @@ -17,11 +17,23 @@ class Reporter(DjangoObjectType): model = ReporterModel +class ArticleConnection(Connection): + '''Article Connection''' + test = String() + + def resolve_test(): + return 'test' + + class Meta: + abstract = True + + class Article(DjangoObjectType): '''Article description''' class Meta: model = ArticleModel interfaces = (Node, ) + connection_class = ArticleConnection class RootQuery(ObjectType): @@ -74,6 +86,7 @@ type Article implements Node { type ArticleConnection { pageInfo: PageInfo! edges: [ArticleEdge]! + test: String } type ArticleEdge { diff --git a/graphene_django/types.py b/graphene_django/types.py index aeef7a6..684863a 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -45,7 +45,7 @@ class DjangoObjectType(ObjectType): @classmethod def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False, only_fields=(), exclude_fields=(), filter_fields=None, connection=None, - use_connection=None, interfaces=(), **options): + connection_class=None, use_connection=None, interfaces=(), **options): assert is_valid_django_model(model), ( 'You need to pass a valid Django Model in {}.Meta, received "{}".' ).format(cls.__name__, model) @@ -71,7 +71,11 @@ class DjangoObjectType(ObjectType): if use_connection and not connection: # We create the connection automatically - connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls) + if not connection_class: + connection_class = Connection + + connection = connection_class.create_type( + '{}Connection'.format(cls.__name__), node=cls) if connection is not None: assert issubclass(connection, Connection), ( From bbcd69967c93d90853e024da9f4ab2073afa5cea Mon Sep 17 00:00:00 2001 From: Justin Tervay <7595639+tervay@users.noreply.github.com> Date: Wed, 1 Nov 2017 13:56:28 -0700 Subject: [PATCH 07/11] Fix typos --- docs/tutorial-plain.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 592f244..d5045aa 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return Ingredient.objects.all() def resolve_category(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Category.objects.get(pk=id) @@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return None def resolve_ingredient(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Ingredient.objects.get(pk=id) From 5491e2cb0094e23ae6f63c4b11f483d4ad33df84 Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:25:22 -0700 Subject: [PATCH 08/11] add test to show .reverse() not being perserved --- graphene_django/filter/tests/test_fields.py | 81 ++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 9a0ba21..1844f4b 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from graphene import Field, ObjectType, Schema, Argument, Float +from graphene import Field, ObjectType, Schema, Argument, Float, Boolean from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import (GlobalIDFormField, @@ -534,3 +534,82 @@ def test_should_query_filter_node_double_limit_raises(): assert str(result.errors[0]) == ( 'Received two sliced querysets (high mark) in the connection, please slice only in one.' ) + +def test_order_by_is_perserved(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node, ) + filter_fields = () + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterType, reverse_order=Boolean()) + + def resolve_all_reporters(self, info, reverse_order=False, **args): + reporters = Reporter.objects.order_by('first_name') + + if reverse_order: + return reporters.reverse() + + return reporters + + Reporter.objects.create( + first_name='b', + ) + r = Reporter.objects.create( + first_name='a', + ) + + schema = Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(first: 1) { + edges { + node { + firstName + } + } + } + } + ''' + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'firstName': 'a', + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + + reverse_query = ''' + query NodeFilteringQuery { + allReporters(first: 1, reverseOrder: true) { + edges { + node { + firstName + } + } + } + } + ''' + + reverse_expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'firstName': 'b', + } + }] + } + } + + reverse_result = schema.execute(reverse_query) + + assert not reverse_result.errors + assert reverse_result.data == reverse_expected From 6d0837e7cbf001992a599d54a5f9ecc0b1601dd6 Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:26:33 -0700 Subject: [PATCH 09/11] add test to show annotation not being perservered --- graphene_django/filter/tests/test_fields.py | 59 ++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 1844f4b..ef09cfa 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from graphene import Field, ObjectType, Schema, Argument, Float, Boolean +from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import (GlobalIDFormField, @@ -10,6 +10,10 @@ from graphene_django.forms import (GlobalIDFormField, from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED +# for annotation test +from django.db.models import TextField, Value +from django.db.models.functions import Concat + pytestmark = [] if DJANGO_FILTER_INSTALLED: @@ -613,3 +617,56 @@ def test_order_by_is_perserved(): assert not reverse_result.errors assert reverse_result.data == reverse_expected + +def test_annotation_is_perserved(): + class ReporterType(DjangoObjectType): + full_name = String() + + def resolve_full_name(instance, info, **args): + return instance.full_name + + class Meta: + model = Reporter + interfaces = (Node, ) + filter_fields = () + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return Reporter.objects.annotate( + full_name=Concat('first_name', Value(' '), 'last_name', output_field=TextField()) + ) + + Reporter.objects.create( + first_name='John', + last_name='Doe', + ) + + schema = Schema(query=Query) + + query = ''' + query NodeFilteringQuery { + allReporters(first: 1) { + edges { + node { + fullName + } + } + } + } + ''' + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'fullName': 'John Doe', + } + }] + } + } + + result = schema.execute(query) + + assert not result.errors + assert result.data == expected From 4013f78ecb012dd5aad32dfc0718bce59ac35fea Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:26:49 -0700 Subject: [PATCH 10/11] fix default_queryset overriding queryset when merging queries --- graphene_django/filter/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index a80d8d7..b93977b 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -61,7 +61,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): 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 = queryset & default_queryset queryset.query.set_limits(low, high) return queryset From a3f3d90ab76cab8301f63e7f6377f153f470ead9 Mon Sep 17 00:00:00 2001 From: mekhami Date: Sat, 4 Nov 2017 12:29:58 -0500 Subject: [PATCH 11/11] Update README to reflect that resolve_only_args is deprecated As resolve_only_args is deprecated, let's remove it from the README. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 6f6d90a..1dd8301 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ class User(DjangoObjectType): class Query(graphene.ObjectType): users = graphene.List(User) - @graphene.resolve_only_args def resolve_users(self): return UserModel.objects.all()