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

Creating support for BoundWidget based of #122 with tests #149

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ coverage.xml
docs/_build/

.idea

# VSCode settings
.vscode/
21 changes: 16 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,22 @@ Output:
<input id="id_name" type="text" name="name" maxlength="100" />
</div>

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
===============================

Expand Down Expand Up @@ -419,8 +435,3 @@ Make sure you have `tox <http://tox.testrun.org/>`_ installed, then type
tox

from the source checkout.

NOT SUPPORTED
=============

MultiWidgets: SplitDateTimeWidget, SplitHiddenDateTimeWidget
31 changes: 31 additions & 0 deletions tests/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
51 changes: 50 additions & 1 deletion tests/tests.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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('<option value="1">one</option>', res)
assertIn('<option value="2">two</option>', 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)
14 changes: 14 additions & 0 deletions widget_tweaks/templatetags/widget_tweaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down