diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 7373837..487fc4e 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -13,7 +13,7 @@ from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_o from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages from django.views.decorators.csrf import csrf_protect -from django.core.exceptions import PermissionDenied, ValidationError, FieldError +from django.core.exceptions import PermissionDenied, ValidationError, FieldError, ImproperlyConfigured from django.core.paginator import Paginator from django.core.urlresolvers import reverse from django.db import models, transaction, router @@ -1467,6 +1467,17 @@ class InlineModelAdmin(BaseModelAdmin): js.extend(['SelectBox.js', 'SelectFilter2.js']) return forms.Media(js=[static('admin/js/%s' % url) for url in js]) + def get_extra(self, request, obj=None, **kwargs): + """Get the number of extra inline forms needed""" + if not isinstance(self.extra, int): + raise ImproperlyConfigured("'%s.extra' should be a integer." + % self.__class__.__name__) + return self.extra + + def get_max_num(self, request, obj=None, **kwargs): + """Get the max number of extra inline forms needed""" + return self.max_num + def get_formset(self, request, obj=None, **kwargs): """Returns a BaseInlineFormSet class for use in admin add/change views.""" if self.declared_fieldsets: @@ -1493,8 +1504,8 @@ class InlineModelAdmin(BaseModelAdmin): "fields": fields, "exclude": exclude, "formfield_callback": partial(self.formfield_for_dbfield, request=request), - "extra": self.extra, - "max_num": self.max_num, + "extra": self.get_extra(request, obj, *kwargs), + "max_num": self.get_max_num(request, obj, *kwargs), "can_delete": can_delete, } diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 8d65f96..06b5055 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -199,11 +199,6 @@ def validate_inline(cls, parent, parent_model): fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) - # extra = 3 - if not isinstance(cls.extra, int): - raise ImproperlyConfigured("'%s.extra' should be a integer." - % cls.__name__) - # max_num = None max_num = getattr(cls, 'max_num', None) if max_num is not None and not isinstance(max_num, int): diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 67e498e..35a33d9 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1629,6 +1629,11 @@ The ``InlineModelAdmin`` class adds: The dynamic link will not appear if the number of currently displayed forms exceeds ``max_num``, or if the user does not have JavaScript enabled. + .. versionadded:: 1.6 + + Also see :meth:`InlineModelAdmin.get_extra` if you need to customize the + behavior for this. + .. _ref-contrib-admin-inline-max-num: .. attribute:: InlineModelAdmin.max_num @@ -1676,6 +1681,50 @@ The ``InlineModelAdmin`` class adds: Returns a ``BaseInlineFormSet`` class for use in admin add/change views. See the example for :class:`ModelAdmin.get_formsets`. +.. method:: InlineModelAdmin.get_extra(self, request, obj=None, **kwargs) + + .. versionadded:: 1.6 + + + Returns the number of extra inline forms needed. By default, returns the + :attr:`InlineModelAdmin.extra` attribute. + + Here, you can programmatically determine the number of extra inline forms + which should be shown on the interface. This may be based on the object + passed as the keyword argument ``obj``. For example:: + + class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_extra(self, request, obj=None, **kwargs): + extra = 2 + if obj: + return extra - obj.binarytree_set.count() + return extra + + +.. method:: InlineModelAdmin.get_max_num(self, request, obj=None, **kwargs) + + .. versionadded:: 1.6 + + + Returns the maximum number of extra inline forms needed. By default, returns the + :attr:`InlineModelAdmin.max_num` attribute. + + Here, you can programmatically determine the maximum number of extra inline + forms which should be shown on the interface. This may be based on the object + passed as the keyword argument ``obj``. For example:: + + class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_max_num(self, request, obj=None, **kwargs): + max_num = 10 + if obj.parent: + return max_num - 5 + return max_num + + Working with a model with two or more foreign keys to the same parent model --------------------------------------------------------------------------- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 0eab854..ca34914 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -184,6 +184,20 @@ Minor features * The jQuery library embedded in the admin has been upgraded to version 1.9.1. +* ``InlineModelAdmin`` can override ``get_extra`` method to programmatically + determine the number of extra inline forms. + +* :class:`~django.contrib.admin.InlineModelAdmin` can override + :meth:`~django.contrib.admin.InlineModelAdmin.get_extra` method + to programmatically determine the number of extra inline forms. + +* ``InlineModelAdmin`` can override ``get_max_num`` method to programmatically + determine the maximum number of extra inline forms. + +* :class:`~django.contrib.admin.InlineModelAdmin` can override + :meth:`~django.contrib.admin.InlineModelAdmin.get_max_num` method + to programmatically determine the maximum number of extra inline forms. + * Syndication feeds (:mod:`django.contrib.syndication`) can now pass extra context through to feed templates using a new `Feed.get_context_data()` callback. diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 44671d0..2f88248 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -129,6 +129,22 @@ class ChildModel1Inline(admin.TabularInline): class ChildModel2Inline(admin.StackedInline): model = ChildModel2 +# admin for #19425 and #18388 +class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_extra(self, request, obj=None, **kwargs): + extra = 2 + if obj: + return extra - obj.binarytree_set.count() + return extra + + def get_max_num(self, request, obj=None, **kwargs): + max_num = 3 + if obj: + return max_num - obj.binarytree_set.count() + return max_num + # admin for #19524 class SightingInline(admin.TabularInline): model = Sighting @@ -150,4 +166,5 @@ site.register(Author, AuthorAdmin) site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline, ReadOnlyInlineInline]) site.register(ProfileCollection, inlines=[ProfileInline]) site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline]) +site.register(BinaryTree, inlines=[BinaryTreeAdmin]) site.register(ExtraTerrestrial, inlines=[SightingInline]) diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py index 82c1c3f..d4ba0ab 100644 --- a/tests/admin_inlines/models.py +++ b/tests/admin_inlines/models.py @@ -183,6 +183,12 @@ class ChildModel2(models.Model): def get_absolute_url(self): return '/child_model2/' + +# Models for #19425 +class BinaryTree(models.Model): + name = models.CharField(max_length=100) + parent = models.ForeignKey('self', null=True, blank=True) + # Models for #19524 class LifeForm(models.Model): diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 714a2f1..1b18181 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -12,7 +12,7 @@ from .admin import InnerInline, TitleInline, site from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, - Sighting, Title, Novel, Chapter, FootNote) + Sighting, Title, Novel, Chapter, FootNote, BinaryTree) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -193,6 +193,20 @@ class TestInline(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(Sighting.objects.filter(et__name='Martian').count(), 1) + def test_custom_get_extra_form(self): + bt_head = BinaryTree(name="Tree Head") + bt_head.save() + bt_child = BinaryTree(name="First Child", parent=bt_head) + bt_child.save() + + # The total number of forms will remain the same in any case + total_forms_hidden = '' + response = self.client.get('/admin/admin_inlines/binarytree/add/') + self.assertContains(response, total_forms_hidden) + + response = self.client.get("/admin/admin_inlines/binarytree/%d/" % bt_head.id) + self.assertContains(response, total_forms_hidden) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class TestInlineMedia(TestCase):