Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SuffixedMultiWidget #681

Merged
merged 4 commits into from
Aug 31, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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