From ebb3e50243448545c7314a1932a9067ddca5960b Mon Sep 17 00:00:00 2001
From: Loic Bistuer <loic.bistuer@sixmedia.com>
Date: Wed, 31 Jul 2013 12:52:11 +0700
Subject: [PATCH] Introduced ModelAdmin.get_fields() and refactored
 get_fieldsets() to use it.

Refs #18681.

This also starts the deprecation of ModelAdmin.declared_fieldsets
---
 django/contrib/admin/options.py  | 60 +++++++++++++++++++++++++-------
 docs/internals/deprecation.txt   |  2 ++
 docs/ref/contrib/admin/index.txt |  8 +++++
 docs/releases/1.7.txt            | 13 +++++++
 tests/modeladmin/tests.py        | 10 ++++++
 5 files changed, 80 insertions(+), 13 deletions(-)

diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index cb50d0e749..b475868598 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -2,6 +2,7 @@ from collections import OrderedDict
 import copy
 import operator
 from functools import partial, reduce, update_wrapper
+import warnings
 
 from django import forms
 from django.conf import settings
@@ -238,13 +239,49 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
 
         return db_field.formfield(**kwargs)
 
-    def _declared_fieldsets(self):
+    @property
+    def declared_fieldsets(self):
+        warnings.warn(
+            "ModelAdmin.declared_fieldsets is deprecated and "
+            "will be removed in Django 1.9.",
+            PendingDeprecationWarning, stacklevel=2
+        )
+
         if self.fieldsets:
             return self.fieldsets
         elif self.fields:
             return [(None, {'fields': self.fields})]
         return None
-    declared_fieldsets = property(_declared_fieldsets)
+
+    def get_fields(self, request, obj=None):
+        """
+        Hook for specifying fields.
+        """
+        return self.fields
+
+    def get_fieldsets(self, request, obj=None):
+        """
+        Hook for specifying fieldsets.
+        """
+        # We access the property and check if it triggers a warning.
+        # If it does, then it's ours and we can safely ignore it, but if
+        # it doesn't then it has been overriden so we must warn about the
+        # deprecation.
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            declared_fieldsets = self.declared_fieldsets
+        if len(w) != 1 or not issubclass(w[0].category, PendingDeprecationWarning):
+            warnings.warn(
+                "ModelAdmin.declared_fieldsets is deprecated and "
+                "will be removed in Django 1.9.",
+                PendingDeprecationWarning
+            )
+            if declared_fieldsets:
+                return declared_fieldsets
+
+        if self.fieldsets:
+            return self.fieldsets
+        return [(None, {'fields': self.get_fields(request, obj)})]
 
     def get_ordering(self, request):
         """
@@ -478,13 +515,11 @@ class ModelAdmin(BaseModelAdmin):
             'delete': self.has_delete_permission(request),
         }
 
-    def get_fieldsets(self, request, obj=None):
-        "Hook for specifying fieldsets for the add form."
-        if self.declared_fieldsets:
-            return self.declared_fieldsets
+    def get_fields(self, request, obj=None):
+        if self.fields:
+            return self.fields
         form = self.get_form(request, obj, fields=None)
-        fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj))
-        return [(None, {'fields': fields})]
+        return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
 
     def get_form(self, request, obj=None, **kwargs):
         """
@@ -1657,12 +1692,11 @@ class InlineModelAdmin(BaseModelAdmin):
 
         return inlineformset_factory(self.parent_model, self.model, **defaults)
 
-    def get_fieldsets(self, request, obj=None):
-        if self.declared_fieldsets:
-            return self.declared_fieldsets
+    def get_fields(self, request, obj=None):
+        if self.fields:
+            return self.fields
         form = self.get_formset(request, obj, fields=None).form
-        fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj))
-        return [(None, {'fields': fields})]
+        return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
 
     def get_queryset(self, request):
         queryset = super(InlineModelAdmin, self).get_queryset(request)
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 9b0dbf8ffa..fe8b48a5a9 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -426,6 +426,8 @@ these changes.
 * ``django.utils.datastructures.SortedDict`` will be removed. Use
   :class:`collections.OrderedDict` from the Python standard library instead.
 
+* ``ModelAdmin.declared_fieldsets`` will be removed.
+
 2.0
 ---
 
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 40ec514331..04dc52a3a1 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -1218,6 +1218,14 @@ templates used by the :class:`ModelAdmin` views:
     changelist that will be linked to the change view, as described in the
     :attr:`ModelAdmin.list_display_links` section.
 
+.. method:: ModelAdmin.get_fields(self, request, obj=None)
+
+    .. versionadded:: 1.7
+
+    The ``get_fields`` method is given the ``HttpRequest`` and the ``obj``
+    being edited (or ``None`` on an add form) and is expected to return a list
+    of fields, as described above in the :attr:`ModelAdmin.fields` section.
+
 .. method:: ModelAdmin.get_fieldsets(self, request, obj=None)
 
     The ``get_fieldsets`` method is given the ``HttpRequest`` and the ``obj``
diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt
index 91b932aff1..970f362949 100644
--- a/docs/releases/1.7.txt
+++ b/docs/releases/1.7.txt
@@ -113,6 +113,11 @@ Minor features
 * The admin's search fields can now be customized per-request thanks to the new
   :meth:`django.contrib.admin.ModelAdmin.get_search_fields` method.
 
+* The :meth:`ModelAdmin.get_fields()
+  <django.contrib.admin.ModelAdmin.get_fields>` method may be overridden to
+  customize the value of :attr:`ModelAdmin.fields
+  <django.contrib.admin.ModelAdmin.fields>`.
+
 Backwards incompatible changes in 1.7
 =====================================
 
@@ -182,3 +187,11 @@ than simply ``myapp/models.py``, Django would look for :ref:`initial SQL data
 <initial-sql>` in ``myapp/models/sql/``. This bug has been fixed so that Django
 will search ``myapp/sql/`` as documented. The old location will continue to
 work until Django 1.9.
+
+``declared_fieldsets`` attribute on ``ModelAdmin.``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``ModelAdmin.declared_fieldsets`` was deprecated. Despite being a private API,
+it will go through a regular deprecation path. This attribute was mostly used
+by methods that bypassed ``ModelAdmin.get_fieldsets()`` but this was considered
+a bug and has been addressed.
diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py
index 616b0889b9..424588cf49 100644
--- a/tests/modeladmin/tests.py
+++ b/tests/modeladmin/tests.py
@@ -51,6 +51,12 @@ class ModelAdminTests(TestCase):
         self.assertEqual(list(ma.get_form(request).base_fields),
             ['name', 'bio', 'sign_date'])
 
+        self.assertEqual(list(ma.get_fields(request)),
+            ['name', 'bio', 'sign_date'])
+
+        self.assertEqual(list(ma.get_fields(request, self.band)),
+            ['name', 'bio', 'sign_date'])
+
     def test_default_fieldsets(self):
         # fieldsets_add and fieldsets_change should return a special data structure that
         # is used in the templates. They should generate the "right thing" whether we
@@ -97,6 +103,10 @@ class ModelAdminTests(TestCase):
 
         ma = BandAdmin(Band, self.site)
 
+        self.assertEqual(list(ma.get_fields(request)), ['name'])
+
+        self.assertEqual(list(ma.get_fields(request, self.band)), ['name'])
+
         self.assertEqual(ma.get_fieldsets(request),
             [(None, {'fields': ['name']})])