diff --git a/.gitignore b/.gitignore
index d1a92b1..197ef8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,3 +54,6 @@ coverage.xml
docs/_build/
.idea
+
+# VSCode settings
+.vscode/
\ No newline at end of file
diff --git a/README.rst b/README.rst
index ef848a1..ac3bd22 100644
--- a/README.rst
+++ b/README.rst
@@ -268,6 +268,22 @@ Output:
+Fields with multiple widgets
+============================
+
+Some fields may render as a `MultiWidget`, composed of multiple subwidgets
+(for example, a `ChoiceField` using `RadioSelect`). You can use the same tags
+and filters, but your template code will need to include a for loop for fields
+like this:
+
+.. code-block:: html+django
+
+ {% load widget_tweaks %}
+
+ {% for widget in form.choice %}
+ {{ widget|add_class:"css_class_1 css_class_2" }}
+ {% endfor %}
+
Mixing render_field and filters
===============================
@@ -419,8 +435,3 @@ Make sure you have `tox `_ installed, then type
tox
from the source checkout.
-
-NOT SUPPORTED
-=============
-
-MultiWidgets: SplitDateTimeWidget, SplitHiddenDateTimeWidget
diff --git a/tests/forms.py b/tests/forms.py
index 7b6a842..e6a5cdb 100644
--- a/tests/forms.py
+++ b/tests/forms.py
@@ -15,6 +15,12 @@ class MyForm(Form):
with_attrs = CharField(widget=TextInput(attrs={"foo": "baz", "egg": "spam"}))
with_cls = CharField(widget=TextInput(attrs={"class": "class0"}))
date = forms.DateField(widget=SelectDateWidget(attrs={"egg": "spam"}))
+ choice = forms.ChoiceField(choices=[(1, "one"), (2, "two")])
+ radio = forms.ChoiceField(
+ label="Radio Input",
+ choices=[("option1", "Option 1"), ("option2", "Option 2")],
+ widget=forms.RadioSelect,
+ )
def render_form(text, form=None, **context_args):
@@ -46,6 +52,31 @@ def render_field(field, template_filter, params, *args, **kwargs):
return render_form(render_field_str, **kwargs)
+def render_choice_field(
+ field, choice_no, template_filter, params, *args, **kwargs
+):
+ """
+ Renders ``field`` of MyForm with choice_no and filter ``template_filter``
+ applied.
+ ``params`` are filter arguments.
+
+ If you want to apply several filters (in a chain),
+ pass extra ``template_filter`` and ``params`` as positional arguments.
+
+ In order to use custom form, pass form instance as ``form``
+ keyword argument.
+ """
+ filters = [(template_filter, params)]
+ filters.extend(zip(args[::2], args[1::2]))
+ filter_strings = ['|%s:"%s"' % (f[0], f[1]) for f in filters]
+ render_field_str = "{{ form.%s.%s%s }}" % (
+ field,
+ choice_no,
+ "".join(filter_strings),
+ )
+ return render_form(render_field_str, **kwargs)
+
+
def render_field_from_tag(field, *attributes):
"""
Renders MyForm's field ``field`` with attributes passed
diff --git a/tests/tests.py b/tests/tests.py
index 80a2921..a00dfa5 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -1,6 +1,6 @@
from unittest import TestCase
-from .forms import render_field, render_field_from_tag, render_form, MyForm
+from .forms import render_field, render_choice_field, render_field_from_tag, render_form, MyForm
def assertIn(value, obj):
@@ -389,3 +389,52 @@ def test_field_arroba_dot(self):
def test_field_double_colon_missing(self):
res = render_form('{{ form.simple|attr:"::class:{active:True}" }}')
assertIn(':class="{active:True}"', res)
+
+
+class SelectFieldTest(TestCase):
+ def test_parent_field(self):
+ res = render_field("choice", "attr", "foo:bar")
+ assertIn("select", res)
+ assertIn('name="choice"', res)
+ assertIn('id="id_choice"', res)
+ assertIn('foo="bar"', res)
+ assertIn('', res)
+ assertIn('', res)
+
+ def test_rendering_id_class(self):
+ res = render_form(
+ '{% render_field form.choice id="id_1" class="c_1" %}'
+ '{% render_field form.choice id="id_2" class="c_2" %}'
+ )
+ self.assertEqual(res.count("id_1"), 1)
+ self.assertEqual(res.count("id_2"), 1)
+ self.assertEqual(res.count("c_1"), 1)
+ self.assertEqual(res.count("c_2"), 1)
+
+
+class RadioFieldTest(TestCase):
+ def test_first_choice(self):
+ res = render_choice_field("radio", 0, "attr", "foo:bar")
+ assertIn('type="radio"', res)
+ assertIn('name="radio"', res)
+ assertIn('value="option1"', res)
+ assertIn('id="id_radio_0"', res)
+ assertIn('foo="bar"', res)
+
+ def test_second_choice(self):
+ res = render_choice_field("radio", 1, "attr", "foo:bar")
+ assertIn('type="radio"', res)
+ assertIn('name="radio"', res)
+ assertIn('value="option2"', res)
+ assertIn('id="id_radio_1"', res)
+ assertIn('foo="bar"', res)
+
+ def test_rendering_id_class(self):
+ res = render_form(
+ '{% render_field form.radio.0 id="id_1" class="c_1" %}'
+ '{% render_field form.radio.1 id="id_2" class="c_2" %}'
+ )
+ self.assertEqual(res.count("id_1"), 1)
+ self.assertEqual(res.count("id_2"), 1)
+ self.assertEqual(res.count("c_1"), 1)
+ self.assertEqual(res.count("c_2"), 1)
diff --git a/widget_tweaks/templatetags/widget_tweaks.py b/widget_tweaks/templatetags/widget_tweaks.py
index 116b3c3..13997e8 100644
--- a/widget_tweaks/templatetags/widget_tweaks.py
+++ b/widget_tweaks/templatetags/widget_tweaks.py
@@ -24,6 +24,20 @@ def _process_field_attributes(field, attr, process):
attribute = params[0].replace("::", ":")
value = params[1] if len(params) == 2 else True
field = copy(field)
+
+ if not hasattr(field, "as_widget"):
+ old_tag = field.tag
+
+ def tag(self, wrap_label=False): # pylint: disable=unused-argument
+ attrs = self.data["attrs"]
+ process(self.parent_widget, attrs, attribute, value)
+ html = old_tag(wrap_label=False)
+ self.tag = old_tag
+ return html
+
+ field.tag = types.MethodType(tag, field)
+ return field
+
# decorate field.as_widget method with updated attributes
old_as_widget = field.as_widget