Ticket #18388: patch_ticket_18338.4.txt

File patch_ticket_18338.4.txt, 9.6 KB (added by areski, 2 years ago)
Line 
1diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
2index 7373837..487fc4e 100644
3--- a/django/contrib/admin/options.py
4+++ b/django/contrib/admin/options.py
5@@ -13,7 +13,7 @@ from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_o
6 from django.contrib.admin.templatetags.admin_static import static
7 from django.contrib import messages
8 from django.views.decorators.csrf import csrf_protect
9-from django.core.exceptions import PermissionDenied, ValidationError, FieldError
10+from django.core.exceptions import PermissionDenied, ValidationError, FieldError, ImproperlyConfigured
11 from django.core.paginator import Paginator
12 from django.core.urlresolvers import reverse
13 from django.db import models, transaction, router
14@@ -1467,6 +1467,17 @@ class InlineModelAdmin(BaseModelAdmin):
15             js.extend(['SelectBox.js', 'SelectFilter2.js'])
16         return forms.Media(js=[static('admin/js/%s' % url) for url in js])
17 
18+    def get_extra(self, request, obj=None, **kwargs):
19+        """Get the number of extra inline forms needed"""
20+        if not isinstance(self.extra, int):
21+            raise ImproperlyConfigured("'%s.extra' should be a integer."
22+                                       % self.__class__.__name__)
23+        return self.extra
24+
25+    def get_max_num(self, request, obj=None, **kwargs):
26+        """Get the max number of extra inline forms needed"""
27+        return self.max_num
28+
29     def get_formset(self, request, obj=None, **kwargs):
30         """Returns a BaseInlineFormSet class for use in admin add/change views."""
31         if self.declared_fieldsets:
32@@ -1493,8 +1504,8 @@ class InlineModelAdmin(BaseModelAdmin):
33             "fields": fields,
34             "exclude": exclude,
35             "formfield_callback": partial(self.formfield_for_dbfield, request=request),
36-            "extra": self.extra,
37-            "max_num": self.max_num,
38+            "extra": self.get_extra(request, obj, *kwargs),
39+            "max_num": self.get_max_num(request, obj, *kwargs),
40             "can_delete": can_delete,
41         }
42 
43diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
44index 8d65f96..06b5055 100644
45--- a/django/contrib/admin/validation.py
46+++ b/django/contrib/admin/validation.py
47@@ -199,11 +199,6 @@ def validate_inline(cls, parent, parent_model):
48 
49     fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True)
50 
51-    # extra = 3
52-    if not isinstance(cls.extra, int):
53-        raise ImproperlyConfigured("'%s.extra' should be a integer."
54-                % cls.__name__)
55-
56     # max_num = None
57     max_num = getattr(cls, 'max_num', None)
58     if max_num is not None and not isinstance(max_num, int):
59diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
60index 67e498e..35a33d9 100644
61--- a/docs/ref/contrib/admin/index.txt
62+++ b/docs/ref/contrib/admin/index.txt
63@@ -1629,6 +1629,11 @@ The ``InlineModelAdmin`` class adds:
64     The dynamic link will not appear if the number of currently displayed forms
65     exceeds ``max_num``, or if the user does not have JavaScript enabled.
66 
67+    .. versionadded:: 1.6
68+
69+    Also see :meth:`InlineModelAdmin.get_extra` if you need to customize the
70+    behavior for this.
71+
72     .. _ref-contrib-admin-inline-max-num:
73 
74 .. attribute:: InlineModelAdmin.max_num
75@@ -1676,6 +1681,50 @@ The ``InlineModelAdmin`` class adds:
76     Returns a ``BaseInlineFormSet`` class for use in admin add/change views.
77     See the example for :class:`ModelAdmin.get_formsets`.
78 
79+.. method:: InlineModelAdmin.get_extra(self, request, obj=None, **kwargs)
80+
81+    .. versionadded:: 1.6
82+
83+
84+    Returns the number of extra inline forms needed. By default, returns the
85+    :attr:`InlineModelAdmin.extra` attribute.
86+
87+    Here, you can programmatically determine the number of extra inline forms
88+    which should be shown on the interface. This may be based on the object
89+    passed as the keyword argument ``obj``. For example::
90+
91+        class BinaryTreeAdmin(admin.TabularInline):
92+            model = BinaryTree
93+
94+            def get_extra(self, request, obj=None, **kwargs):
95+                extra = 2
96+                if obj:
97+                  return extra - obj.binarytree_set.count()
98+                return extra
99+
100+
101+.. method:: InlineModelAdmin.get_max_num(self, request, obj=None, **kwargs)
102+
103+    .. versionadded:: 1.6
104+
105+
106+    Returns the maximum number of extra inline forms needed. By default, returns the
107+    :attr:`InlineModelAdmin.max_num` attribute.
108+
109+    Here, you can programmatically determine the maximum number of extra inline
110+    forms which should be shown on the interface. This may be based on the object
111+    passed as the keyword argument ``obj``. For example::
112+
113+        class BinaryTreeAdmin(admin.TabularInline):
114+            model = BinaryTree
115+
116+            def get_max_num(self, request, obj=None, **kwargs):
117+                max_num = 10
118+                if obj.parent:
119+                    return max_num - 5
120+                return max_num
121+
122+
123 Working with a model with two or more foreign keys to the same parent model
124 ---------------------------------------------------------------------------
125 
126diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
127index 0eab854..ca34914 100644
128--- a/docs/releases/1.6.txt
129+++ b/docs/releases/1.6.txt
130@@ -184,6 +184,20 @@ Minor features
131 
132 * The jQuery library embedded in the admin has been upgraded to version 1.9.1.
133 
134+* ``InlineModelAdmin`` can override ``get_extra`` method to programmatically
135+  determine the number of extra inline forms.
136+
137+* :class:`~django.contrib.admin.InlineModelAdmin` can override
138+  :meth:`~django.contrib.admin.InlineModelAdmin.get_extra` method
139+  to programmatically determine the number of extra inline forms.
140+
141+* ``InlineModelAdmin`` can override ``get_max_num`` method to programmatically
142+  determine the maximum number of extra inline forms.
143+
144+* :class:`~django.contrib.admin.InlineModelAdmin` can override
145+  :meth:`~django.contrib.admin.InlineModelAdmin.get_max_num` method
146+  to programmatically determine the maximum number of extra inline forms.
147+
148 * Syndication feeds (:mod:`django.contrib.syndication`) can now pass extra
149   context through to feed templates using a new `Feed.get_context_data()`
150   callback.
151diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py
152index 44671d0..2f88248 100644
153--- a/tests/admin_inlines/admin.py
154+++ b/tests/admin_inlines/admin.py
155@@ -129,6 +129,22 @@ class ChildModel1Inline(admin.TabularInline):
156 class ChildModel2Inline(admin.StackedInline):
157     model = ChildModel2
158 
159+# admin for #19425 and #18388
160+class BinaryTreeAdmin(admin.TabularInline):
161+    model = BinaryTree
162+
163+    def get_extra(self, request, obj=None, **kwargs):
164+        extra = 2
165+        if obj:
166+            return extra - obj.binarytree_set.count()
167+        return extra
168+
169+    def get_max_num(self, request, obj=None, **kwargs):
170+        max_num = 3
171+        if obj:
172+            return max_num - obj.binarytree_set.count()
173+        return max_num
174+
175 # admin for #19524
176 class SightingInline(admin.TabularInline):
177     model = Sighting
178@@ -150,4 +166,5 @@ site.register(Author, AuthorAdmin)
179 site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline, ReadOnlyInlineInline])
180 site.register(ProfileCollection, inlines=[ProfileInline])
181 site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline])
182+site.register(BinaryTree, inlines=[BinaryTreeAdmin])
183 site.register(ExtraTerrestrial, inlines=[SightingInline])
184diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py
185index 82c1c3f..d4ba0ab 100644
186--- a/tests/admin_inlines/models.py
187+++ b/tests/admin_inlines/models.py
188@@ -183,6 +183,12 @@ class ChildModel2(models.Model):
189     def get_absolute_url(self):
190         return '/child_model2/'
191 
192+
193+# Models for #19425
194+class BinaryTree(models.Model):
195+    name = models.CharField(max_length=100)
196+    parent = models.ForeignKey('self', null=True, blank=True)
197+
198 # Models for #19524
199 
200 class LifeForm(models.Model):
201diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
202index 714a2f1..1b18181 100644
203--- a/tests/admin_inlines/tests.py
204+++ b/tests/admin_inlines/tests.py
205@@ -12,7 +12,7 @@ from .admin import InnerInline, TitleInline, site
206 from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
207     OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
208     ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2,
209-    Sighting, Title, Novel, Chapter, FootNote)
210+    Sighting, Title, Novel, Chapter, FootNote, BinaryTree)
211 
212 
213 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
214@@ -193,6 +193,20 @@ class TestInline(TestCase):
215         self.assertEqual(response.status_code, 302)
216         self.assertEqual(Sighting.objects.filter(et__name='Martian').count(), 1)
217 
218+    def test_custom_get_extra_form(self):
219+        bt_head = BinaryTree(name="Tree Head")
220+        bt_head.save()
221+        bt_child = BinaryTree(name="First Child", parent=bt_head)
222+        bt_child.save()
223+
224+        # The total number of forms will remain the same in any case
225+        total_forms_hidden = '<input id="id_binarytree_set-TOTAL_FORMS" name="binarytree_set-TOTAL_FORMS" type="hidden" value="2" />'
226+        response = self.client.get('/admin/admin_inlines/binarytree/add/')
227+        self.assertContains(response, total_forms_hidden)
228+
229+        response = self.client.get("/admin/admin_inlines/binarytree/%d/" % bt_head.id)
230+        self.assertContains(response, total_forms_hidden)
231+
232 
233 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
234 class TestInlineMedia(TestCase):
Back to Top