From b1c359a562ec19f0007e64866f3e3b1afeeaf222 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 15 Jan 2018 02:42:48 -0500 Subject: [PATCH] Make lookup required when value is provided --- django_filters/fields.py | 13 +++++++++++-- tests/test_fields.py | 3 +++ tests/test_filtering.py | 9 +++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/django_filters/fields.py b/django_filters/fields.py index b1126015a..b6903ba16 100644 --- a/django_filters/fields.py +++ b/django_filters/fields.py @@ -92,6 +92,10 @@ def __new__(cls, value, lookup_expr): class LookupChoiceField(forms.MultiValueField): + default_error_messages = { + 'lookup_required': _('Select a lookup.'), + } + def __init__(self, field, lookup_choices, *args, **kwargs): empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL) fields = (field, ChoiceField(choices=lookup_choices, empty_label=empty_label)) @@ -103,8 +107,13 @@ def __init__(self, field, lookup_choices, *args, **kwargs): def compress(self, data_list): if len(data_list) == 2: value, lookup_expr = data_list - if value not in EMPTY_VALUES and lookup_expr not in EMPTY_VALUES: - return Lookup(value=value, lookup_expr=lookup_expr) + if value not in EMPTY_VALUES: + if lookup_expr not in EMPTY_VALUES: + return Lookup(value=value, lookup_expr=lookup_expr) + else: + raise forms.ValidationError( + self.error_messages['lookup_required'], + code='lookup_required') return None diff --git a/tests/test_fields.py b/tests/test_fields.py index 98aeaf9f4..5b251227a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -122,6 +122,9 @@ def test_clean(self): f.clean([]), None) + with self.assertRaisesMessage(forms.ValidationError, 'Select a lookup.'): + f.clean(['12.34', '']) + def test_render_used_html5(self): inner = forms.DecimalField() f = LookupChoiceField(inner, [('gt', 'gt'), ('lt', 'lt')], empty_label=None) diff --git a/tests/test_filtering.py b/tests/test_filtering.py index fa8c760ca..6571c8fdc 100644 --- a/tests/test_filtering.py +++ b/tests/test_filtering.py @@ -1644,10 +1644,12 @@ def test_filtering(self): f = F({'price': '15', 'price_lookup': 'lt'}) self.assertQuerysetEqual(f.qs, ['Ender\'s Game'], lambda o: o.title) f = F({'price': '', 'price_lookup': 'lt'}) + self.assertTrue(f.is_valid()) self.assertQuerysetEqual(f.qs, ['Ender\'s Game', 'Rainbow Six', 'Snowcrash'], lambda o: o.title, ordered=False) f = F({'price': '15'}) + self.assertFalse(f.is_valid()) self.assertQuerysetEqual(f.qs, ['Ender\'s Game', 'Rainbow Six', 'Snowcrash'], lambda o: o.title, ordered=False) @@ -1666,6 +1668,13 @@ def test_lookup_choices_validation(self): 'price': ['Select a valid choice. asdf is not one of the available choices.'], }) + def test_lookup_omitted(self): + f = self.BookFilter({'price': '1'}) + self.assertFalse(f.is_valid()) + self.assertEqual(f.errors, { + 'price': ['Select a lookup.'], + }) + # use naive datetimes, as pytz is required to perform # date lookups when timezones are involved.