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