Repository: zostera/django-bootstrap3

Issue:
Inline radio buttons and inline checkboxes not working in Django 4
Visible in example app (`make example`) by switching Django dependency of example env in `pyproject.toml`.

Django < 4 (correct)
<img width="1095" alt="Scherm­afbeelding 2023-06-21 om 16 03 41" src="https://github.com/zostera/django-bootstrap3/assets/82315/d589cff3-4827-445c-a58a-d597283e9ee5">

Django >= 4 (wrong)
<img width="1101" alt="Scherm­afbeelding 2023-06-21 om 16 00 40" src="https://github.com/zostera/django-bootstrap3/assets/82315/92a00ad3-8810-45c0-9cfe-a1ccebcec9e8">



Discussion:

Resolving patch:
diff --git a/src/bootstrap3/renderers.py b/src/bootstrap3/renderers.py
--- a/src/bootstrap3/renderers.py
+++ b/src/bootstrap3/renderers.py
@@ -1,3 +1,5 @@
+import re
+
 from django.forms import (
     BaseForm,
     BaseFormSet,
@@ -31,7 +33,7 @@
     render_label,
 )
 from .text import text_value
-from .utils import add_css_class, render_template_file
+from .utils import IS_PRE_DJANGO4, add_css_class, render_template_file
 
 try:
     # If Django is set up without a database, importing this widget gives RuntimeError
@@ -311,15 +313,17 @@ def add_widget_attrs(self):
 
     def list_to_class(self, html, klass):
         classes = add_css_class(klass, self.get_size_class())
-        mapping = [
-            ("<ul", "<div"),
-            ("</ul>", "</div>"),
-            ("<li", f'<div class="{classes}"'),
-            ("</li>", "</div>"),
-        ]
-        for k, v in mapping:
-            html = html.replace(k, v)
-        return html
+        if IS_PRE_DJANGO4:
+            return self._list_to_class_pre_django4(html, classes)
+        return re.sub("<div>\s*<label", f'<div class="{classes}"><label', html)
+
+    def _list_to_class_pre_django4(self, html, classes):
+        return (
+            html.replace("<ul", "<div")
+            .replace("</ul>", "</div>")
+            .replace("<li", f'<div class="{classes}"')
+            .replace("</li>", "</div>")
+        )
 
     def put_inside_label(self, html):
         content = f"{html} {self.label}"
diff --git a/src/bootstrap3/utils.py b/src/bootstrap3/utils.py
--- a/src/bootstrap3/utils.py
+++ b/src/bootstrap3/utils.py
@@ -1,6 +1,7 @@
 import re
 from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
 
+from django import get_version
 from django.forms.utils import flatatt
 from django.template import Variable, VariableDoesNotExist
 from django.template.base import FilterExpression, TemplateSyntaxError, kwarg_re
@@ -13,6 +14,10 @@
 
 from .text import text_value
 
+# TODO: Remove after support for Django 3.2 ends
+IS_PRE_DJANGO4 = get_version() < "4"
+
+
 # RegEx for quoted string
 QUOTED_STRING = re.compile(r'^["\'](?P<noquotes>.+)["\']$')
 


Test patch:
diff --git a/tests/app/forms.py b/tests/app/forms.py
--- a/tests/app/forms.py
+++ b/tests/app/forms.py
@@ -13,6 +13,10 @@
 )
 
 
+class RadioSetTestForm(forms.Form):
+    radio = forms.ChoiceField(widget=forms.RadioSelect, choices=RADIO_CHOICES)
+
+
 class SmallTestForm(forms.Form):
     sender = forms.EmailField(label="Sender © unicode", help_text='E.g., "me@example.com"')
     subject = forms.CharField(
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -6,8 +6,9 @@
 
 from bootstrap3.exceptions import BootstrapError
 from bootstrap3.text import text_concat, text_value
-from bootstrap3.utils import add_css_class, render_tag, url_to_attrs_dict
+from bootstrap3.utils import IS_PRE_DJANGO4, add_css_class, render_tag, url_to_attrs_dict
 from tests.app.forms import (
+    RadioSetTestForm,
     SmallTestForm,
     TestForm,
     get_title_from_html,
@@ -521,3 +522,32 @@ def test_show_addons_number(self):
 
     def test_show_addons_url(self):
         self.assertFieldHasAddons("url")
+
+
+class InlineLayoutTestCase(TestCase):
+    def test_radioset_inline(self):
+        form = RadioSetTestForm()
+        res = render_template_with_form("{% bootstrap_form form layout='inline' %}", {"form": form})
+        expected = """
+            <div class="form-group bootstrap3-req">
+            <label class="sr-only">Radio</label>
+            <div id="id_radio">
+            <div class="radio">
+            <label for="id_radio_0">
+            <input type="radio" name="radio" value="1" class="" title="" required id="id_radio_0">Radio 1
+            </label>
+            </div>
+            <div class="radio">
+            <label for="id_radio_1">
+            <input type="radio" name="radio" value="2" class="" title="" required id="id_radio_1">Radio 2
+            </label>
+            </div>
+            </div>
+            </div>
+            """
+        if IS_PRE_DJANGO4:
+            expected = expected.replace(
+                '<label class="sr-only">Radio</label>',
+                '<label class="sr-only" for="id_radio_0">Radio</label>',
+            )
+        self.assertHTMLEqual(res, expected)