From 758dfd4750693b4ae5f3a0badbea0d7beaf4be13 Mon Sep 17 00:00:00 2001 From: Antoni Martyniuk Date: Sat, 5 Nov 2016 01:59:55 +0100 Subject: [PATCH 1/3] provides support for query array notation --- django_filters/widgets.py | 27 +++++++++++++++ tests/test_widgets.py | 73 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/django_filters/widgets.py b/django_filters/widgets.py index 7127306..b4c930c 100644 --- a/django_filters/widgets.py +++ b/django_filters/widgets.py @@ -11,6 +11,7 @@ except: from django import forms from django.db.models.fields import BLANK_CHOICE_DASH from django.forms.widgets import flatatt +from django.utils.datastructures import MultiValueDict from django.utils.encoding import force_text from django.utils.safestring import mark_safe from django.utils.six import string_types @@ -171,3 +172,29 @@ class BaseCSVWidget(forms.Widget): class CSVWidget(BaseCSVWidget, forms.TextInput): pass + + +class QueryArrayWidget(BaseCSVWidget, forms.TextInput): + """ + Enables request query array notation that might be consumed by MultipleChoiceFilter + + 1. Values can be provided as csv string: ?foo=bar,baz + 2. Values can be provided as query array: ?foo[]=bar&foo[]=baz + """ + + def value_from_datadict(self, data, files, name): + if not isinstance(data, MultiValueDict): + data = MultiValueDict(data) + + request_data = data.getlist(name, data.getlist('%s[]' % name)) + if isinstance(request_data, string_types): + request_data = [request_data] + + if not request_data: + return [] + + extracted_data = set() + for item in request_data: + extracted_data.update([x.strip() for x in item.rstrip(',').split(',') if x]) + + return list(extracted_data) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 2856cb1..8648257 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.forms import TextInput, Select -from django_filters.widgets import BooleanWidget +from django_filters.widgets import BooleanWidget, QueryArrayWidget from django_filters.widgets import BaseCSVWidget from django_filters.widgets import CSVWidget from django_filters.widgets import RangeWidget @@ -279,3 +279,74 @@ class CSVSelectTests(TestCase): self.assertHTMLEqual(w.render('price', [1, 2]), """ """) + + +class QueryArrayWidgetTests(TestCase): + + def test_widget_value_from_datadict(self): + w = QueryArrayWidget() + + # Values can be provided as csv string: ?foo=bar,baz + data = {'price': None} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, []) + + data = {'price': '1'} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, ['1']) + + data = {'price': '1,2'} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, ['1', '2']) + + data = {'price': '1,,2'} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, ['1', '2']) + + data = {'price': '1,'} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, ['1']) + + data = {'price': ','} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, []) + + data = {'price': ''} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, []) + + result = w.value_from_datadict({}, {}, 'price') + self.assertEqual(result, []) + + # Values can be provided as query array: ?foo[]=bar&foo[]=baz + + data = {'price[]': None} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, []) + + data = {'price[]': ['1']} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, ['1']) + + data = {'price[]': ['1', '2']} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, ['1', '2']) + + data = {'price[]': ['1', '', '2']} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, ['1', '2']) + + data = {'price[]': ['1', '']} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, ['1']) + + data = {'price[]': ['', '']} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, []) + + data = {'price[]': []} + result = w.value_from_datadict(data, {}, 'price') + self.assertEqual(result, []) + + result = w.value_from_datadict({}, {}, 'price') + self.assertEqual(result, []) From 016aa81ab46a01c3f3b963c12b4bd613b6163aa0 Mon Sep 17 00:00:00 2001 From: Antoni Martyniuk Date: Sat, 5 Nov 2016 03:16:56 +0100 Subject: [PATCH 2/3] simplify input processing, make sure csv path won't be executed for native array input values --- django_filters/widgets.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/django_filters/widgets.py b/django_filters/widgets.py index b4c930c..1b6b230 100644 --- a/django_filters/widgets.py +++ b/django_filters/widgets.py @@ -178,23 +178,32 @@ class QueryArrayWidget(BaseCSVWidget, forms.TextInput): """ Enables request query array notation that might be consumed by MultipleChoiceFilter - 1. Values can be provided as csv string: ?foo=bar,baz + 1. Values can be provided as csv string: ?foo=bar,baz 2. Values can be provided as query array: ?foo[]=bar&foo[]=baz + 3. Values can be provided as query array: ?foo=bar&foo=baz + + Note: Duplicate and empty values are skipped from results """ def value_from_datadict(self, data, files, name): if not isinstance(data, MultiValueDict): data = MultiValueDict(data) - request_data = data.getlist(name, data.getlist('%s[]' % name)) - if isinstance(request_data, string_types): - request_data = [request_data] + values_list = data.getlist(name, data.getlist('%s[]' % name)) or [] - if not request_data: - return [] + if isinstance(values_list, string_types): + values_list = [values_list] - extracted_data = set() - for item in request_data: - extracted_data.update([x.strip() for x in item.rstrip(',').split(',') if x]) + # apparently its an array, so no need to process it's values as csv + # ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2] + # ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2] + if len(values_list) > 1: + ret = [x for x in values_list if x] + elif len(values_list) == 1: + # treat first element as csv string + # ?foo=1,2 -> data.getlist(foo) -> foo = ['1,2'] + ret = [x.strip() for x in values_list[0].rstrip(',').split(',') if x] + else: + ret = [] - return list(extracted_data) + return list(set(ret)) From 703172c29fcf838762427ea331e6d933a083961d Mon Sep 17 00:00:00 2001 From: Antoni Martyniuk Date: Sat, 5 Nov 2016 03:22:02 +0100 Subject: [PATCH 3/3] fix tests --- tests/test_widgets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 8648257..1d9d3e8 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -297,11 +297,11 @@ class QueryArrayWidgetTests(TestCase): data = {'price': '1,2'} result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1', '2']) + self.assertEqual(sorted(result), ['1', '2']) data = {'price': '1,,2'} result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1', '2']) + self.assertEqual(sorted(result), ['1', '2']) data = {'price': '1,'} result = w.value_from_datadict(data, {}, 'price') @@ -330,11 +330,11 @@ class QueryArrayWidgetTests(TestCase): data = {'price[]': ['1', '2']} result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1', '2']) + self.assertEqual(sorted(result), ['1', '2']) data = {'price[]': ['1', '', '2']} result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1', '2']) + self.assertEqual(sorted(result), ['1', '2']) data = {'price[]': ['1', '']} result = w.value_from_datadict(data, {}, 'price')