From 5c191b9062ed1da958f741042ec29eedc716492a Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Sun, 24 Mar 2019 23:42:06 -0400 Subject: [PATCH 1/4] Add support for filterset_class meta parameter * Allow for use of either filter_fields or filterset_class * Add tests to check that the behavior is similar to filter_fields * Add documentation to show how to make use of the parameter --- docs/filtering.rst | 29 +++++++++++- graphene_django/converter.py | 5 +- graphene_django/filter/fields.py | 17 ++++--- graphene_django/filter/tests/test_fields.py | 52 +++++++++++++++++++++ graphene_django/types.py | 15 ++++-- 5 files changed, 105 insertions(+), 13 deletions(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index feafd40..e27f8ce 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -100,7 +100,7 @@ features of ``django-filter``. This is done by transparently creating a ``filter_fields``. However, you may find this to be insufficient. In these cases you can -create your own ``Filterset`` as follows: +create your own ``FilterSet``. You can pass it directly as follows: .. code:: python @@ -127,6 +127,33 @@ create your own ``Filterset`` as follows: all_animals = DjangoFilterConnectionField(AnimalNode, filterset_class=AnimalFilter) +You can also specify the ``FilterSet`` class using the ``filerset_class`` +parameter when defining your ``DjangoObjectType``, however, this can't be used +in unison with the ``filter_fields`` parameter: + +.. code:: python + + class AnimalFilter(django_filters.FilterSet): + # Do case-insensitive lookups on 'name' + name = django_filters.CharFilter(lookup_expr=['iexact']) + + class Meta: + # Assume you have an Animal model defined with the following fields + model = Animal + fields = ['name', 'genus', 'is_domesticated'] + + + class AnimalNode(DjangoObjectType): + class Meta: + model = Animal + filterset_class = AnimalFilter + interfaces = (relay.Node, ) + + + class Query(ObjectType): + animal = relay.Node.Field(AnimalNode) + all_animals = DjangoFilterConnectionField(AnimalNode) + 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 diff --git a/graphene_django/converter.py b/graphene_django/converter.py index c40313d..6fc1227 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -181,8 +181,9 @@ def convert_field_to_list_or_connection(field, registry=None): # into a DjangoConnectionField if _type._meta.connection: # Use a DjangoFilterConnectionField if there are - # defined filter_fields in the DjangoObjectType Meta - if _type._meta.filter_fields: + # defined filter_fields or a filterset_class in the + # DjangoObjectType Meta + if _type._meta.filter_fields or _type._meta.filterset_class: from .filter.fields import DjangoFilterConnectionField return DjangoFilterConnectionField(_type) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index cb42543..9aa629f 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -35,14 +35,17 @@ class DjangoFilterConnectionField(DjangoConnectionField): @property def filterset_class(self): if not self._filterset_class: - fields = self._fields or self.node_type._meta.filter_fields - meta = dict(model=self.model, fields=fields) - if self._extra_filter_meta: - meta.update(self._extra_filter_meta) + if not self.node_type._meta.filterset_class: + fields = self._fields or self.node_type._meta.filter_fields + meta = dict(model=self.model, fields=fields) + if self._extra_filter_meta: + meta.update(self._extra_filter_meta) - self._filterset_class = get_filterset_class( - self._provided_filterset_class, **meta - ) + self._filterset_class = get_filterset_class( + self._provided_filterset_class, **meta + ) + else: + self._filterset_class = self.node_type._meta.filterset_class return self._filterset_class diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index f9ef0ae..534ebb9 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -227,6 +227,58 @@ def test_filter_filterset_information_on_meta_related(): assert_not_orderable(articles_field) +def test_filter_filterset_class_information_on_meta(): + class ReporterFilter(FilterSet): + class Meta: + model = Reporter + fields = ["first_name", "articles"] + + class ReporterFilterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filterset_class = ReporterFilter + + field = DjangoFilterConnectionField(ReporterFilterNode) + assert_arguments(field, "first_name", "articles") + assert_not_orderable(field) + + +def test_filter_filterset_class_information_on_meta_related(): + class ReporterFilter(FilterSet): + class Meta: + model = Reporter + fields = ["first_name", "articles"] + + class ArticleFilter(FilterSet): + class Meta: + model = Article + fields = ["headline", "reporter"] + + class ReporterFilterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filterset_class = ReporterFilter + + class ArticleFilterNode(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + filterset_class = ArticleFilter + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterFilterNode) + all_articles = DjangoFilterConnectionField(ArticleFilterNode) + reporter = Field(ReporterFilterNode) + article = Field(ArticleFilterNode) + + schema = Schema(query=Query) + articles_field = ReporterFilterNode._meta.fields["articles"].get_type() + assert_arguments(articles_field, "headline", "reporter") + assert_not_orderable(articles_field) + + def test_filter_filterset_related_results(): class ReporterFilterNode(DjangoObjectType): class Meta: diff --git a/graphene_django/types.py b/graphene_django/types.py index 4441a9a..ef72b9b 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -44,6 +44,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions): connection = None # type: Type[Connection] filter_fields = () + filterset_class = None class DjangoObjectType(ObjectType): @@ -56,6 +57,7 @@ class DjangoObjectType(ObjectType): only_fields=(), exclude_fields=(), filter_fields=None, + filterset_class=None, connection=None, connection_class=None, use_connection=None, @@ -74,9 +76,15 @@ class DjangoObjectType(ObjectType): "The attribute registry in {} needs to be an instance of " 'Registry, received "{}".' ).format(cls.__name__, registry) - - if not DJANGO_FILTER_INSTALLED and filter_fields: - raise Exception("Can only set filter_fields if Django-Filter is installed") + + if filter_fields and filterset_class: + raise Exception("Can't set both filter_fields and filterset_class") + + if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): + raise Exception(( + "Can only set filter_fields or filterset_class if " + "Django-Filter is installed" + )) django_fields = yank_fields_from_attrs( construct_fields(model, registry, only_fields, exclude_fields), _as=Field @@ -107,6 +115,7 @@ class DjangoObjectType(ObjectType): _meta.model = model _meta.registry = registry _meta.filter_fields = filter_fields + _meta.filterset_class = filterset_class _meta.fields = django_fields _meta.connection = connection From 4d905a46ac39a2f494f94ab177c84ba7c0c859cc Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Mon, 25 Mar 2019 10:03:54 -0400 Subject: [PATCH 2/4] Fixed flake8 lint error --- graphene_django/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index ef72b9b..b33c6bf 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -76,10 +76,10 @@ class DjangoObjectType(ObjectType): "The attribute registry in {} needs to be an instance of " 'Registry, received "{}".' ).format(cls.__name__, registry) - + if filter_fields and filterset_class: raise Exception("Can't set both filter_fields and filterset_class") - + if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): raise Exception(( "Can only set filter_fields or filterset_class if " From 132c4cb9d4174ced2ca716609e6f730f21d799ff Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Mon, 25 Mar 2019 23:45:14 -0400 Subject: [PATCH 3/4] Fixed so that GrapheneFilterSetMixin is used with any provided filterset_class --- graphene_django/filter/fields.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 9aa629f..7c85e9a 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -35,17 +35,16 @@ class DjangoFilterConnectionField(DjangoConnectionField): @property def filterset_class(self): if not self._filterset_class: - if not self.node_type._meta.filterset_class: - fields = self._fields or self.node_type._meta.filter_fields - meta = dict(model=self.model, fields=fields) - if self._extra_filter_meta: - meta.update(self._extra_filter_meta) + fields = self._fields or self.node_type._meta.filter_fields + meta = dict(model=self.model, fields=fields) + if self._extra_filter_meta: + meta.update(self._extra_filter_meta) - self._filterset_class = get_filterset_class( - self._provided_filterset_class, **meta - ) - else: - self._filterset_class = self.node_type._meta.filterset_class + filterset_class = self._provided_filterset_class or ( + self.node_type._meta.filterset_class) + self._filterset_class = get_filterset_class( + filterset_class, **meta + ) return self._filterset_class From d2f8bf730bbe571dbe568621e630c8f01dec9c55 Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Wed, 27 Mar 2019 14:05:42 -0400 Subject: [PATCH 4/4] Test exception when both filterset_class and filter_fields are set --- graphene_django/filter/tests/test_fields.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 534ebb9..eb6581b 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -227,6 +227,21 @@ def test_filter_filterset_information_on_meta_related(): assert_not_orderable(articles_field) +def test_filter_filterset_class_filter_fields_exception(): + with pytest.raises(Exception): + class ReporterFilter(FilterSet): + class Meta: + model = Reporter + fields = ["first_name", "articles"] + + class ReporterFilterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filterset_class = ReporterFilter + filter_fields = ["first_name", "articles"] + + def test_filter_filterset_class_information_on_meta(): class ReporterFilter(FilterSet): class Meta: