From d3c80b9d34a063a1fe50c9fe2cd708bbb232848c Mon Sep 17 00:00:00 2001 From: Diogo Machado Date: Mon, 16 Mar 2015 21:54:20 +0000 Subject: [PATCH] group filter fields with meta option 'together' --- AUTHORS | 3 ++- django_filters/filterset.py | 20 ++++++++++++++++++ docs/usage.txt | 18 ++++++++++++++++ setup.py | 2 +- tests/test_filterset.py | 42 +++++++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index b16752a..6ef5337 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,4 +18,5 @@ Vladimir Sidorenko Tom Christie Remco Wendt Axel Haustant -Brad Erickson \ No newline at end of file +Brad Erickson +Diogo Laginha \ No newline at end of file diff --git a/django_filters/filterset.py b/django_filters/filterset.py index 64285be..60d020b 100644 --- a/django_filters/filterset.py +++ b/django_filters/filterset.py @@ -141,6 +141,8 @@ class FilterSetOptions(object): self.order_by = getattr(options, 'order_by', False) self.form = getattr(options, 'form', forms.Form) + + self.together = getattr(options, 'together', None) class FilterSetMetaclass(type): @@ -352,6 +354,22 @@ class BaseFilterSet(object): @property def form(self): + + def full_clean(form): + super(form.__class__, form).full_clean() + message = 'Following fields must be together: %s' + together = self._meta.together + cleaned_data = form.cleaned_data + if isinstance(together[0], (list, tuple)): + for each in together: + count = len([i for i in each if cleaned_data.get(i)]) + if 0 < count < len(each): + return form.add_error(None, message % ','.join(each)) + else: + count = len([i for i in together if cleaned_data.get(i)]) + if 0 < count < len(together): + return form.add_error(None, message % ','.join(together)) + if not hasattr(self, '_form'): fields = OrderedDict([ (name, filter_.field) @@ -359,6 +377,8 @@ class BaseFilterSet(object): fields[self.order_by_field] = self.ordering_field Form = type(str('%sForm' % self.__class__.__name__), (self._meta.form,), fields) + if self._meta.together: + Form.full_clean = full_clean if self.is_bound: self._form = Form(self.data, prefix=self.form_prefix) else: diff --git a/docs/usage.txt b/docs/usage.txt index 3a35fff..5b37df8 100644 --- a/docs/usage.txt +++ b/docs/usage.txt @@ -199,6 +199,24 @@ The inner ``Meta`` class also takes an optional ``form`` argument. This is a form class from which ``FilterSet.form`` will subclass. This works similar to the ``form`` option on a ``ModelAdmin.`` +Group fields with ``together`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The inner ``Meta`` class also takes an optional ``together`` argument. This +is a list of lists, each containing field names. For convenience can be a +single list/tuple when dealing with a single set of fields. Fields within a +field set must either be all or none present in the request for +``FilterSet.form`` to be valid. + + import django_filters + + class ProductFilter(django_filters.FilterSet): + class Meta: + model = Product + fields = ['price', 'release_date', 'rating'] + together = ['rating', 'price'] + + Non-Meta options ---------------- diff --git a/setup.py b/setup.py index 1e55b33..e120cc2 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ if sys.argv[-1] == 'publish': setup( name='django-filter', - version='0.9.2', + version='0.9.3', description=('Django-filter is a reusable Django application for allowing' ' users to filter querysets dynamically.'), long_description=readme, diff --git a/tests/test_filterset.py b/tests/test_filterset.py index e0256cd..8a9ca1c 100644 --- a/tests/test_filterset.py +++ b/tests/test_filterset.py @@ -628,3 +628,45 @@ class FilterSetOrderingTests(TestCase): f = F({'o': 'status'}, queryset=self.qs) self.assertQuerysetEqual( f.qs, ['carl', 'alex', 'aaron', 'jacob'], lambda o: o.username) + + + +class FilterSetTogetherTests(TestCase): + + def setUp(self): + self.alex = User.objects.create(username='alex', status=1) + self.jacob = User.objects.create(username='jacob', status=2) + self.qs = User.objects.all().order_by('id') + + def test_fields_set(self): + class F(FilterSet): + class Meta: + model = User + fields = ['username', 'status', 'is_active', 'first_name'] + together = [ + ('username', 'status'), + ('first_name', 'is_active'), + ] + + f = F({}, queryset=self.qs) + self.assertEqual(f.qs.count(), 2) + f = F({'username': 'alex'}, queryset=self.qs) + self.assertEqual(f.qs.count(), 0) + f = F({'username': 'alex', 'status': 1}, queryset=self.qs) + self.assertEqual(f.qs.count(), 1) + self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk) + + def test_single_fields_set(self): + class F(FilterSet): + class Meta: + model = User + fields = ['username', 'status'] + together = ['username', 'status'] + + f = F({}, queryset=self.qs) + self.assertEqual(f.qs.count(), 2) + f = F({'username': 'alex'}, queryset=self.qs) + self.assertEqual(f.qs.count(), 0) + f = F({'username': 'alex', 'status': 1}, queryset=self.qs) + self.assertEqual(f.qs.count(), 1) + self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)