diff --git a/docs/filtering.rst b/docs/filtering.rst index d02366f20..76619287d 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 c40313df0..6fc1227f2 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -181,8 +181,9 @@ def dynamic_type(): # 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 cb4254370..7c85e9a6d 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -40,8 +40,10 @@ def filterset_class(self): if self._extra_filter_meta: meta.update(self._extra_filter_meta) + filterset_class = self._provided_filterset_class or ( + self.node_type._meta.filterset_class) self._filterset_class = get_filterset_class( - self._provided_filterset_class, **meta + filterset_class, **meta ) return self._filterset_class diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index f9ef0ae2f..eb6581b30 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -227,6 +227,73 @@ class Query(ObjectType): 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: + 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 d7a049367..ded8a1580 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -45,6 +45,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions): connection = None # type: Type[Connection] filter_fields = () + filterset_class = None class DjangoObjectType(ObjectType): @@ -57,6 +58,7 @@ def __init_subclass_with_meta__( only_fields=(), exclude_fields=(), filter_fields=None, + filterset_class=None, connection=None, connection_class=None, use_connection=None, @@ -76,8 +78,14 @@ def __init_subclass_with_meta__( '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 @@ -108,6 +116,7 @@ def __init_subclass_with_meta__( _meta.model = model _meta.registry = registry _meta.filter_fields = filter_fields + _meta.filterset_class = filterset_class _meta.fields = django_fields _meta.connection = connection