Skip to content

Commit

Permalink
Support "contains" and "overlap" filtering (v2) (#1100)
Browse files Browse the repository at this point in the history
* Fix project setup

* Support contains/overlap filters

* Add Python 2.7 support

* Adjust docstrings

* Remove unused fixtures
  • Loading branch information
lucas-bremond authored Jan 19, 2021
1 parent 66c8901 commit e0a5d1c
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 12 deletions.
5 changes: 4 additions & 1 deletion graphene_django/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ class MissingType(object):
# Postgres fields are only available in Django with psycopg2 installed
# and we cannot have psycopg2 on PyPy
from django.contrib.postgres.fields import (
IntegerRangeField,
ArrayField,
HStoreField,
JSONField as PGJSONField,
RangeField,
)
except ImportError:
ArrayField, HStoreField, PGJSONField, RangeField = (MissingType,) * 4
IntegerRangeField, ArrayField, HStoreField, PGJSONField, RangeField = (
MissingType,
) * 5

try:
# JSONField is only available from Django 3.1
Expand Down
128 changes: 128 additions & 0 deletions graphene_django/filter/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from mock import MagicMock
import pytest

from django.db import models
from django.db.models.query import QuerySet
from django_filters import filters
from django_filters import FilterSet
import graphene
from graphene.relay import Node
from graphene_django import DjangoObjectType
from graphene_django.utils import DJANGO_FILTER_INSTALLED

from ...compat import ArrayField

pytestmark = []

if DJANGO_FILTER_INSTALLED:
from graphene_django.filter import DjangoFilterConnectionField
else:
pytestmark.append(
pytest.mark.skipif(
True, reason="django_filters not installed or not compatible"
)
)


STORE = {"events": []}


@pytest.fixture
def Event():
class Event(models.Model):
name = models.CharField(max_length=50)
tags = ArrayField(models.CharField(max_length=50))

return Event


@pytest.fixture
def EventFilterSet(Event):

from django.contrib.postgres.forms import SimpleArrayField

class ArrayFilter(filters.Filter):
base_field_class = SimpleArrayField

class EventFilterSet(FilterSet):
class Meta:
model = Event
fields = {
"name": ["exact"],
}

tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains")
tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap")

return EventFilterSet


@pytest.fixture
def EventType(Event, EventFilterSet):
class EventType(DjangoObjectType):
class Meta:
model = Event
interfaces = (Node,)
filterset_class = EventFilterSet

return EventType


@pytest.fixture
def Query(Event, EventType):
class Query(graphene.ObjectType):
events = DjangoFilterConnectionField(EventType)

def resolve_events(self, info, **kwargs):

events = [
Event(name="Live Show", tags=["concert", "music", "rock"],),
Event(name="Musical", tags=["movie", "music"],),
Event(name="Ballet", tags=["concert", "dance"],),
]

STORE["events"] = events

m_queryset = MagicMock(spec=QuerySet)
m_queryset.model = Event

def filter_events(**kwargs):
if "tags__contains" in kwargs:
STORE["events"] = list(
filter(
lambda e: set(kwargs["tags__contains"]).issubset(
set(e.tags)
),
STORE["events"],
)
)
if "tags__overlap" in kwargs:
STORE["events"] = list(
filter(
lambda e: not set(kwargs["tags__overlap"]).isdisjoint(
set(e.tags)
),
STORE["events"],
)
)

def mock_queryset_filter(*args, **kwargs):
filter_events(**kwargs)
return m_queryset

def mock_queryset_none(*args, **kwargs):
STORE["events"] = []
return m_queryset

def mock_queryset_count(*args, **kwargs):
return len(STORE["events"])

m_queryset.all.return_value = m_queryset
m_queryset.filter.side_effect = mock_queryset_filter
m_queryset.none.side_effect = mock_queryset_none
m_queryset.count.side_effect = mock_queryset_count
m_queryset.__getitem__.side_effect = STORE["events"].__getitem__

return m_queryset

return Query
82 changes: 82 additions & 0 deletions graphene_django/filter/tests/test_contains_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import pytest

from graphene import Schema

from ...compat import ArrayField, MissingType


@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_string_contains_multiple(Query):
"""
Test contains filter on a string field.
"""

schema = Schema(query=Query)

query = """
query {
events (tags_Contains: ["concert", "music"]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Live Show"}},
]


@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_string_contains_one(Query):
"""
Test contains filter on a string field.
"""

schema = Schema(query=Query)

query = """
query {
events (tags_Contains: ["music"]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Live Show"}},
{"node": {"name": "Musical"}},
]


@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_string_contains_none(Query):
"""
Test contains filter on a string field.
"""

schema = Schema(query=Query)

query = """
query {
events (tags_Contains: []) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == []
84 changes: 84 additions & 0 deletions graphene_django/filter/tests/test_overlap_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import pytest

from graphene import Schema

from ...compat import ArrayField, MissingType


@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_string_overlap_multiple(Query):
"""
Test overlap filter on a string field.
"""

schema = Schema(query=Query)

query = """
query {
events (tags_Overlap: ["concert", "music"]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Live Show"}},
{"node": {"name": "Musical"}},
{"node": {"name": "Ballet"}},
]


@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_string_overlap_one(Query):
"""
Test overlap filter on a string field.
"""

schema = Schema(query=Query)

query = """
query {
events (tags_Overlap: ["music"]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Live Show"}},
{"node": {"name": "Musical"}},
]


@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_string_overlap_none(Query):
"""
Test overlap filter on a string field.
"""

schema = Schema(query=Query)

query = """
query {
events (tags_Overlap: []) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == []
16 changes: 7 additions & 9 deletions graphene_django/filter/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import six

from graphene import List
import graphene

from django_filters.utils import get_model_field
from django_filters.filters import Filter, BaseCSVFilter
Expand Down Expand Up @@ -41,11 +41,11 @@ def get_filtering_args_from_filterset(filterset_class, type):

field = convert_form_field(form_field)

if filter_type in ["in", "range"]:
# Replace CSV filters (`in`, `range`) argument type to be a list of
if filter_type in {"in", "range", "contains", "overlap"}:
# Replace CSV filters (`in`, `range`, `contains`, `overlap`) argument type to be a list of
# the same type as the field. See comments in
# `replace_csv_filters` method for more details.
field = List(field.get_type())
field = graphene.List(field.get_type())

field_type = field.Argument()
field_type.description = filter_field.label
Expand All @@ -71,7 +71,7 @@ def get_filterset_class(filterset_class, **meta):

def replace_csv_filters(filterset_class):
"""
Replace the "in" and "range" filters (that are not explicitly declared) to not be BaseCSVFilter (BaseInFilter, BaseRangeFilter) objects anymore
Replace the "in", "contains", "overlap" and "range" filters (that are not explicitly declared) to not be BaseCSVFilter (BaseInFilter, BaseRangeFilter) objects anymore
but regular Filter objects that simply use the input value as filter argument on the queryset.
This is because those BaseCSVFilter are expecting a string as input with comma separated value but with GraphQl we
Expand All @@ -81,8 +81,7 @@ def replace_csv_filters(filterset_class):
"""
for name, filter_field in six.iteritems(filterset_class.base_filters):
filter_type = filter_field.lookup_expr
if filter_type == "in":
assert isinstance(filter_field, BaseCSVFilter)
if filter_type in {"in", "contains", "overlap"}:
filterset_class.base_filters[name] = InFilter(
field_name=filter_field.field_name,
lookup_expr=filter_field.lookup_expr,
Expand All @@ -92,8 +91,7 @@ def replace_csv_filters(filterset_class):
**filter_field.extra
)

if filter_type == "range":
assert isinstance(filter_field, BaseCSVFilter)
elif filter_type == "range":
filterset_class.base_filters[name] = RangeFilter(
field_name=filter_field.field_name,
lookup_expr=filter_field.lookup_expr,
Expand Down
4 changes: 2 additions & 2 deletions graphene_django/tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import graphene
from graphene.relay import Node

from ..compat import JSONField, MissingType
from ..compat import IntegerRangeField, MissingType
from ..fields import DjangoConnectionField
from ..types import DjangoObjectType
from ..utils import DJANGO_FILTER_INSTALLED
Expand Down Expand Up @@ -113,7 +113,7 @@ def resolve_reporter(self, info):
assert result.data == expected


@pytest.mark.skipif(JSONField is MissingType, reason="RangeField should exist")
@pytest.mark.skipif(IntegerRangeField is MissingType, reason="RangeField should exist")
def test_should_query_postgres_fields():
from django.contrib.postgres.fields import (
IntegerRangeField,
Expand Down

0 comments on commit e0a5d1c

Please sign in to comment.