Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
History
-------

0.3.10 (2018-11-14)
~~~~~~~~~~~~~~~~~~

* Only running ``distinct`` on queryset when one of filters is on one-to-many relation.
This should help with performance.
See `#26 <https://github.com/miki725/django-url-filter/issues/26>`_.

0.3.9 (2018-11-12)
~~~~~~~~~~~~~~~~~~

Expand Down
21 changes: 18 additions & 3 deletions tests/backends/test_django.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,32 @@ def test_filter(self):
qs = mock.Mock()

backend = DjangoFilterBackend(qs)
backend.model = Place
backend.bind([
FilterSpec(['name'], 'exact', 'value', False),
FilterSpec(['address'], 'contains', 'value', True),
])

result = backend.filter()

assert result == qs.filter.return_value.exclude.return_value.distinct.return_value
assert result == qs.filter.return_value.exclude.return_value
qs.filter.assert_called_once_with(name__exact='value')
qs.filter.return_value.exclude.assert_called_once_with(address__contains='value')

def test_filter_to_many(self):
qs = mock.Mock()

backend = DjangoFilterBackend(qs)
backend.model = Place
backend.bind([
FilterSpec(['restaurant', 'waiter', 'name'], 'exact', 'value', False),
])

result = backend.filter()

assert result == qs.filter.return_value.distinct.return_value
qs.filter.assert_called_once_with(restaurant__waiter__name__exact='value')

def test_filter_callable_specs(self):
qs = mock.Mock()

Expand All @@ -93,5 +108,5 @@ def foo(queryset, spec):

result = backend.filter()

assert result == qs.distinct.return_value.filter.return_value
qs.distinct.return_value.filter.assert_called_once_with(spec)
assert result == qs.filter.return_value
qs.filter.assert_called_once_with(spec)
6 changes: 4 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
[tox]
envlist =
{py27,py36,pypy,pypy3}-django{18,11}
{py36,pypy3}-django20
{py27,py36,py37,pypy,pypy3}-django{18,11}
{py36,py37,pypy3}-django{20,latest}

[testenv]
basepython =
py27: python2.7
py36: python3.6
py37: python3.7
pypy: pypy
pypy3: pypy3
setenv =
Expand All @@ -20,6 +21,7 @@ deps =
django18: djangorestframework<3.7
django11: django<2
django20: django<2.1
djangolatest: django
whitelist_externals =
make

Expand Down
2 changes: 1 addition & 1 deletion url_filter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

__author__ = 'Miroslav Shubernetskiy'
__email__ = 'miroslav@miki725.com'
__version__ = '0.3.9'
__version__ = '0.3.10'
16 changes: 15 additions & 1 deletion url_filter/backends/django.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

from django.core.exceptions import FieldDoesNotExist
from django.db.models.constants import LOOKUP_SEP

from ..utils import suppress
from .base import BaseFilterBackend


Expand Down Expand Up @@ -110,4 +112,16 @@ def filter_by_specs(self, queryset):
for lookup, value in exclude.items():
queryset = queryset.exclude(**{lookup: value})

return queryset.distinct()
to_many = self._is_any_to_many()
return queryset.distinct() if to_many else queryset

def _is_any_to_many(self):
return any(self._is_to_many(self.model, i.components) for i in self.regular_specs)

def _is_to_many(self, model, components):
if not components:
return False

with suppress(FieldDoesNotExist):
f = model._meta.get_field(components[0])
return f.one_to_many or f.many_to_many or self._is_to_many(f.related_model, components[1:])
19 changes: 19 additions & 0 deletions url_filter/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import inspect
from contextlib import contextmanager


class FilterSpec(object):
Expand Down Expand Up @@ -242,3 +243,21 @@ def dict_pop(key, d):
"""
d.pop(key, None)
return d


@contextmanager
def suppress(e):
"""
Suppress given exception type

For example::

>>> with suppress(ValueError):
... print('test')
... raise ValueError
test
"""
try:
yield
except e:
pass