Skip to content

Commit 4dd64e5

Browse files
author
Carlton Gibson
authored
Merge pull request carltongibson#549 from pySilver/feature/typed-multiple-values-filter
Add `TypedMultipleChoiceFilter`
2 parents e6bafd5 + f7481a1 commit 4dd64e5

File tree

4 files changed

+186
-0
lines changed

4 files changed

+186
-0
lines changed

django_filters/filters.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
'TimeFilter',
4949
'TimeRangeFilter',
5050
'TypedChoiceFilter',
51+
'TypedMultipleChoiceFilter',
5152
'UUIDFilter',
5253
]
5354

@@ -301,6 +302,10 @@ def get_filter_predicate(self, v):
301302
return {self.name: v}
302303

303304

305+
class TypedMultipleChoiceFilter(MultipleChoiceFilter):
306+
field_class = forms.TypedMultipleChoiceField
307+
308+
304309
class DateFilter(Filter):
305310
field_class = forms.DateField
306311

docs/ref/filters.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,13 @@ test.
276276
Override `is_noop` if you require a different test for your application.
277277

278278

279+
``TypedMultipleChoiceFilter``
280+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
281+
282+
Like ``MultipleChoiceFilter``, but in addition accepts the ``coerce`` parameter, as
283+
in ``TypedChoiceFilter``.
284+
285+
279286
``DateFilter``
280287
~~~~~~~~~~~~~~
281288

tests/test_filtering.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from django_filters.filters import DurationFilter
2424
from django_filters.filters import MultipleChoiceFilter
2525
from django_filters.filters import ModelChoiceFilter
26+
from django_filters.filters import TypedMultipleChoiceFilter
2627
from django_filters.filters import ModelMultipleChoiceFilter
2728
from django_filters.filters import NumberFilter
2829
from django_filters.filters import OrderingFilter
@@ -251,6 +252,41 @@ class Meta:
251252
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
252253

253254

255+
class TypedMultipleChoiceFilterTests(TestCase):
256+
257+
def test_filtering(self):
258+
User.objects.create(username='alex', status=1)
259+
User.objects.create(username='jacob', status=2)
260+
User.objects.create(username='aaron', status=2)
261+
User.objects.create(username='carl', status=0)
262+
263+
264+
class F(FilterSet):
265+
status = TypedMultipleChoiceFilter(choices=STATUS_CHOICES, coerce=lambda x: x[0:2])
266+
267+
class Meta:
268+
model = User
269+
fields = ['status']
270+
271+
qs = User.objects.all().order_by('username')
272+
f = F(queryset=qs)
273+
self.assertQuerysetEqual(
274+
f.qs, ['aa', 'ja', 'al', 'ca'],
275+
lambda o: o.username[0:2], False)
276+
277+
f = F({'status': ['0']}, queryset=qs)
278+
self.assertQuerysetEqual(
279+
f.qs, ['ca'], lambda o: o.username[0:2])
280+
281+
f = F({'status': ['0', '1']}, queryset=qs)
282+
self.assertQuerysetEqual(
283+
f.qs, ['al', 'ca'], lambda o: o.username[0:2])
284+
285+
f = F({'status': ['0', '1', '2']}, queryset=qs)
286+
self.assertQuerysetEqual(
287+
f.qs, ['aa', 'al', 'ca', 'ja'], lambda o: o.username[0:2])
288+
289+
254290
class DateFilterTests(TestCase):
255291

256292
def test_filtering(self):

tests/test_filters.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
BooleanFilter,
2828
ChoiceFilter,
2929
MultipleChoiceFilter,
30+
TypedMultipleChoiceFilter,
3031
DateFilter,
3132
DateTimeFilter,
3233
TimeFilter,
@@ -523,6 +524,143 @@ def test_filter_conjoined_true(self):
523524
expected_pks, item[1], item[0]))
524525

525526

527+
class TypedMultipleChoiceFilterTests(TestCase):
528+
529+
def test_default_field(self):
530+
f = TypedMultipleChoiceFilter()
531+
field = f.field
532+
self.assertIsInstance(field, forms.TypedMultipleChoiceField)
533+
534+
def test_filtering_requires_name(self):
535+
qs = mock.Mock(spec=['filter'])
536+
f = TypedMultipleChoiceFilter()
537+
with self.assertRaises(TypeError):
538+
f.filter(qs, ['value'])
539+
540+
def test_conjoined_default_value(self):
541+
f = TypedMultipleChoiceFilter()
542+
self.assertFalse(f.conjoined)
543+
544+
def test_conjoined_true(self):
545+
f = TypedMultipleChoiceFilter(conjoined=True)
546+
self.assertTrue(f.conjoined)
547+
548+
def test_filtering(self):
549+
qs = mock.Mock(spec=['filter'])
550+
f = TypedMultipleChoiceFilter(name='somefield')
551+
with mock.patch('django_filters.filters.Q') as mockQclass:
552+
mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock()
553+
mockQclass.side_effect = [mockQ1, mockQ2]
554+
555+
f.filter(qs, ['value'])
556+
557+
self.assertEqual(mockQclass.call_args_list,
558+
[mock.call(), mock.call(somefield='value')])
559+
mockQ1.__ior__.assert_called_once_with(mockQ2)
560+
qs.filter.assert_called_once_with(mockQ1.__ior__.return_value)
561+
qs.filter.return_value.distinct.assert_called_once_with()
562+
563+
def test_filtering_exclude(self):
564+
qs = mock.Mock(spec=['exclude'])
565+
f = TypedMultipleChoiceFilter(name='somefield', exclude=True)
566+
with mock.patch('django_filters.filters.Q') as mockQclass:
567+
mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock()
568+
mockQclass.side_effect = [mockQ1, mockQ2]
569+
570+
f.filter(qs, ['value'])
571+
572+
self.assertEqual(mockQclass.call_args_list,
573+
[mock.call(), mock.call(somefield='value')])
574+
mockQ1.__ior__.assert_called_once_with(mockQ2)
575+
qs.exclude.assert_called_once_with(mockQ1.__ior__.return_value)
576+
qs.exclude.return_value.distinct.assert_called_once_with()
577+
578+
def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices(self):
579+
qs = mock.Mock(spec=[])
580+
f = TypedMultipleChoiceFilter(name='somefield', required=True)
581+
f.always_filter = False
582+
result = f.filter(qs, [])
583+
self.assertEqual(len(f.field.choices), 0)
584+
self.assertEqual(qs, result)
585+
586+
f.field.choices = ['some', 'values', 'here']
587+
result = f.filter(qs, ['some', 'values', 'here'])
588+
self.assertEqual(qs, result)
589+
590+
result = f.filter(qs, ['other', 'values', 'there'])
591+
self.assertEqual(qs, result)
592+
593+
def test_filtering_skipped_with_empty_list_value_and_some_choices(self):
594+
qs = mock.Mock(spec=[])
595+
f = TypedMultipleChoiceFilter(name='somefield')
596+
f.field.choices = ['some', 'values', 'here']
597+
result = f.filter(qs, [])
598+
self.assertEqual(qs, result)
599+
600+
def test_filter_conjoined_true(self):
601+
"""Tests that a filter with `conjoined=True` returns objects that
602+
have all the values included in `value`. For example filter
603+
users that have all of this books.
604+
605+
"""
606+
book_kwargs = {'price': 1, 'average_rating': 1}
607+
books = []
608+
books.append(Book.objects.create(**book_kwargs))
609+
books.append(Book.objects.create(**book_kwargs))
610+
books.append(Book.objects.create(**book_kwargs))
611+
books.append(Book.objects.create(**book_kwargs))
612+
books.append(Book.objects.create(**book_kwargs))
613+
books.append(Book.objects.create(**book_kwargs))
614+
615+
user1 = User.objects.create()
616+
user2 = User.objects.create()
617+
user3 = User.objects.create()
618+
user4 = User.objects.create()
619+
user5 = User.objects.create()
620+
621+
user1.favorite_books.add(books[0], books[1])
622+
user2.favorite_books.add(books[0], books[1], books[2])
623+
user3.favorite_books.add(books[1], books[2])
624+
user4.favorite_books.add(books[2], books[3])
625+
user5.favorite_books.add(books[4], books[5])
626+
627+
filter_list = (
628+
((books[0].pk, books[0].pk), # values
629+
[1, 2]), # list of user.pk that have `value` books
630+
((books[1].pk, books[1].pk),
631+
[1, 2, 3]),
632+
((books[2].pk, books[2].pk),
633+
[2, 3, 4]),
634+
((books[3].pk, books[3].pk),
635+
[4, ]),
636+
((books[4].pk, books[4].pk),
637+
[5, ]),
638+
((books[0].pk, books[1].pk),
639+
[1, 2]),
640+
((books[0].pk, books[2].pk),
641+
[2, ]),
642+
((books[1].pk, books[2].pk),
643+
[2, 3]),
644+
((books[2].pk, books[3].pk),
645+
[4, ]),
646+
((books[4].pk, books[5].pk),
647+
[5, ]),
648+
((books[3].pk, books[4].pk),
649+
[]),
650+
)
651+
users = User.objects.all()
652+
653+
for item in filter_list:
654+
f = TypedMultipleChoiceFilter(name='favorite_books__pk', conjoined=True)
655+
queryset = f.filter(users, item[0])
656+
expected_pks = [c[0] for c in queryset.values_list('pk')]
657+
self.assertListEqual(
658+
expected_pks,
659+
item[1],
660+
'Lists Differ: {0} != {1} for case {2}'.format(
661+
expected_pks, item[1], item[0]))
662+
663+
526664
class DateFilterTests(TestCase):
527665

528666
def test_default_field(self):

0 commit comments

Comments
 (0)