Index: tests/regressiontests/admin_views/tests.py
===================================================================
--- tests/regressiontests/admin_views/tests.py	(revision 16291)
+++ tests/regressiontests/admin_views/tests.py	(working copy)
@@ -37,7 +37,8 @@
     Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit,
     Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
     Question, Answer, Inquisition, Actor, FoodDelivery,
-    RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory)
+    RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory,
+    Parent, Child)
 
 
 class AdminViewBasicTest(TestCase):
@@ -3001,3 +3002,46 @@
             self.assert_non_localized_year(response, 2000)
             self.assert_non_localized_year(response, 2003)
             self.assert_non_localized_year(response, 2005)
+
+class AdminHooksTests(TestCase):
+    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)
\ No newline at end of file
Index: tests/regressiontests/admin_views/models.py
===================================================================
--- tests/regressiontests/admin_views/models.py	(revision 16291)
+++ tests/regressiontests/admin_views/models.py	(working copy)
@@ -386,6 +386,14 @@
 class ParentAdmin(admin.ModelAdmin):
     model = Parent
     inlines = [ChildInline]
+    
+    def save_model_and_relations(self, request, obj, form, formsets, change):
+        super(ParentAdmin, self).save_model_and_relations(request, obj, form, formsets, change)
+        first_name, last_name = obj.name.split()
+        for child in obj.child_set.all():
+            if len(child.name.split()) < 2:
+                child.name = child.name + ' ' + last_name
+                child.save()
 
 class EmptyModel(models.Model):
     def __unicode__(self):
Index: tests/regressiontests/modeladmin/tests.py
===================================================================
--- tests/regressiontests/modeladmin/tests.py	(revision 16291)
+++ tests/regressiontests/modeladmin/tests.py	(working copy)
@@ -302,8 +302,36 @@
         self.assertEqual(
             list(ma.get_formsets(request))[0]().forms[0].fields.keys(),
             ['extra', 'transport', 'id', 'DELETE', 'main_band'])
+    
+    def test_save_model_and_relations_should_call_save_model_and_save_formset(self):
+        method_calls = {
+            'save_model': [],
+            'save_formset': [],
+            'form.save_m2m': False
+        }
+        class BandAdmin(ModelAdmin):
+            def save_model(self, request, obj, form, change):
+                method_calls['save_model'] = (request, obj, form, change)
 
+            def save_formset(self, request, form, formset, change):
+                method_calls['save_formset'].append((request, form, formset, change))
 
+        class FakeForm(object):
+            def save_m2m(self):
+                method_calls['form.save_m2m'] = True
+
+        request, obj, form, formset1, formset2 = object(), object, FakeForm(), object(), object()
+
+        band_admin = BandAdmin(Band, self.site)
+        band_admin.save_model_and_relations(request, obj, form, [formset1,formset2], change=True)
+
+        self.assertTrue(1, len(method_calls['save_model']))
+        self.assertTrue(2, len(method_calls['save_formset']))
+        self.assertEqual((request, obj, form, True), method_calls['save_model'])
+        self.assertTrue((request, form, formset1, True), method_calls['save_formset'][0])
+        self.assertTrue((request, form, formset2, True), method_calls['save_formset'][1])
+
+
 class ValidationTests(unittest.TestCase):
     def test_validation_only_runs_in_debug(self):
         # Ensure validation only runs when DEBUG = True
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(revision 16291)
+++ django/contrib/admin/options.py	(working copy)
@@ -683,6 +683,19 @@
         """
         formset.save()
 
+    def save_model_and_relations(self, request, obj, form, formsets, change):
+        """
+        Given a model instance, a bound form and a list of inline formsets
+        save everything in to the database. 
+    
+        At this point save_form() has already been called, so just 
+        form.save_m2m() needs to be called.
+        """
+        self.save_model(request, obj, form, change=change)
+        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
@@ -886,10 +899,7 @@
                                   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_and_relations(request, new_object, form, formsets, change=False)
 
                 self.log_addition(request, new_object)
                 return self.response_add(request, new_object)
@@ -988,10 +998,7 @@
                 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_and_relations(request, new_object, form, formsets, change=True)
 
                 change_message = self.construct_change_message(request, form, formsets)
                 self.log_change(request, new_object, change_message)
