Skip to content

Commit

Permalink
Merge pull request carltongibson#100 from noirbizarre/exclude
Browse files Browse the repository at this point in the history
Allow exclusion filter
  • Loading branch information
apollo13 committed Nov 23, 2013
2 parents 79e9de4 + bdf9308 commit 012833b
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 15 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Marc Fargas
Vladimir Sidorenko
Tom Christie
Remco Wendt
Axel Haustant
12 changes: 8 additions & 4 deletions django_filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Filter(object):
field_class = forms.Field

def __init__(self, name=None, label=None, widget=None, action=None,
lookup_type='exact', required=False, distinct=False, **kwargs):
lookup_type='exact', required=False, distinct=False, exclude=False, **kwargs):
self.name = name
self.label = label
if action:
Expand All @@ -40,13 +40,15 @@ def __init__(self, name=None, label=None, widget=None, action=None,
self.required = required
self.extra = kwargs
self.distinct = distinct
self.exclude = exclude

self.creation_counter = Filter.creation_counter
Filter.creation_counter += 1

@property
def field(self):
if not hasattr(self, '_field'):
help_text = _('This is an exclusion filter') if self.exclude else ''
if (self.lookup_type is None or
isinstance(self.lookup_type, (list, tuple))):
if self.lookup_type is None:
Expand All @@ -56,10 +58,11 @@ def field(self):
(x, x) for x in LOOKUP_TYPES if x in self.lookup_type]
self._field = LookupTypeField(self.field_class(
required=self.required, widget=self.widget, **self.extra),
lookup, required=self.required, label=self.label)
lookup, required=self.required, label=self.label, help_text=help_text)
else:
self._field = self.field_class(required=self.required,
label=self.label, widget=self.widget, **self.extra)
label=self.label, widget=self.widget,
help_text=help_text, **self.extra)
return self._field

def filter(self, qs, value):
Expand All @@ -70,7 +73,8 @@ def filter(self, qs, value):
lookup = self.lookup_type
if value in ([], (), {}, None, ''):
return qs
qs = qs.filter(**{'%s__%s' % (self.name, lookup): value})
method = qs.exclude if self.exclude else qs.filter
qs = method(**{'%s__%s' % (self.name, lookup): value})
if self.distinct:
qs = qs.distinct()
return qs
Expand Down
Binary file added django_filters/locale/fr/LC_MESSAGES/django.mo
Binary file not shown.
47 changes: 47 additions & 0 deletions django_filters/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Django Filter translation.
# Copyright (C) 2013
# This file is distributed under the same license as the django_filter package.
# Axel Haustant <[email protected]>, 2013.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-05 19:24+0200\n"
"PO-Revision-Date: 2013-07-05 19:24+0200\n"
"Last-Translator: Axel Haustant <[email protected]>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
"Language: French\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

#: filters.py:51
msgid "This is an exclusion filter"
msgstr "Ceci est un filtre d'exclusion"

#: filters.py:158
msgid "Any date"
msgstr "Toutes les dates"

#: filters.py:159
msgid "Today"
msgstr "Aujourd'hui"

#: filters.py:164
msgid "Past 7 days"
msgstr "7 derniers jours"

#: filters.py:168
msgid "This month"
msgstr "Ce mois-ci"

#: filters.py:172
msgid "This year"
msgstr "Cette année"

#: widgets.py:63
msgid "All"
msgstr "Tous"
9 changes: 8 additions & 1 deletion docs/ref/filters.txt
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,18 @@ ORM. If a ``list`` or ``tuple`` is provided, then the user can select from thos
options.

``distinct``
~~~~~~~~~~~~~~~
~~~~~~~~~~~~

A boolean value that specifies whether the Filter will use distinct on the
queryset. This option can be used to eliminate duplicate results when using filters that span related models. Defaults to ``False``.

``exclude``
~~~~~~~~~~~

A boolean value that specifies whether the Filter should use ``filter`` or ``exclude`` on the queryset.
Defaults to ``False``.


``**kwargs``
~~~~~~~~~~~~

Expand Down
30 changes: 26 additions & 4 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class FilterTests(TestCase):
def test_creation(self):
f = Filter()
self.assertEqual(f.lookup_type, 'exact')
self.assertEqual(f.exclude, False)

def test_creation_order(self):
f = Filter()
Expand All @@ -42,6 +43,13 @@ def test_default_field(self):
f = Filter()
field = f.field
self.assertIsInstance(field, forms.Field)
self.assertEqual(field.help_text, '')

def test_field_with_exclusion(self):
f = Filter(exclude=True)
field = f.field
self.assertIsInstance(field, forms.Field)
self.assertEqual(field.help_text, 'This is an exclusion filter')

def test_field_with_single_lookup_type(self):
f = Filter(lookup_type='iexact')
Expand All @@ -55,6 +63,12 @@ def test_field_with_none_lookup_type(self):
choice_field = field.fields[1]
self.assertEqual(len(choice_field.choices), len(LOOKUP_TYPES))

def test_field_with_lookup_type_and_exlusion(self):
f = Filter(lookup_type=None, exclude=True)
field = f.field
self.assertIsInstance(field, LookupTypeField)
self.assertEqual(field.help_text, 'This is an exclusion filter')

def test_field_with_list_lookup_type(self):
f = Filter(lookup_type=('istartswith', 'iendswith'))
field = f.field
Expand All @@ -69,23 +83,24 @@ def test_field_params(self):
widget='somewidget')
f.field
mocked.assert_called_once_with(required=False,
label='somelabel', widget='somewidget')
label='somelabel', widget='somewidget', help_text=mock.ANY)

def test_field_extra_params(self):
with mock.patch.object(Filter, 'field_class',
spec=['__call__']) as mocked:
f = Filter(someattr='someattr')
f.field
mocked.assert_called_once_with(required=mock.ANY,
label=mock.ANY, widget=mock.ANY, someattr='someattr')
label=mock.ANY, widget=mock.ANY, help_text=mock.ANY,
someattr='someattr')

def test_field_with_required_filter(self):
with mock.patch.object(Filter, 'field_class',
spec=['__call__']) as mocked:
f = Filter(required=True)
f.field
mocked.assert_called_once_with(required=True,
label=mock.ANY, widget=mock.ANY)
label=mock.ANY, widget=mock.ANY, help_text=mock.ANY)

def test_filtering(self):
qs = mock.Mock(spec=['filter'])
Expand All @@ -94,6 +109,13 @@ def test_filtering(self):
qs.filter.assert_called_once_with(None__exact='value')
self.assertNotEqual(qs, result)

def test_filtering_exclude(self):
qs = mock.Mock(spec=['filter', 'exclude'])
f = Filter(exclude=True)
result = f.filter(qs, 'value')
qs.exclude.assert_called_once_with(None__exact='value')
self.assertNotEqual(qs, result)

def test_filtering_uses_name(self):
qs = mock.Mock(spec=['filter'])
f = Filter(name='somefield')
Expand Down Expand Up @@ -443,4 +465,4 @@ def test_default_field_with_assigning_model(self):
f = AllValuesFilter()
f.model = mocked
field = f.field
self.assertIsInstance(field, forms.ChoiceField)
self.assertIsInstance(field, forms.ChoiceField)
23 changes: 17 additions & 6 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Meta:

f = F().form
self.assertIsInstance(f, MyForm)

def test_form_prefix(self):
class F(FilterSet):
class Meta:
Expand All @@ -55,7 +55,7 @@ class Meta:

f = F(prefix='prefix').form
self.assertEqual(f.prefix, 'prefix')

def test_form_fields(self):
class F(FilterSet):
class Meta:
Expand All @@ -68,6 +68,17 @@ class Meta:
self.assertEqual(sorted(f.fields['status'].choices),
sorted(STATUS_CHOICES))

def test_form_fields_exclusion(self):
class F(FilterSet):
title = CharFilter(exclude=True)

class Meta:
model = Book
fields = ('title',)

f = F().form
self.assertEqual(f.fields['title'].help_text, "This is an exclusion filter")

def test_form_fields_using_widget(self):
class F(FilterSet):
status = ChoiceFilter(widget=forms.RadioSelect,
Expand Down Expand Up @@ -108,7 +119,7 @@ class Meta:
f = F().form
self.assertEqual(f.fields['book_title'].label, None)
self.assertEqual(f['book_title'].label, 'Book title')

def test_form_field_with_manual_name_and_label(self):
class F(FilterSet):
f1 = CharFilter(name='title', label="Book title")
Expand Down Expand Up @@ -183,7 +194,7 @@ class Meta:
model = User
fields = ['username', 'status']
order_by = True

f = F().form
self.assertEqual(f.fields['o'].choices,
[('username', 'Account'), ('-username', 'Account (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
Expand All @@ -196,7 +207,7 @@ class Meta:
model = User
fields = ['account', 'status']
order_by = True

f = F().form
self.assertEqual(f.fields['o'].choices,
[('username', 'Account'), ('-username', 'Account (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
Expand Down Expand Up @@ -257,7 +268,7 @@ class Meta:
model = User
fields = ['username', 'status']
order_by = [('status', 'Current status')]

f = F().form
self.assertEqual(
f.fields['o'].choices, [('status', 'Current status')])
Expand Down

0 comments on commit 012833b

Please sign in to comment.