Skip to content

Commit

Permalink
Add SuffixedMultiWidget (#681)
Browse files Browse the repository at this point in the history
* Add SuffixedMultiWidget

* Add SuffixedMultiWidget support for django < 1.11

* Add SuffixedMultiWidget tests

* Add SuffixedMultiWidget docs
  • Loading branch information
Ryan P Kilby authored and carltongibson committed Oct 19, 2017
1 parent bc29377 commit 58ff255
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 1 deletion.
60 changes: 60 additions & 0 deletions django_filters/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from collections import Iterable
from itertools import chain
from re import search, sub

import django
from django import forms
Expand Down Expand Up @@ -80,6 +81,65 @@ def option_string(self):
return '<li><a%(attrs)s href="?%(query_string)s">%(label)s</a></li>'


class SuffixedMultiWidget(forms.MultiWidget):
"""
A MultiWidget that allows users to provide custom suffixes instead of indexes.
- Suffixes must be unique.
- There must be the same number of suffixes as fields.
"""
suffixes = []

def __init__(self, *args, **kwargs):
super(SuffixedMultiWidget, self).__init__(*args, **kwargs)

assert len(self.widgets) == len(self.suffixes)
assert len(self.suffixes) == len(set(self.suffixes))

def suffixed(self, name, suffix):
return '_'.join([name, suffix]) if suffix else name

def get_context(self, name, value, attrs):
context = super(SuffixedMultiWidget, self).get_context(name, value, attrs)
for subcontext, suffix in zip(context['widget']['subwidgets'], self.suffixes):
subcontext['name'] = self.suffixed(name, suffix)

return context

def value_from_datadict(self, data, files, name):
return [
widget.value_from_datadict(data, files, self.suffixed(name, suffix))
for widget, suffix in zip(self.widgets, self.suffixes)
]

def value_omitted_from_data(self, data, files, name):
return all(
widget.value_omitted_from_data(data, files, self.suffixed(name, suffix))
for widget, suffix in zip(self.widgets, self.suffixes)
)

# Django < 1.11 compat
def format_output(self, rendered_widgets):
rendered_widgets = [
self.replace_name(output, i)
for i, output in enumerate(rendered_widgets)
]
return '\n'.join(rendered_widgets)

def replace_name(self, output, index):
result = search(r'name="(?P<name>.*)_%d"' % index, output)
name = result.group('name')
name = self.suffixed(name, self.suffixes[index])
name = 'name="%s"' % name

return sub(r'name=".*_%d"' % index, name, output)

def decompress(self, value):
if value is None:
return [None, None]
return value


class RangeWidget(forms.MultiWidget):
template_name = 'django_filters/widgets/multiwidget.html'

Expand Down
25 changes: 25 additions & 0 deletions docs/ref/widgets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,28 @@ a ``RangeField``:
.. code-block:: python

date_range = DateFromToRangeFilter(widget=RangeWidget(attrs={'placeholder': 'YYYY/MM/DD'}))


``SuffixedMultiWidget``
~~~~~~~~~~~~~~~~~~~~~~~

Extends Django's builtin ``MultiWidget`` to append custom suffixes instead of
indices. For example, take a range widget that accepts minimum and maximum
bounds. By default, the resulting query params would look like the following:

.. code-block:: http

GET /products?price_0=10&price_1=25 HTTP/1.1

By using ``SuffixedMultiWidget`` instead, you can provide human-friendly suffixes.

.. code-block:: python

class RangeWidget(SuffixedMultiWidget):
suffixes = ['min', 'max']

The query names are now a little more ergonomic.

.. code-block:: http

GET /products?price_min=10&price_max=25 HTTP/1.1
67 changes: 66 additions & 1 deletion tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
LinkWidget,
LookupTypeWidget,
QueryArrayWidget,
RangeWidget
RangeWidget,
SuffixedMultiWidget
)


Expand Down Expand Up @@ -123,6 +124,70 @@ def test_widget_value_from_datadict(self):
self.assertEqual(result, 'test-val1')


class SuffixedMultiWidgetTests(TestCase):
def test_assertions(self):
# number of widgets must match suffixes
with self.assertRaises(AssertionError):
SuffixedMultiWidget(widgets=[BooleanWidget])

# suffixes must be unique
class W(SuffixedMultiWidget):
suffixes = ['a', 'a']

with self.assertRaises(AssertionError):
W(widgets=[BooleanWidget, BooleanWidget])

# should succeed
class W(SuffixedMultiWidget):
suffixes = ['a', 'b']
W(widgets=[BooleanWidget, BooleanWidget])

def test_render(self):
class W(SuffixedMultiWidget):
suffixes = ['min', 'max']

w = W(widgets=[TextInput, TextInput])
self.assertHTMLEqual(w.render('price', ''), """
<input name="price_min" type="text" />
<input name="price_max" type="text" />
""")

# blank suffix
class W(SuffixedMultiWidget):
suffixes = [None, 'lookup']

w = W(widgets=[TextInput, TextInput])
self.assertHTMLEqual(w.render('price', ''), """
<input name="price" type="text" />
<input name="price_lookup" type="text" />
""")

def test_value_from_datadict(self):
class W(SuffixedMultiWidget):
suffixes = ['min', 'max']

w = W(widgets=[TextInput, TextInput])
result = w.value_from_datadict({
'price_min': '1',
'price_max': '2',
}, {}, 'price')
self.assertEqual(result, ['1', '2'])

result = w.value_from_datadict({}, {}, 'price')
self.assertEqual(result, [None, None])

# blank suffix
class W(SuffixedMultiWidget):
suffixes = ['', 'lookup']

w = W(widgets=[TextInput, TextInput])
result = w.value_from_datadict({
'price': '1',
'price_lookup': 'lt',
}, {}, 'price')
self.assertEqual(result, ['1', 'lt'])


class RangeWidgetTests(TestCase):

def test_widget(self):
Expand Down

0 comments on commit 58ff255

Please sign in to comment.