Repository: django-crispy-forms/django-crispy-forms

Issue:
First AccordionGroup can never be set to collapsed at initial page load.
When there are no errors, the first `AccordionGroup` in an `Accordion` will always be set to active, even you originally set it as `active=False`. For example, in this case:

``` python
    test_form.helper.layout = Layout(
        Accordion(
            AccordionGroup(
                'one',
                'first_name',
                active=False,
            ),
        )
    )
```

One might expect that this single group be collapsed at initial page render. However, the `active` kwarg, as defined, will be ignored. It is overrided as `True`.

This behavior is unexpected and overly restrictive. There may be reasons why you want to render an Accordion with all internal groups initially closed. Further, the Twitter Bootstrap documentation [see http://getbootstrap.com/2.3.2/javascript.html#collapse] allows this rendered state as a possibility at initial page load.



Discussion:

Resolving patch:
diff --git a/crispy_forms/bootstrap.py b/crispy_forms/bootstrap.py
--- a/crispy_forms/bootstrap.py
+++ b/crispy_forms/bootstrap.py
@@ -180,6 +180,7 @@ def __init__(self, name, *fields, **kwargs):
         super(Container, self).__init__(*fields, **kwargs)
         self.template = kwargs.pop('template', self.template)
         self.name = name
+        self._active_originally_included = "active" in kwargs
         self.active = kwargs.pop("active", False)
         if not self.css_id:
             self.css_id = slugify(self.name)
@@ -205,14 +206,30 @@ class ContainerHolder(Div):
     """
     def first_container_with_errors(self, errors):
         """
-        Returns the first container with errors, otherwise returns the first one
+        Returns the first container with errors, otherwise returns None.
         """
         for tab in self.fields:
             errors_here = any(error in tab for error in errors)
             if errors_here:
                 return tab
+        return None
 
-        return self.fields[0]
+    def open_target_group_for_form(self, form):
+        """
+        Makes sure that the first group that should be open is open.
+        This is either the first group with errors or the first group
+        in the container, unless that first group was originally set to
+        active=False.
+        """
+        target = self.first_container_with_errors(form.errors.keys())
+        if target is None:
+            target = self.fields[0]
+            if not target._active_originally_included:
+                target.active = True
+            return target
+
+        target.active = True
+        return target
 
 
 class Tab(Container):
@@ -249,8 +266,8 @@ def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
         for tab in self.fields:
             tab.active = False
 
-        # The first tab with errors will be active
-        self.first_container_with_errors(form.errors.keys()).active = True
+        # Open the group that should be open.
+        self.open_target_group_for_form(form)
 
         for tab in self.fields:
             content += render_field(
@@ -293,8 +310,8 @@ def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
         if not self.css_id:
             self.css_id = "-".join(["accordion", text_type(randint(1000, 9999))])
 
-        # first group with errors or first groupt will be visible, others will be collapsed
-        self.first_container_with_errors(form.errors.keys()).active = True
+        # Open the group that should be open.
+        self.open_target_group_for_form(form)
 
         for group in self.fields:
             group.data_parent = self.css_id


Test patch:
diff --git a/crispy_forms/tests/test_layout_objects.py b/crispy_forms/tests/test_layout_objects.py
--- a/crispy_forms/tests/test_layout_objects.py
+++ b/crispy_forms/tests/test_layout_objects.py
@@ -207,6 +207,43 @@ def test_accordion_and_accordiongroup(self):
         self.assertEqual(html.count('name="password1"'), 1)
         self.assertEqual(html.count('name="password2"'), 1)
 
+    def test_accordion_active_false_not_rendered(self):
+        test_form = TestForm()
+        test_form.helper = FormHelper()
+        test_form.helper.layout = Layout(
+            Accordion(
+                AccordionGroup(
+                    'one',
+                    'first_name',
+                ),
+                # there is no ``active`` kwarg here.
+            )
+        )
+
+        # The first time, there should be one of them there.
+        html = render_crispy_form(test_form)
+
+        if self.current_template_pack == 'bootstrap':
+            accordion_class = "accordion-body"
+        else:
+            accordion_class = "panel-collapse"
+
+        self.assertEqual(html.count('<div id="one" class="%s collapse in"' % accordion_class), 1)
+
+        test_form.helper.layout = Layout(
+            Accordion(
+                AccordionGroup(
+                    'one',
+                    'first_name',
+                    active=False,  # now ``active`` manually set as False
+                ),
+            )
+        )
+
+        # This time, it shouldn't be there at all.
+        html = render_crispy_form(test_form)
+        self.assertEqual(html.count('<div id="one" class="%s collapse in"' % accordion_class), 0)
+
     def test_alert(self):
         test_form = TestForm()
         test_form.helper = FormHelper()