Repository: zostera/django-bootstrap4

Issue:
Different div class for checkboxes
There is a different div class in the Bootstrap's docs for checkboxes.

https://getbootstrap.com/docs/4.1/components/forms/#overview

Now it's "form-group form-check" but the library generates only the "form-group" div.


Discussion:
For Bootstrap 4.0 (https://getbootstrap.com/docs/4.0/components/forms/#overview) it is only the "form-check" div class.
Alternatively you can use the [custom checkbox](https://getbootstrap.com/docs/4.1/components/forms/#checkboxes) like this:

```html
<div class="custom-control custom-checkbox">
  <input type="checkbox" class="custom-control-input" id="id_test_checkbox">
  <label class="custom-control-label" for="id_test_checkbox">Check this custom checkbox</label>
</div>
```

Resolving patch:
diff --git a/bootstrap4/renderers.py b/bootstrap4/renderers.py
--- a/bootstrap4/renderers.py
+++ b/bootstrap4/renderers.py
@@ -331,6 +331,8 @@ def add_class_attrs(self, widget=None):
             classes = add_css_class(classes, "form-control", prepend=True)
             # For these widget types, add the size class here
             classes = add_css_class(classes, self.get_size_class())
+        elif isinstance(widget, CheckboxInput):
+            classes = add_css_class(classes, "form-check-input", prepend=True)
 
         if self.field.errors:
             if self.error_css_class:
@@ -392,13 +394,11 @@ def list_to_class(self, html, klass):
         return html
 
     def add_checkbox_label(self, html):
-        return "{field}{label}".format(
-            field=html,
-            label=render_label(
-                content=mark_safe(self.field.label),
-                label_for=self.field.id_for_label,
-                label_title=escape(strip_tags(self.field_help)),
-            ),
+        return html + render_label(
+            content=self.field.label,
+            label_for=self.field.id_for_label,
+            label_title=escape(strip_tags(self.field_help)),
+            label_class="form-check-label",
         )
 
     def fix_date_select_input(self, html):
@@ -445,7 +445,7 @@ def wrap_widget(self, html):
         if isinstance(self.widget, CheckboxInput):
             # Wrap checkboxes
             # Note checkboxes do not get size classes, see #318
-            html = '<div class="checkbox">{content}</div>'.format(content=html)
+            html = '<div class="form-check">{content}</div>'.format(content=html)
         return html
 
     def make_input_group_addon(self, inner_class, outer_class, content):
@@ -486,7 +486,7 @@ def make_input_group(self, html):
             )
         return html
 
-    def append_to_field(self, html):
+    def append_help_and_error(self, html):
         field_help = self.field_help or None
         field_errors = self.field_errors
         if field_help or field_errors:
@@ -503,6 +503,16 @@ def append_to_field(self, html):
             html += help_html
         return html
 
+    def append_to_field(self, html):
+        if isinstance(self.widget, CheckboxInput):
+            return html
+        return self.append_help_and_error(html)
+
+    def append_to_checkbox_field(self, html):
+        if not isinstance(self.widget, CheckboxInput):
+            return html
+        return self.append_help_and_error(html)
+
     def get_field_class(self):
         field_class = self.field_class
         if not field_class and self.layout == "horizontal":
@@ -581,6 +591,7 @@ def _render(self):
         self.restore_widget_attrs()
         # Start post render
         html = self.post_widget_render(html)
+        html = self.append_to_checkbox_field(html)
         html = self.wrap_widget(html)
         html = self.make_input_group(html)
         html = self.append_to_field(html)


Test patch:
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -2,6 +2,8 @@
 from __future__ import unicode_literals
 
 import re
+
+from bs4 import BeautifulSoup
 from django import forms
 from django.contrib.admin.widgets import AdminSplitDateTime
 from django.contrib.gis import forms as gisforms
@@ -441,6 +443,62 @@ def test_password(self):
         self.assertIn('type="password"', res)
         self.assertIn('placeholder="Password"', res)
 
+    def test_checkbox(self):
+        """Test Checkbox rendering, because it is special."""
+        def _select_one_element(html, selector, err_msg):
+            lst = html.select(selector)
+            self.assertEqual(len(lst), 1, err_msg)
+            return lst[0]
+
+        res = render_form_field("cc_myself")
+        # strip out newlines and spaces around newlines
+        res = "".join(line.strip() for line in res.split('\n'))
+        res = BeautifulSoup(res, 'html.parser')
+        form_group = _select_one_element(
+            res,
+            ".form-group",
+            "Checkbox should be rendered inside a .form-group.",
+        )
+        form_check = _select_one_element(
+            form_group,
+            ".form-check",
+            "There should be a .form-check inside .form-group",
+        )
+        checkbox = _select_one_element(
+            form_check,
+            "input",
+            "The checkbox should be inside the .form-check",
+        )
+        self.assertIn(
+            "form-check-input",
+            checkbox["class"],
+            "The checkbox should have the class 'form-check-input'.",
+        )
+        label = checkbox.nextSibling
+        self.assertIsNotNone(label, "The label should be rendered after the checkbox.")
+        self.assertEqual(label.name, "label", "After the checkbox there should be a label.")
+        self.assertEqual(
+            label["for"],
+            checkbox["id"],
+            "The for attribute of the label should be the id of the checkbox.",
+        )
+        help_text = label.nextSibling
+        self.assertIsNotNone(help_text, "The help text should be rendered after the label.")
+        self.assertEqual(
+            help_text.name,
+            "small", "The help text should be rendered as <small> tag.",
+        )
+        self.assertIn(
+            "form-text",
+            help_text["class"],
+            "The help text should have the class 'form-text'.",
+        )
+        self.assertIn(
+            "text-muted",
+            help_text["class"],
+            "The help text should have the class 'text-muted'.",
+        )
+
     def test_required_field(self):
         required_css_class = "bootstrap4-req"
         required_field = render_form_field("subject")
@@ -718,13 +776,13 @@ def test_for_formset(self):
 
 class PaginatorTest(TestCase):
     def test_url_replace_param(self):
-        self.assertEquals(
+        self.assertEqual(
             url_replace_param("/foo/bar?baz=foo", "baz", "yohoo"), "/foo/bar?baz=yohoo"
         )
-        self.assertEquals(
+        self.assertEqual(
             url_replace_param("/foo/bar?baz=foo", "baz", None), "/foo/bar"
         )
-        self.assertEquals(
+        self.assertEqual(
             url_replace_param("/foo/bar#id", "baz", "foo"), "/foo/bar?baz=foo#id"
         )