Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Fix `RadioSelectButtonGroup` not rendering as a button group. The renderer replaced the class attribute of a widget's enclosing element instead of adding to it, discarding `btn-group btn-group-toggle`. Django's own `RadioSelect` and `CheckboxSelectMultiple` set no class there, so their output is unchanged (#894).
- Fix `{% buttons layout="horizontal" %}` emitting Bootstrap 3 markup: the label carried `control-label` instead of `col-form-label`, and the wrapper had no `row` for the `col-*` classes to divide, so the button did not line up with the fields above it (#895).
- **Breaking:** Fix invalid HTML in `RadioSelectButtonGroup`. Every option label carried the widget's own `id`, so a group of N options rendered N+1 identical `id` attributes and no label was associated with its input. Labels now carry `for` pointing at their own input and no `id` (#309).
- Fix `form_check_class` being ignored for single checkboxes; it was only applied to `RadioSelect` and `CheckboxSelectMultiple` (#290).
Expand Down
21 changes: 12 additions & 9 deletions src/bootstrap4/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,20 +324,23 @@ def list_to_class(self, html, klass):
classes = add_css_class(klass, self.get_size_class())
soup = BeautifulSoup(html, features="html.parser")
enclosing_div = soup.find("div")
enclosing_div.attrs["class"] = classes
# Add to the widget's own classes rather than replacing them. Django's RadioSelect and
# CheckboxSelectMultiple templates put no class here, so this is a no-op for them, but a
# custom widget template may depend on its classes, as RadioSelectButtonGroup does on
# btn-group to be a button group at all.
widget_classes = " ".join(enclosing_div.attrs.get("class", []))
enclosing_div.attrs["class"] = add_css_class(widget_classes, classes)
for inner_div in enclosing_div.find_all("div"):
inner_div.attrs["class"] = inner_div.attrs.get("class", []) + [self.form_check_class]
# Apply bootstrap4 classes to labels and inputs.
# A simple 'replace' isn't enough as we don't want to have several 'class' attr definition, which would happen
# if we tried to 'html.replace("input", "input class=...")'
enclosing_div = soup.find("div", {"class": classes})
if enclosing_div:
for label in enclosing_div.find_all("label"):
label.attrs["class"] = label.attrs.get("class", []) + ["form-check-label"]
try:
label.input.attrs["class"] = label.input.attrs.get("class", []) + ["form-check-input"]
except AttributeError:
pass
for label in enclosing_div.find_all("label"):
label.attrs["class"] = label.attrs.get("class", []) + ["form-check-label"]
try:
label.input.attrs["class"] = label.input.attrs.get("class", []) + ["form-check-input"]
except AttributeError:
pass
return str(soup)

def get_checkbox_label_class(self):
Expand Down
16 changes: 16 additions & 0 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,22 @@ def test_checkbox_form_check_class(self):
self.assertIn('<div class="custom-control">', res)
self.assertNotIn('<div class="form-check">', res)

def test_radio_select_button_group_keeps_widget_classes(self):
"""The renderer must not strip the classes a custom widget template sets (#894)."""
res = render_form_field("category5")
soup = BeautifulSoup(res, "html.parser")
enclosing_div = soup.select_one(".form-group > div")
self.assertIn("btn-group", enclosing_div["class"])
self.assertIn("btn-group-toggle", enclosing_div["class"])

def test_radio_select_keeps_default_classes(self):
"""Django's own widgets set no class here, so their output is unchanged (#894)."""
for field in ("category1", "category2"):
with self.subTest(field=field):
soup = BeautifulSoup(render_form_field(field), "html.parser")
enclosing_div = soup.select_one(".form-group > div")
self.assertNotIn("btn-group", enclosing_div["class"])

def test_radio_select_button_group_label_ids(self):
"""Each button group label must point at its own input, with no duplicate ids (#309)."""
res = render_form_field("category5")
Expand Down
Loading