diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 772734aae47..550f46aed38 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -696,6 +696,18 @@ class ModelAdmin(BaseModelAdmin): """ formset.save() + def save_related(self, request, form, formsets, change): + """ + Given the ``HttpRequest``, the parent ``ModelForm`` instance, the + list of inline formsets and a boolean value based on whether the + parent is being added or changed, save the related objects to the + database. Note that at this point save_form() and save_model() have + already been called. + """ + form.save_m2m() + for formset in formsets: + self.save_formset(request, form, formset, change=change) + def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): opts = self.model._meta app_label = opts.app_label @@ -899,11 +911,8 @@ class ModelAdmin(BaseModelAdmin): prefix=prefix, queryset=inline.queryset(request)) formsets.append(formset) if all_valid(formsets) and form_validated: - self.save_model(request, new_object, form, change=False) - form.save_m2m() - for formset in formsets: - self.save_formset(request, form, formset, change=False) - + self.save_model(request, new_object, form, False) + self.save_related(request, form, formsets, False) self.log_addition(request, new_object) return self.response_add(request, new_object) else: @@ -1001,11 +1010,8 @@ class ModelAdmin(BaseModelAdmin): formsets.append(formset) if all_valid(formsets) and form_validated: - self.save_model(request, new_object, form, change=True) - form.save_m2m() - for formset in formsets: - self.save_formset(request, form, formset, change=True) - + self.save_model(request, new_object, form, True) + self.save_related(request, form, formsets, True) change_message = self.construct_change_message(request, form, formsets) self.log_change(request, new_object, change_message) return self.response_change(request, new_object) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 4b472e71f0a..43057a31b05 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -978,6 +978,16 @@ templates used by the :class:`ModelAdmin` views: else: return ['name'] +.. method:: ModelAdmin.save_related(self, request, form, formsets, change) + + .. versionadded:: 1.4 + + The ``save_related`` method is given the ``HttpRequest``, the parent + ``ModelForm`` instance, the list of inline formsets and a boolean value + based on whether the parent is being added or changed. Here you can do any + pre- or post-save operations for objects related to the parent. Note + that at this point the parent object and its form have already been saved. + .. method:: ModelAdmin.get_readonly_fields(self, request, obj=None) .. versionadded:: 1.2 diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 20f1ba63cf9..781093123c4 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -80,6 +80,13 @@ to work similarly to how desktop GUIs do it. The new hook :meth:`~django.contrib.admin.ModelAdmin.get_ordering` for specifying the ordering dynamically (e.g. depending on the request) has also been added. +``ModelAdmin.save_related()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new :meth:`~django.contrib.admin.ModelAdmin.save_related` hook was added to +:mod:`~django.contrib.admin.ModelAdmin` to ease the customization of how +related objects are saved in the admin. + Tools for cryptographic signing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 52d96a93f64..8d79f44bb12 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -391,6 +391,14 @@ class ParentAdmin(admin.ModelAdmin): model = Parent inlines = [ChildInline] + def save_related(self, request, form, formsets, change): + super(ParentAdmin, self).save_related(request, form, formsets, change) + first_name, last_name = form.instance.name.split() + for child in form.instance.child_set.all(): + if len(child.name.split()) < 2: + child.name = child.name + ' ' + last_name + child.save() + class EmptyModel(models.Model): def __unicode__(self): return "Primary key = %s" % self.id diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 25056b5e04a..7128f04a64b 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -38,7 +38,7 @@ from models import (Article, BarAccount, CustomArticle, EmptyModel, Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee, Question, Answer, Inquisition, Actor, FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory, - ComplexSortedPerson) + ComplexSortedPerson, Parent, Child) class AdminViewBasicTest(TestCase): @@ -3113,3 +3113,50 @@ class DateHierarchyTests(TestCase): self.assert_non_localized_year(response, 2000) self.assert_non_localized_year(response, 2003) self.assert_non_localized_year(response, 2005) + +class AdminCustomSaveRelatedTests(TestCase): + """ + Ensure that one can easily customize the way related objects are saved. + Refs #16115. + """ + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def test_should_be_able_to_edit_related_objects_on_add_view(self): + post = { + 'child_set-TOTAL_FORMS': '3', + 'child_set-INITIAL_FORMS': '0', + 'name': 'Josh Stone', + 'child_set-0-name': 'Paul', + 'child_set-1-name': 'Catherine', + } + response = self.client.post('/test_admin/admin/admin_views/parent/add/', post) + self.assertEqual(1, Parent.objects.count()) + self.assertEqual(2, Child.objects.count()) + + children_names = list(Child.objects.order_by('name').values_list('name', flat=True)) + + self.assertEqual('Josh Stone', Parent.objects.latest('id').name) + self.assertEqual([u'Catherine Stone', u'Paul Stone'], children_names) + + def test_should_be_able_to_edit_related_objects_on_change_view(self): + parent = Parent.objects.create(name='Josh Stone') + paul = Child.objects.create(parent=parent, name='Paul') + catherine = Child.objects.create(parent=parent, name='Catherine') + post = { + 'child_set-TOTAL_FORMS': '5', + 'child_set-INITIAL_FORMS': '2', + 'name': 'Josh Stone', + 'child_set-0-name': 'Paul', + 'child_set-0-id': paul.id, + 'child_set-1-name': 'Catherine', + 'child_set-1-id': catherine.id, + } + response = self.client.post('/test_admin/admin/admin_views/parent/%s/' % parent.id, post) + + children_names = list(Child.objects.order_by('name').values_list('name', flat=True)) + + self.assertEqual('Josh Stone', Parent.objects.latest('id').name) + self.assertEqual([u'Catherine Stone', u'Paul Stone'], children_names)