Skip to content

Commit

Permalink
Allowed customising default lookup expression. (#1129)
Browse files Browse the repository at this point in the history
  • Loading branch information
carltongibson authored Mar 4, 2020
1 parent 6251bb5 commit fbb67b6
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 29 deletions.
2 changes: 2 additions & 0 deletions django_filters/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
DEFAULTS = {
'DISABLE_HELP_TEXT': False,

'DEFAULT_LOOKUP_EXPR': 'exact',

# empty/null choices
'EMPTY_CHOICE_LABEL': '---------',
'NULL_CHOICE_LABEL': None,
Expand Down
12 changes: 4 additions & 8 deletions django_filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ class Filter(object):
creation_counter = 0
field_class = forms.Field

def __init__(self, field_name=None, lookup_expr='exact', *, label=None,
def __init__(self, field_name=None, lookup_expr=None, *, label=None,
method=None, distinct=False, exclude=False, **kwargs):
if lookup_expr is None:
lookup_expr = settings.DEFAULT_LOOKUP_EXPR
self.field_name = field_name
self.lookup_expr = lookup_expr
self.label = label
Expand All @@ -81,12 +83,6 @@ def __init__(self, field_name=None, lookup_expr='exact', *, label=None,
self.creation_counter = Filter.creation_counter
Filter.creation_counter += 1

# TODO: remove assertion in 2.1
assert not isinstance(self.lookup_expr, (type(None), list)), \
"The `lookup_expr` argument no longer accepts `None` or a list of " \
"expressions. Use the `LookupChoiceFilter` instead. See: " \
"https://django-filter.readthedocs.io/en/master/guide/migration.html"

def get_method(self, qs):
"""Return filter method based on whether we're excluding
or simply filtering.
Expand Down Expand Up @@ -254,7 +250,7 @@ def filter(self, qs, value):

def get_filter_predicate(self, v):
name = self.field_name
if name and self.lookup_expr != 'exact':
if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR:
name = LOOKUP_SEP.join([name, self.lookup_expr])
try:
return {name: getattr(v, self.field.to_field_name)}
Expand Down
12 changes: 7 additions & 5 deletions django_filters/filterset.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ def get_fields(cls):
# Remove excluded fields
exclude = exclude or []
if not isinstance(fields, dict):
fields = [(f, ['exact']) for f in fields if f not in exclude]
fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude]
else:
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]

Expand All @@ -310,9 +310,9 @@ def get_filter_name(cls, field_name, lookup_expr):
filter_name = LOOKUP_SEP.join([field_name, lookup_expr])

# This also works with transformed exact lookups, such as 'date__exact'
_exact = LOOKUP_SEP + 'exact'
if filter_name.endswith(_exact):
filter_name = filter_name[:-len(_exact)]
_default_expr = LOOKUP_SEP + settings.DEFAULT_LOOKUP_EXPR
if filter_name.endswith(_default_expr):
filter_name = filter_name[:-len(_default_expr)]

return filter_name

Expand Down Expand Up @@ -366,7 +366,9 @@ def get_filters(cls):
return filters

@classmethod
def filter_for_field(cls, field, field_name, lookup_expr='exact'):
def filter_for_field(cls, field, field_name, lookup_expr=None):
if lookup_expr is None:
lookup_expr = settings.DEFAULT_LOOKUP_EXPR
field, lookup_type = resolve_field(field, lookup_expr)

default = {
Expand Down
3 changes: 2 additions & 1 deletion docs/guide/usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ The above would generate 'price__lt', 'price__gt', 'release_date', and

The filter lookup type 'exact' is an implicit default and therefore never
added to a filter name. In the above example, the release date's exact
filter is 'release_date', not 'release_date__exact'.
filter is 'release_date', not 'release_date__exact'. This can be overridden
by the FILTERS_DEFAULT_LOOKUP_EXPR setting.

Items in the ``fields`` sequence in the ``Meta`` class may include
"relationship paths" using Django's ``__`` syntax to filter on fields on a
Expand Down
8 changes: 8 additions & 0 deletions docs/ref/settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ default values. All settings are prefixed with ``FILTERS_``, although this
is a bit verbose it helps to make it easy to identify these settings.


FILTERS_DEFAULT_LOOKUP_EXPR
---------------------------

Default: ``'exact'``

Set the default lookup expression to be generated, when none is defined.


FILTERS_EMPTY_CHOICE_LABEL
--------------------------

Expand Down
3 changes: 3 additions & 0 deletions tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ def test_verbose_lookups(self):
self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict)
self.assertIn('exact', settings.VERBOSE_LOOKUPS)

def test_default_lookup_expr(self):
self.assertEqual(settings.DEFAULT_LOOKUP_EXPR, 'exact')

def test_disable_help_text(self):
self.assertFalse(settings.DISABLE_HELP_TEXT)

Expand Down
13 changes: 0 additions & 13 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,6 @@ def test_field_with_single_lookup_expr(self):
field = f.field
self.assertIsInstance(field, forms.Field)

def test_field_with_lookup_types_removal(self):
msg = (
"The `lookup_expr` argument no longer accepts `None` or a list of "
"expressions. Use the `LookupChoiceFilter` instead. See: "
"https://django-filter.readthedocs.io/en/master/guide/migration.html"
)

with self.assertRaisesMessage(AssertionError, msg):
Filter(lookup_expr=[])

with self.assertRaisesMessage(AssertionError, msg):
Filter(lookup_expr=None)

def test_field_params(self):
with mock.patch.object(Filter, 'field_class',
spec=['__call__']) as mocked:
Expand Down
55 changes: 53 additions & 2 deletions tests/test_filterset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import unittest

from django.db import models
from django.test import TestCase
from django.test import TestCase, override_settings

from django_filters.exceptions import FieldLookupError
from django_filters.filters import (
Expand Down Expand Up @@ -198,6 +198,13 @@ def test_transformed_lookup_expr(self):
self.assertIsInstance(result, NumberFilter)
self.assertEqual(result.field_name, 'date')

@override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains')
def test_modified_default_lookup(self):
f = User._meta.get_field('username')
result = FilterSet.filter_for_field(f, 'username')
self.assertIsInstance(result, CharFilter)
self.assertEqual(result.lookup_expr, 'icontains')

@unittest.skip('todo')
def test_filter_overrides(self):
pass
Expand Down Expand Up @@ -330,6 +337,13 @@ class F(FilterSet):
self.assertEqual(len(F.base_filters), 1)
self.assertListEqual(list(F.base_filters), ['username'])

@override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains')
def test_declaring_filter_other_default_lookup(self):
class F(FilterSet):
username = CharFilter()

self.assertEqual(F.base_filters['username'].lookup_expr, 'icontains')

def test_model_derived(self):
class F(FilterSet):
class Meta:
Expand All @@ -341,6 +355,16 @@ class Meta:
self.assertListEqual(list(F.base_filters),
['title', 'price', 'average_rating'])

@override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains')
def test_model_derived_other_default_lookup(self):
class F(FilterSet):
class Meta:
model = Book
fields = '__all__'

for filter_ in F.base_filters.values():
self.assertEqual(filter_.lookup_expr, 'icontains')

def test_model_no_fields_or_exclude(self):
with self.assertRaises(AssertionError) as excinfo:
class F(FilterSet):
Expand Down Expand Up @@ -401,7 +425,6 @@ class Meta:

def test_meta_fields_dictionary_derived(self):
class F(FilterSet):

class Meta:
model = Book
fields = {'price': ['exact', 'gte', 'lte'], }
Expand All @@ -412,6 +435,20 @@ class Meta:
expected_list = ['price', 'price__gte', 'price__lte', ]
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))

@override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='lte')
def test_meta_fields_dictionary_derived_other_default_lookup(self):
class F(FilterSet):

class Meta:
model = Book
fields = {'price': ['exact', 'gte', 'lte'], }

self.assertEqual(len(F.declared_filters), 0)
self.assertEqual(len(F.base_filters), 3)

expected_list = ['price__exact', 'price__gte', 'price', ]
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))

def test_meta_fields_containing_autofield(self):
class F(FilterSet):
username = CharFilter()
Expand Down Expand Up @@ -634,6 +671,20 @@ class Grandchild(Child):
self.assertEqual(len(Child.base_filters), 1)
self.assertEqual(len(Grandchild.base_filters), 1)

@override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='lt')
def test_transforms_other_default_lookup(self):
class F(FilterSet):
class Meta:
model = Article
fields = {
'published': ['lt', 'year__lt'],
}

self.assertEqual(len(F.base_filters), 2)

expected_list = ['published', 'published__year']
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))

def test_declared_filter_multiple_inheritance(self):
class A(FilterSet):
f = CharFilter()
Expand Down

0 comments on commit fbb67b6

Please sign in to comment.