From 5399ccc0f4257676981ef7937ea84be36f7058a6 Mon Sep 17 00:00:00 2001
From: Karen Tracey <kmtracey@gmail.com>
Date: Sat, 7 Nov 2015 10:46:50 -0500
Subject: [PATCH] Fixed #494 -- Added ability to specify classes on admin
 inline fieldsets.

This includes the ability to collapse inlines by specifying a class named
'collapse'.
---
 django/contrib/admin/helpers.py               |  1 +
 django/contrib/admin/options.py               |  3 +++
 .../templates/admin/edit_inline/stacked.html  |  2 ++
 .../templates/admin/edit_inline/tabular.html  |  2 +-
 docs/ref/contrib/admin/index.txt              | 10 ++++++++++
 docs/releases/1.10.txt                        |  5 +++++
 tests/admin_inlines/admin.py                  |  2 ++
 tests/admin_inlines/tests.py                  | 19 +++++++++++++++++++
 8 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 1217bd78df..9cb49473fd 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -235,6 +235,7 @@ class InlineAdminFormSet(object):
         if prepopulated_fields is None:
             prepopulated_fields = {}
         self.prepopulated_fields = prepopulated_fields
+        self.classes = ' '.join(inline.classes) if inline.classes else ''
 
     def __iter__(self):
         for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 4fb5f3ddc8..42880aba81 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1800,6 +1800,7 @@ class InlineModelAdmin(BaseModelAdmin):
     can_delete = True
     show_change_link = False
     checks_class = InlineModelAdminChecks
+    classes = None
 
     def __init__(self, parent_model, admin_site):
         self.admin_site = admin_site
@@ -1819,6 +1820,8 @@ class InlineModelAdmin(BaseModelAdmin):
               'inlines%s.js' % extra]
         if self.filter_vertical or self.filter_horizontal:
             js.extend(['SelectBox.js', 'SelectFilter2.js'])
+        if self.classes and 'collapse' in self.classes:
+            js.append('collapse%s.js' % extra)
         return forms.Media(js=['admin/js/%s' % url for url in js])
 
     def get_extra(self, request, obj=None, **kwargs):
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
index 9025a17afb..65af259a21 100644
--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -3,6 +3,7 @@
      id="{{ inline_admin_formset.formset.prefix }}-group"
      data-inline-type="stacked"
      data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
+<fieldset class="module {{ inline_admin_formset.classes }}">
   <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
 {{ inline_admin_formset.formset.management_form }}
 {{ inline_admin_formset.formset.non_form_errors }}
@@ -20,4 +21,5 @@
   {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
   {{ inline_admin_form.fk_field.field }}
 </div>{% endfor %}
+</fieldset>
 </div>
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index bcf3e6bd1c..f04faadf2f 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -4,7 +4,7 @@
      data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
   <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
 {{ inline_admin_formset.formset.management_form }}
-<fieldset class="module">
+<fieldset class="module {{ inline_admin_formset.classes }}">
    <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
    {{ inline_admin_formset.formset.non_form_errors }}
    <table>
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 4173c063dc..a36859c012 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -2058,6 +2058,16 @@ The ``InlineModelAdmin`` class adds:
     parent model fails to validate, it may be left in an inconsistent state as
     described in the warning in :ref:`validation-on-modelform`.
 
+.. attribute:: InlineModelAdmin.classes
+
+    .. versionadded:: 1.10
+
+    A list or tuple containing extra CSS classes to apply to the fieldset that
+    is rendered for the inlines. Defaults to ``None``. As with classes
+    configured in :attr:`~ModelAdmin.fieldsets`, inlines with a ``collapse``
+    class will be initially collapsed and their header will have a small "show"
+    link.
+
 .. attribute:: InlineModelAdmin.extra
 
     This controls the number of extra forms the formset will display in
diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt
index 7be22b12f4..09c437f941 100644
--- a/docs/releases/1.10.txt
+++ b/docs/releases/1.10.txt
@@ -42,6 +42,11 @@ Minor features
 * All inline JavaScript is removed so you can enable the
   ``Content-Security-Policy`` HTTP header if you wish.
 
+* The new :attr:`InlineModelAdmin.classes
+  <django.contrib.admin.InlineModelAdmin.classes>` attribute allows specifying
+  classes on inline fieldsets. Inlines with a ``collapse`` class will be
+  initially collapsed and their header will have a small "show" link.
+
 :mod:`django.contrib.admindocs`
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py
index 65d74a4c81..b7fd6424d6 100644
--- a/tests/admin_inlines/admin.py
+++ b/tests/admin_inlines/admin.py
@@ -20,10 +20,12 @@ class BookInline(admin.TabularInline):
 
 class NonAutoPKBookTabularInline(admin.TabularInline):
     model = NonAutoPKBook
+    classes = ('collapse',)
 
 
 class NonAutoPKBookStackedInline(admin.StackedInline):
     model = NonAutoPKBook
+    classes = ('collapse',)
 
 
 class EditablePKBookTabularInline(admin.TabularInline):
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index b2102480bc..0a50202340 100644
--- a/tests/admin_inlines/tests.py
+++ b/tests/admin_inlines/tests.py
@@ -874,6 +874,25 @@ class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
         self.assertEqual(len(self.selenium.find_elements_by_css_selector(
             "%s.row2" % row_selector)), 1, msg="Expect one row2 styled row")
 
+    def test_collapsed_inlines(self):
+        # Collapsed inlines have SHOW/HIDE links.
+        self.admin_login(username='super', password='secret')
+        self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_author_add'))
+        # One field is in a stacked inline, other in a tabular one.
+        test_fields = ['id_nonautopkbook_set-0-title', 'id_nonautopkbook_set-2-0-title']
+        show_links = self.selenium.find_elements_by_link_text('SHOW')
+        self.assertEqual(len(show_links), 2)
+        for show_index, field_name in enumerate(test_fields, 0):
+            self.assertFalse(self.selenium.find_element_by_id(field_name).is_displayed())
+            show_links[show_index].click()
+            self.assertTrue(self.selenium.find_element_by_id(field_name).is_displayed())
+        hide_links = self.selenium.find_elements_by_link_text('HIDE')
+        self.assertEqual(len(hide_links), 2)
+        for hide_index, field_name in enumerate(test_fields, 0):
+            self.assertTrue(self.selenium.find_element_by_id(field_name).is_displayed())
+            hide_links[hide_index].click()
+            self.assertFalse(self.selenium.find_element_by_id(field_name).is_displayed())
+
 
 class SeleniumChromeTests(SeleniumFirefoxTests):
     webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'