diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 937bbd1ebb..c5096076a9 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -602,10 +602,48 @@ class ModelAdmin(BaseModelAdmin): self.get_changelist_form(request), extra=0, fields=self.list_editable, **defaults) - def get_formsets(self, request, obj=None): + def _get_formsets(self, request, obj): + """ + Helper function that exists to allow the deprecation warning to be + executed while this function continues to return a generator. + """ for inline in self.get_inline_instances(request, obj): yield inline.get_formset(request, obj) + def get_formsets(self, request, obj=None): + warnings.warn( + "ModelAdmin.get_formsets() is deprecated and will be removed in " + "Django 1.9. Use ModelAdmin.get_formsets_with_inlines() instead.", + PendingDeprecationWarning, stacklevel=2 + ) + return self._get_formsets(request, obj) + + def get_formsets_with_inlines(self, request, obj=None): + """ + Yields formsets and the corresponding inlines. + """ + # We call get_formsets() [deprecated] 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 overridden so we must warn about the + # deprecation. + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + formsets = self.get_formsets(request, obj) + + if len(w) != 1 or not issubclass(w[0].category, PendingDeprecationWarning): + warnings.warn( + "ModelAdmin.get_formsets() is deprecated and will be removed in " + "Django 1.9. Use ModelAdmin.get_formsets_with_inlines() instead.", + PendingDeprecationWarning + ) + if formsets: + zipped = zip(formsets, self.get_inline_instances(request, None)) + for formset, inline in zipped: + yield formset, inline + else: + for inline in self.get_inline_instances(request, obj): + yield inline.get_formset(request, obj), inline + def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True): return self.paginator(queryset, per_page, orphans, allow_empty_first_page) @@ -1185,8 +1223,6 @@ class ModelAdmin(BaseModelAdmin): raise PermissionDenied ModelForm = self.get_form(request) - formsets = [] - inline_instances = self.get_inline_instances(request, None) if request.method == 'POST': form = ModelForm(request.POST, request.FILES) if form.is_valid(): @@ -1195,7 +1231,7 @@ class ModelAdmin(BaseModelAdmin): else: form_validated = False new_object = self.model() - formsets = self._create_formsets(request, new_object, inline_instances) + formsets, inline_instances = self._create_formsets(request, new_object) if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, False) self.save_related(request, form, formsets, False) @@ -1213,7 +1249,7 @@ class ModelAdmin(BaseModelAdmin): if isinstance(f, models.ManyToManyField): initial[k] = initial[k].split(",") form = ModelForm(initial=initial) - formsets = self._create_formsets(request, self.model(), inline_instances) + formsets, inline_instances = self._create_formsets(request, self.model()) adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.get_prepopulated_fields(request), @@ -1266,7 +1302,6 @@ class ModelAdmin(BaseModelAdmin): current_app=self.admin_site.name)) ModelForm = self.get_form(request, obj) - inline_instances = self.get_inline_instances(request, obj) if request.method == 'POST': form = ModelForm(request.POST, request.FILES, instance=obj) if form.is_valid(): @@ -1275,7 +1310,7 @@ class ModelAdmin(BaseModelAdmin): else: form_validated = False new_object = obj - formsets = self._create_formsets(request, new_object, inline_instances) + formsets, inline_instances = self._create_formsets(request, new_object) if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, True) self.save_related(request, form, formsets, True) @@ -1285,7 +1320,7 @@ class ModelAdmin(BaseModelAdmin): else: form = ModelForm(instance=obj) - formsets = self._create_formsets(request, obj, inline_instances) + formsets, inline_instances = self._create_formsets(request, obj) adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.get_prepopulated_fields(request, obj), @@ -1566,14 +1601,15 @@ class ModelAdmin(BaseModelAdmin): "admin/object_history.html" ], context, current_app=self.admin_site.name) - def _create_formsets(self, request, obj, inline_instances): + def _create_formsets(self, request, obj): "Helper function to generate formsets for add/change_view." formsets = [] + inline_instances = [] prefixes = {} get_formsets_args = [request] if obj.pk: get_formsets_args.append(obj) - for FormSet, inline in zip(self.get_formsets(*get_formsets_args), inline_instances): + for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args): prefix = FormSet.get_default_prefix() prefixes[prefix] = prefixes.get(prefix, 0) + 1 if prefixes[prefix] != 1 or not prefix: @@ -1590,7 +1626,8 @@ class ModelAdmin(BaseModelAdmin): 'save_as_new': '_saveasnew' in request.POST }) formsets.append(FormSet(**formset_params)) - return formsets + inline_instances.append(inline) + return formsets, inline_instances class InlineModelAdmin(BaseModelAdmin): diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 9e473dfa46..c91ea2d218 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -453,6 +453,8 @@ these changes. * ``django.db.backends.util`` * ``django.forms.util`` +* ``ModelAdmin.get_formsets`` will be removed. + 2.0 --- diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index dd31d11dee..2e02785e81 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -454,7 +454,7 @@ subclass:: .. attribute:: ModelAdmin.inlines See :class:`InlineModelAdmin` objects below as well as - :meth:`ModelAdmin.get_formsets`. + :meth:`ModelAdmin.get_formsets_with_inlines`. .. attribute:: ModelAdmin.list_display @@ -1365,7 +1365,10 @@ templates used by the :class:`ModelAdmin` views: .. method:: ModelAdmin.get_formsets(self, request, obj=None) - Yields :class:`InlineModelAdmin`\s for use in admin add and change views. + .. deprecated:: 1.7 + Use :meth:`get_formsets_with_inlines()` instead. + + Yields :class:`InlineModelAdmin`\s for use in admin add and change views. For example if you wanted to display a particular inline only in the change view, you could override ``get_formsets`` as follows:: @@ -1380,6 +1383,24 @@ templates used by the :class:`ModelAdmin` views: continue yield inline.get_formset(request, obj) +.. method:: ModelAdmin.get_formsets_with_inlines(self, request, obj=None) + + Yields (``FormSet``, :class:`InlineModelAdmin`) pairs for use in admin add + and change views. + + For example if you wanted to display a particular inline only in the change + view, you could override ``get_formsets_with_inlines`` as follows:: + + class MyModelAdmin(admin.ModelAdmin): + inlines = [MyInline, SomeOtherInline] + + def get_formsets_with_inlines(self, request, obj=None): + for inline in self.get_inline_instances(request, obj): + # hide MyInline in the add view + if isinstance(inline, MyInline) and obj is None: + continue + yield inline.get_formset(request, obj), inline + .. method:: ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs) The ``formfield_for_foreignkey`` method on a ``ModelAdmin`` allows you to diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 04ebb1596c..148da5af86 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -466,13 +466,13 @@ than simply ``myapp/models.py``, Django would look for :ref:`initial SQL data will search ``myapp/sql/`` as documented. The old location will continue to work until Django 1.9. -``declared_fieldsets`` attribute on ``ModelAdmin.`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``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. +``ModelAdmin.declared_fieldsets`` has been 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. ``syncdb`` ~~~~~~~~~~ @@ -491,3 +491,10 @@ to ``utils.py`` in an effort to unify all util and utils references: * ``django.contrib.gis.db.backends.util`` * ``django.db.backends.util`` * ``django.forms.util`` + +``get_formsets`` method on ``ModelAdmin`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``ModelAdmin.get_formsets`` has been deprecated in favor of the new +:meth:`~django.contrib.admin.ModelAdmin.get_formsets_with_inlines`, in order to +better handle the case of selecting showing inlines on a ``ModelAdmin``. diff --git a/tests/generic_inline_admin/tests.py b/tests/generic_inline_admin/tests.py index 83bee2f4e0..a7e7bd65b7 100644 --- a/tests/generic_inline_admin/tests.py +++ b/tests/generic_inline_admin/tests.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import warnings from django.conf import settings from django.contrib import admin @@ -277,7 +278,7 @@ class GenericInlineModelAdminTest(TestCase): ma = EpisodeAdmin(Episode, self.site) self.assertEqual( - list(list(ma.get_formsets(request))[0]().forms[0].fields), + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), ['keywords', 'id', 'DELETE']) def test_custom_form_meta_exclude(self): @@ -307,7 +308,7 @@ class GenericInlineModelAdminTest(TestCase): ma = EpisodeAdmin(Episode, self.site) self.assertEqual( - list(list(ma.get_formsets(request))[0]().forms[0].fields), + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), ['url', 'keywords', 'id', 'DELETE']) # Then, only with `ModelForm` ----------------- @@ -323,7 +324,7 @@ class GenericInlineModelAdminTest(TestCase): ma = EpisodeAdmin(Episode, self.site) self.assertEqual( - list(list(ma.get_formsets(request))[0]().forms[0].fields), + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), ['description', 'keywords', 'id', 'DELETE']) def test_get_fieldsets(self): @@ -345,3 +346,89 @@ class GenericInlineModelAdminTest(TestCase): ma = MediaInline(Media, self.site) form = ma.get_formset(None).form self.assertEqual(form._meta.fields, ['url', 'description']) + + def test_get_formsets_with_inlines(self): + """ + get_formsets() triggers a deprecation warning when get_formsets is + overridden. + """ + class MediaForm(ModelForm): + class Meta: + model = Media + exclude = ['url'] + + class MediaInline(GenericTabularInline): + exclude = ['description'] + form = MediaForm + model = Media + + class EpisodeAdmin(admin.ModelAdmin): + inlines = [ + MediaInline + ] + + def get_formsets(self, request, obj=None): + return [] + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ma = EpisodeAdmin(Episode, self.site) + list(ma.get_formsets_with_inlines(request)) + # Verify that the deprecation warning was triggered when get_formsets was called + # This verifies that we called that method. + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[0].category, PendingDeprecationWarning)) + + class EpisodeAdmin(admin.ModelAdmin): + inlines = [ + MediaInline + ] + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ma = EpisodeAdmin(Episode, self.site) + list(ma.get_formsets_with_inlines(request)) + self.assertEqual(len(w), 0) + + def test_get_formsets_with_inlines_returns_tuples(self): + """ + Ensure that get_formsets_with_inlines() returns the correct tuples. + """ + class MediaForm(ModelForm): + class Meta: + model = Media + exclude = ['url'] + + class MediaInline(GenericTabularInline): + form = MediaForm + model = Media + + class AlternateInline(GenericTabularInline): + form = MediaForm + model = Media + + class EpisodeAdmin(admin.ModelAdmin): + inlines = [ + AlternateInline, MediaInline + ] + ma = EpisodeAdmin(Episode, self.site) + inlines = ma.get_inline_instances(request) + for (formset, inline), other_inline in zip(ma.get_formsets_with_inlines(request), inlines): + self.assertIsInstance(formset, other_inline.get_formset(request).__class__) + + class EpisodeAdmin(admin.ModelAdmin): + inlines = [ + AlternateInline, MediaInline + ] + + def get_formsets(self, request, obj=None): + # Catch the deprecation warning to force the usage of get_formsets + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + return super(EpisodeAdmin, self).get_formsets(request, obj) + + ma = EpisodeAdmin(Episode, self.site) + inlines = ma.get_inline_instances(request) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + for (formset, inline), other_inline in zip(ma.get_formsets_with_inlines(request), inlines): + self.assertIsInstance(formset, other_inline.get_formset(request).__class__) diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 424588cf49..53c977a890 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -207,7 +207,7 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertEqual( - list(list(ma.get_formsets(request))[0]().forms[0].fields), + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), ['main_band', 'opening_band', 'id', 'DELETE']) def test_custom_form_meta_exclude(self): @@ -253,7 +253,7 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertEqual( - list(list(ma.get_formsets(request))[0]().forms[0].fields), + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), ['main_band', 'opening_band', 'day', 'id', 'DELETE']) def test_custom_form_validation(self): @@ -327,7 +327,7 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertEqual( - list(list(ma.get_formsets(request))[0]().forms[0].fields), + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), ['main_band', 'day', 'transport', 'id', 'DELETE']) def test_queryset_override(self): @@ -521,7 +521,7 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertEqual( - list(list(ma.get_formsets(request))[0]().forms[0].fields), + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), ['extra', 'transport', 'id', 'DELETE', 'main_band'])