mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-24 10:34:10 +03:00
fix: empty list is not an empty value for list filters even when a custom filtering method is provided (#1450)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
This commit is contained in:
parent
720db1f987
commit
0473f1a9a3
|
@ -1,3 +1,6 @@
|
||||||
|
import sys
|
||||||
|
from pathlib import PurePath
|
||||||
|
|
||||||
# For backwards compatibility, we import JSONField to have it available for import via
|
# For backwards compatibility, we import JSONField to have it available for import via
|
||||||
# this compat module (https://github.com/graphql-python/graphene-django/issues/1428).
|
# this compat module (https://github.com/graphql-python/graphene-django/issues/1428).
|
||||||
# Django's JSONField is available in Django 3.2+ (the minimum version we support)
|
# Django's JSONField is available in Django 3.2+ (the minimum version we support)
|
||||||
|
@ -19,4 +22,23 @@ try:
|
||||||
RangeField,
|
RangeField,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
IntegerRangeField, ArrayField, HStoreField, RangeField = (MissingType,) * 4
|
IntegerRangeField, HStoreField, RangeField = (MissingType,) * 3
|
||||||
|
|
||||||
|
# For unit tests we fake ArrayField using JSONFields
|
||||||
|
if any(
|
||||||
|
PurePath(sys.argv[0]).match(p)
|
||||||
|
for p in [
|
||||||
|
"**/pytest",
|
||||||
|
"**/py.test",
|
||||||
|
"**/pytest/__main__.py",
|
||||||
|
]
|
||||||
|
):
|
||||||
|
|
||||||
|
class ArrayField(JSONField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if len(args) > 0:
|
||||||
|
self.base_field = args[0]
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
ArrayField = MissingType
|
||||||
|
|
|
@ -1,13 +1,36 @@
|
||||||
from django_filters.constants import EMPTY_VALUES
|
from django_filters.constants import EMPTY_VALUES
|
||||||
|
from django_filters.filters import FilterMethod
|
||||||
|
|
||||||
from .typed_filter import TypedFilter
|
from .typed_filter import TypedFilter
|
||||||
|
|
||||||
|
|
||||||
|
class ArrayFilterMethod(FilterMethod):
|
||||||
|
def __call__(self, qs, value):
|
||||||
|
if value is None:
|
||||||
|
return qs
|
||||||
|
return self.method(qs, self.f.field_name, value)
|
||||||
|
|
||||||
|
|
||||||
class ArrayFilter(TypedFilter):
|
class ArrayFilter(TypedFilter):
|
||||||
"""
|
"""
|
||||||
Filter made for PostgreSQL ArrayField.
|
Filter made for PostgreSQL ArrayField.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@TypedFilter.method.setter
|
||||||
|
def method(self, value):
|
||||||
|
"""
|
||||||
|
Override method setter so that in case a custom `method` is provided
|
||||||
|
(see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method),
|
||||||
|
it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method
|
||||||
|
of the `FilterMethod` class) and instead use our ArrayFilterMethod that consider empty lists as values.
|
||||||
|
|
||||||
|
Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)`
|
||||||
|
which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead.
|
||||||
|
"""
|
||||||
|
TypedFilter.method.fset(self, value)
|
||||||
|
if value is not None:
|
||||||
|
self.filter = ArrayFilterMethod(self)
|
||||||
|
|
||||||
def filter(self, qs, value):
|
def filter(self, qs, value):
|
||||||
"""
|
"""
|
||||||
Override the default filter class to check first whether the list is
|
Override the default filter class to check first whether the list is
|
||||||
|
|
|
@ -1,12 +1,36 @@
|
||||||
|
from django_filters.filters import FilterMethod
|
||||||
|
|
||||||
from .typed_filter import TypedFilter
|
from .typed_filter import TypedFilter
|
||||||
|
|
||||||
|
|
||||||
|
class ListFilterMethod(FilterMethod):
|
||||||
|
def __call__(self, qs, value):
|
||||||
|
if value is None:
|
||||||
|
return qs
|
||||||
|
return self.method(qs, self.f.field_name, value)
|
||||||
|
|
||||||
|
|
||||||
class ListFilter(TypedFilter):
|
class ListFilter(TypedFilter):
|
||||||
"""
|
"""
|
||||||
Filter that takes a list of value as input.
|
Filter that takes a list of value as input.
|
||||||
It is for example used for `__in` filters.
|
It is for example used for `__in` filters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@TypedFilter.method.setter
|
||||||
|
def method(self, value):
|
||||||
|
"""
|
||||||
|
Override method setter so that in case a custom `method` is provided
|
||||||
|
(see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method),
|
||||||
|
it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method
|
||||||
|
of the `FilterMethod` class) and instead use our ListFilterMethod that consider empty lists as values.
|
||||||
|
|
||||||
|
Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)`
|
||||||
|
which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead.
|
||||||
|
"""
|
||||||
|
TypedFilter.method.fset(self, value)
|
||||||
|
if value is not None:
|
||||||
|
self.filter = ListFilterMethod(self)
|
||||||
|
|
||||||
def filter(self, qs, value):
|
def filter(self, qs, value):
|
||||||
"""
|
"""
|
||||||
Override the default filter class to check first whether the list is
|
Override the default filter class to check first whether the list is
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from unittest.mock import MagicMock
|
from functools import reduce
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -25,15 +25,15 @@ else:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
STORE = {"events": []}
|
|
||||||
|
|
||||||
|
|
||||||
class Event(models.Model):
|
class Event(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
tags = ArrayField(models.CharField(max_length=50))
|
tags = ArrayField(models.CharField(max_length=50))
|
||||||
tag_ids = ArrayField(models.IntegerField())
|
tag_ids = ArrayField(models.IntegerField())
|
||||||
random_field = ArrayField(models.BooleanField())
|
random_field = ArrayField(models.BooleanField())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Event [{self.name}]"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def EventFilterSet():
|
def EventFilterSet():
|
||||||
|
@ -48,6 +48,14 @@ def EventFilterSet():
|
||||||
tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains")
|
tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains")
|
||||||
tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap")
|
tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap")
|
||||||
tags = ArrayFilter(field_name="tags", lookup_expr="exact")
|
tags = ArrayFilter(field_name="tags", lookup_expr="exact")
|
||||||
|
tags__len = ArrayFilter(
|
||||||
|
field_name="tags", lookup_expr="len", input_type=graphene.Int
|
||||||
|
)
|
||||||
|
tags__len__in = ArrayFilter(
|
||||||
|
field_name="tags",
|
||||||
|
method="tags__len__in_filter",
|
||||||
|
input_type=graphene.List(graphene.Int),
|
||||||
|
)
|
||||||
|
|
||||||
# Those are actually not usable and only to check type declarations
|
# Those are actually not usable and only to check type declarations
|
||||||
tags_ids__contains = ArrayFilter(field_name="tag_ids", lookup_expr="contains")
|
tags_ids__contains = ArrayFilter(field_name="tag_ids", lookup_expr="contains")
|
||||||
|
@ -61,6 +69,14 @@ def EventFilterSet():
|
||||||
)
|
)
|
||||||
random_field = ArrayFilter(field_name="random_field", lookup_expr="exact")
|
random_field = ArrayFilter(field_name="random_field", lookup_expr="exact")
|
||||||
|
|
||||||
|
def tags__len__in_filter(self, queryset, _name, value):
|
||||||
|
if not value:
|
||||||
|
return queryset.none()
|
||||||
|
return reduce(
|
||||||
|
lambda q1, q2: q1.union(q2),
|
||||||
|
[queryset.filter(tags__len=v) for v in value],
|
||||||
|
).distinct()
|
||||||
|
|
||||||
return EventFilterSet
|
return EventFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,68 +99,94 @@ def Query(EventType):
|
||||||
we are running unit tests in sqlite which does not have ArrayFields.
|
we are running unit tests in sqlite which does not have ArrayFields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
events = [
|
||||||
|
Event(name="Live Show", tags=["concert", "music", "rock"]),
|
||||||
|
Event(name="Musical", tags=["movie", "music"]),
|
||||||
|
Event(name="Ballet", tags=["concert", "dance"]),
|
||||||
|
Event(name="Speech", tags=[]),
|
||||||
|
]
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
events = DjangoFilterConnectionField(EventType)
|
events = DjangoFilterConnectionField(EventType)
|
||||||
|
|
||||||
def resolve_events(self, info, **kwargs):
|
def resolve_events(self, info, **kwargs):
|
||||||
events = [
|
class FakeQuerySet(QuerySet):
|
||||||
Event(name="Live Show", tags=["concert", "music", "rock"]),
|
def __init__(self, model=None):
|
||||||
Event(name="Musical", tags=["movie", "music"]),
|
self.model = Event
|
||||||
Event(name="Ballet", tags=["concert", "dance"]),
|
self.__store = list(events)
|
||||||
Event(name="Speech", tags=[]),
|
|
||||||
]
|
|
||||||
|
|
||||||
STORE["events"] = events
|
def all(self):
|
||||||
|
return self
|
||||||
|
|
||||||
m_queryset = MagicMock(spec=QuerySet)
|
def filter(self, **kwargs):
|
||||||
m_queryset.model = Event
|
queryset = FakeQuerySet()
|
||||||
|
queryset.__store = list(self.__store)
|
||||||
def filter_events(**kwargs):
|
if "tags__contains" in kwargs:
|
||||||
if "tags__contains" in kwargs:
|
queryset.__store = list(
|
||||||
STORE["events"] = list(
|
filter(
|
||||||
filter(
|
lambda e: set(kwargs["tags__contains"]).issubset(
|
||||||
lambda e: set(kwargs["tags__contains"]).issubset(
|
set(e.tags)
|
||||||
set(e.tags)
|
),
|
||||||
),
|
queryset.__store,
|
||||||
STORE["events"],
|
)
|
||||||
)
|
)
|
||||||
)
|
if "tags__overlap" in kwargs:
|
||||||
if "tags__overlap" in kwargs:
|
queryset.__store = list(
|
||||||
STORE["events"] = list(
|
filter(
|
||||||
filter(
|
lambda e: not set(kwargs["tags__overlap"]).isdisjoint(
|
||||||
lambda e: not set(kwargs["tags__overlap"]).isdisjoint(
|
set(e.tags)
|
||||||
set(e.tags)
|
),
|
||||||
),
|
queryset.__store,
|
||||||
STORE["events"],
|
)
|
||||||
)
|
)
|
||||||
)
|
if "tags__exact" in kwargs:
|
||||||
if "tags__exact" in kwargs:
|
queryset.__store = list(
|
||||||
STORE["events"] = list(
|
filter(
|
||||||
filter(
|
lambda e: set(kwargs["tags__exact"]) == set(e.tags),
|
||||||
lambda e: set(kwargs["tags__exact"]) == set(e.tags),
|
queryset.__store,
|
||||||
STORE["events"],
|
)
|
||||||
)
|
)
|
||||||
)
|
if "tags__len" in kwargs:
|
||||||
|
queryset.__store = list(
|
||||||
|
filter(
|
||||||
|
lambda e: len(e.tags) == kwargs["tags__len"],
|
||||||
|
queryset.__store,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
def mock_queryset_filter(*args, **kwargs):
|
def union(self, *args):
|
||||||
filter_events(**kwargs)
|
queryset = FakeQuerySet()
|
||||||
return m_queryset
|
queryset.__store = self.__store
|
||||||
|
for arg in args:
|
||||||
|
queryset.__store += arg.__store
|
||||||
|
return queryset
|
||||||
|
|
||||||
def mock_queryset_none(*args, **kwargs):
|
def none(self):
|
||||||
STORE["events"] = []
|
queryset = FakeQuerySet()
|
||||||
return m_queryset
|
queryset.__store = []
|
||||||
|
return queryset
|
||||||
|
|
||||||
def mock_queryset_count(*args, **kwargs):
|
def count(self):
|
||||||
return len(STORE["events"])
|
return len(self.__store)
|
||||||
|
|
||||||
m_queryset.all.return_value = m_queryset
|
def distinct(self):
|
||||||
m_queryset.filter.side_effect = mock_queryset_filter
|
queryset = FakeQuerySet()
|
||||||
m_queryset.none.side_effect = mock_queryset_none
|
queryset.__store = []
|
||||||
m_queryset.count.side_effect = mock_queryset_count
|
for event in self.__store:
|
||||||
m_queryset.__getitem__.side_effect = lambda index: STORE[
|
if event not in queryset.__store:
|
||||||
"events"
|
queryset.__store.append(event)
|
||||||
].__getitem__(index)
|
queryset.__store = sorted(queryset.__store, key=lambda e: e.name)
|
||||||
|
return queryset
|
||||||
|
|
||||||
return m_queryset
|
def __getitem__(self, index):
|
||||||
|
return self.__store[index]
|
||||||
|
|
||||||
|
return FakeQuerySet()
|
||||||
|
|
||||||
return Query
|
return Query
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def schema(Query):
|
||||||
|
return graphene.Schema(query=Query)
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from graphene import Schema
|
|
||||||
|
|
||||||
from ...compat import ArrayField, MissingType
|
from ...compat import ArrayField, MissingType
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_contains_multiple(Query):
|
def test_array_field_contains_multiple(schema):
|
||||||
"""
|
"""
|
||||||
Test contains filter on a array field of string.
|
Test contains filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags_Contains: ["concert", "music"]) {
|
events (tags_Contains: ["concert", "music"]) {
|
||||||
|
@ -32,13 +28,11 @@ def test_array_field_contains_multiple(Query):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_contains_one(Query):
|
def test_array_field_contains_one(schema):
|
||||||
"""
|
"""
|
||||||
Test contains filter on a array field of string.
|
Test contains filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags_Contains: ["music"]) {
|
events (tags_Contains: ["music"]) {
|
||||||
|
@ -59,13 +53,11 @@ def test_array_field_contains_one(Query):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_contains_empty_list(Query):
|
def test_array_field_contains_empty_list(schema):
|
||||||
"""
|
"""
|
||||||
Test contains filter on a array field of string.
|
Test contains filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags_Contains: []) {
|
events (tags_Contains: []) {
|
||||||
|
|
186
graphene_django/filter/tests/test_array_field_custom_filter.py
Normal file
186
graphene_django/filter/tests/test_array_field_custom_filter.py
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ...compat import ArrayField, MissingType
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
|
def test_array_field_len_filter(schema):
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len: 2) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["events"]["edges"] == [
|
||||||
|
{"node": {"name": "Musical"}},
|
||||||
|
{"node": {"name": "Ballet"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len: 0) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["events"]["edges"] == [
|
||||||
|
{"node": {"name": "Speech"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len: 10) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["events"]["edges"] == []
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len: "2") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == 'Int cannot represent non-integer value: "2"'
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len: True) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Int cannot represent non-integer value: True"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
|
def test_array_field_custom_filter(schema):
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len_In: 2) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["events"]["edges"] == [
|
||||||
|
{"node": {"name": "Ballet"}},
|
||||||
|
{"node": {"name": "Musical"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len_In: [0, 2]) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["events"]["edges"] == [
|
||||||
|
{"node": {"name": "Ballet"}},
|
||||||
|
{"node": {"name": "Musical"}},
|
||||||
|
{"node": {"name": "Speech"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len_In: [10]) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["events"]["edges"] == []
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len_In: []) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["events"]["edges"] == []
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len_In: "12") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == 'Int cannot represent non-integer value: "12"'
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
events (tags_Len_In: True) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Int cannot represent non-integer value: True"
|
|
@ -1,18 +1,14 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from graphene import Schema
|
|
||||||
|
|
||||||
from ...compat import ArrayField, MissingType
|
from ...compat import ArrayField, MissingType
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_exact_no_match(Query):
|
def test_array_field_exact_no_match(schema):
|
||||||
"""
|
"""
|
||||||
Test exact filter on a array field of string.
|
Test exact filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags: ["concert", "music"]) {
|
events (tags: ["concert", "music"]) {
|
||||||
|
@ -30,13 +26,11 @@ def test_array_field_exact_no_match(Query):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_exact_match(Query):
|
def test_array_field_exact_match(schema):
|
||||||
"""
|
"""
|
||||||
Test exact filter on a array field of string.
|
Test exact filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags: ["movie", "music"]) {
|
events (tags: ["movie", "music"]) {
|
||||||
|
@ -56,13 +50,11 @@ def test_array_field_exact_match(Query):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_exact_empty_list(Query):
|
def test_array_field_exact_empty_list(schema):
|
||||||
"""
|
"""
|
||||||
Test exact filter on a array field of string.
|
Test exact filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags: []) {
|
events (tags: []) {
|
||||||
|
@ -82,11 +74,10 @@ def test_array_field_exact_empty_list(Query):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_filter_schema_type(Query):
|
def test_array_field_filter_schema_type(schema):
|
||||||
"""
|
"""
|
||||||
Check that the type in the filter is an array field like on the object type.
|
Check that the type in the filter is an array field like on the object type.
|
||||||
"""
|
"""
|
||||||
schema = Schema(query=Query)
|
|
||||||
schema_str = str(schema)
|
schema_str = str(schema)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
|
@ -112,6 +103,8 @@ def test_array_field_filter_schema_type(Query):
|
||||||
"tags_Contains": "[String!]",
|
"tags_Contains": "[String!]",
|
||||||
"tags_Overlap": "[String!]",
|
"tags_Overlap": "[String!]",
|
||||||
"tags": "[String!]",
|
"tags": "[String!]",
|
||||||
|
"tags_Len": "Int",
|
||||||
|
"tags_Len_In": "[Int]",
|
||||||
"tagsIds_Contains": "[Int!]",
|
"tagsIds_Contains": "[Int!]",
|
||||||
"tagsIds_Overlap": "[Int!]",
|
"tagsIds_Overlap": "[Int!]",
|
||||||
"tagsIds": "[Int!]",
|
"tagsIds": "[Int!]",
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from graphene import Schema
|
|
||||||
|
|
||||||
from ...compat import ArrayField, MissingType
|
from ...compat import ArrayField, MissingType
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_overlap_multiple(Query):
|
def test_array_field_overlap_multiple(schema):
|
||||||
"""
|
"""
|
||||||
Test overlap filter on a array field of string.
|
Test overlap filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags_Overlap: ["concert", "music"]) {
|
events (tags_Overlap: ["concert", "music"]) {
|
||||||
|
@ -34,13 +30,11 @@ def test_array_field_overlap_multiple(Query):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_overlap_one(Query):
|
def test_array_field_overlap_one(schema):
|
||||||
"""
|
"""
|
||||||
Test overlap filter on a array field of string.
|
Test overlap filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags_Overlap: ["music"]) {
|
events (tags_Overlap: ["music"]) {
|
||||||
|
@ -61,13 +55,11 @@ def test_array_field_overlap_one(Query):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
|
||||||
def test_array_field_overlap_empty_list(Query):
|
def test_array_field_overlap_empty_list(schema):
|
||||||
"""
|
"""
|
||||||
Test overlap filter on a array field of string.
|
Test overlap filter on a array field of string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
events (tags_Overlap: []) {
|
events (tags_Overlap: []) {
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
|
import operator
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.db.models import Q
|
||||||
from django_filters import FilterSet
|
from django_filters import FilterSet
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
@ -44,6 +48,10 @@ def schema():
|
||||||
only_first = TypedFilter(
|
only_first = TypedFilter(
|
||||||
input_type=graphene.Boolean, method="only_first_filter"
|
input_type=graphene.Boolean, method="only_first_filter"
|
||||||
)
|
)
|
||||||
|
headline_search = ListFilter(
|
||||||
|
method="headline_search_filter",
|
||||||
|
input_type=graphene.List(graphene.String),
|
||||||
|
)
|
||||||
|
|
||||||
def first_n_filter(self, queryset, _name, value):
|
def first_n_filter(self, queryset, _name, value):
|
||||||
return queryset[:value]
|
return queryset[:value]
|
||||||
|
@ -54,6 +62,13 @@ def schema():
|
||||||
else:
|
else:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def headline_search_filter(self, queryset, _name, value):
|
||||||
|
if not value:
|
||||||
|
return queryset.none()
|
||||||
|
return queryset.filter(
|
||||||
|
reduce(operator.or_, [Q(headline__icontains=v) for v in value])
|
||||||
|
)
|
||||||
|
|
||||||
class ArticleType(DjangoObjectType):
|
class ArticleType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Article
|
model = Article
|
||||||
|
@ -87,6 +102,7 @@ def test_typed_filter_schema(schema):
|
||||||
"lang_InStr": "[String]",
|
"lang_InStr": "[String]",
|
||||||
"firstN": "Int",
|
"firstN": "Int",
|
||||||
"onlyFirst": "Boolean",
|
"onlyFirst": "Boolean",
|
||||||
|
"headlineSearch": "[String]",
|
||||||
}
|
}
|
||||||
|
|
||||||
all_articles_filters = (
|
all_articles_filters = (
|
||||||
|
@ -104,24 +120,7 @@ def test_typed_filters_work(schema):
|
||||||
Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es")
|
Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es")
|
||||||
Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es")
|
Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es")
|
||||||
Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en")
|
Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en")
|
||||||
|
Article.objects.create(headline="AB", reporter=reporter, editor=reporter, lang="es")
|
||||||
query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }"
|
|
||||||
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data["articles"]["edges"] == [
|
|
||||||
{"node": {"headline": "A"}},
|
|
||||||
{"node": {"headline": "B"}},
|
|
||||||
]
|
|
||||||
|
|
||||||
query = 'query { articles (lang_InStr: ["es"]) { edges { node { headline } } } }'
|
|
||||||
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data["articles"]["edges"] == [
|
|
||||||
{"node": {"headline": "A"}},
|
|
||||||
{"node": {"headline": "B"}},
|
|
||||||
]
|
|
||||||
|
|
||||||
query = 'query { articles (lang_Contains: "n") { edges { node { headline } } } }'
|
query = 'query { articles (lang_Contains: "n") { edges { node { headline } } } }'
|
||||||
|
|
||||||
|
@ -137,7 +136,7 @@ def test_typed_filters_work(schema):
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data["articles"]["edges"] == [
|
assert result.data["articles"]["edges"] == [
|
||||||
{"node": {"headline": "A"}},
|
{"node": {"headline": "A"}},
|
||||||
{"node": {"headline": "B"}},
|
{"node": {"headline": "AB"}},
|
||||||
]
|
]
|
||||||
|
|
||||||
query = "query { articles (onlyFirst: true) { edges { node { headline } } } }"
|
query = "query { articles (onlyFirst: true) { edges { node { headline } } } }"
|
||||||
|
@ -147,3 +146,86 @@ def test_typed_filters_work(schema):
|
||||||
assert result.data["articles"]["edges"] == [
|
assert result.data["articles"]["edges"] == [
|
||||||
{"node": {"headline": "A"}},
|
{"node": {"headline": "A"}},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_filters_work(schema):
|
||||||
|
reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="")
|
||||||
|
Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es")
|
||||||
|
Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es")
|
||||||
|
Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en")
|
||||||
|
Article.objects.create(headline="AB", reporter=reporter, editor=reporter, lang="es")
|
||||||
|
|
||||||
|
query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }"
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["articles"]["edges"] == [
|
||||||
|
{"node": {"headline": "A"}},
|
||||||
|
{"node": {"headline": "AB"}},
|
||||||
|
{"node": {"headline": "B"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = 'query { articles (lang_InStr: ["es"]) { edges { node { headline } } } }'
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["articles"]["edges"] == [
|
||||||
|
{"node": {"headline": "A"}},
|
||||||
|
{"node": {"headline": "AB"}},
|
||||||
|
{"node": {"headline": "B"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = "query { articles (lang_InStr: []) { edges { node { headline } } } }"
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["articles"]["edges"] == []
|
||||||
|
|
||||||
|
query = "query { articles (lang_InStr: null) { edges { node { headline } } } }"
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["articles"]["edges"] == [
|
||||||
|
{"node": {"headline": "A"}},
|
||||||
|
{"node": {"headline": "AB"}},
|
||||||
|
{"node": {"headline": "B"}},
|
||||||
|
{"node": {"headline": "C"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = 'query { articles (headlineSearch: ["a", "B"]) { edges { node { headline } } } }'
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["articles"]["edges"] == [
|
||||||
|
{"node": {"headline": "A"}},
|
||||||
|
{"node": {"headline": "AB"}},
|
||||||
|
{"node": {"headline": "B"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = "query { articles (headlineSearch: []) { edges { node { headline } } } }"
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["articles"]["edges"] == []
|
||||||
|
|
||||||
|
query = "query { articles (headlineSearch: null) { edges { node { headline } } } }"
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["articles"]["edges"] == [
|
||||||
|
{"node": {"headline": "A"}},
|
||||||
|
{"node": {"headline": "AB"}},
|
||||||
|
{"node": {"headline": "B"}},
|
||||||
|
{"node": {"headline": "C"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
query = 'query { articles (headlineSearch: [""]) { edges { node { headline } } } }'
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["articles"]["edges"] == [
|
||||||
|
{"node": {"headline": "A"}},
|
||||||
|
{"node": {"headline": "AB"}},
|
||||||
|
{"node": {"headline": "B"}},
|
||||||
|
{"node": {"headline": "C"}},
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user