diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index 1e40a2c..7ed2336 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -19,6 +19,9 @@ MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS'
 ORDERING_FIELD_NAME = 'ORDER'
 DELETION_FIELD_NAME = 'DELETE'
 
+class BrokenFormError(Exception):
+    pass
+
 class ManagementForm(Form):
     """
     ``ManagementForm`` is used to keep track of how many form instances
@@ -31,6 +34,11 @@ class ManagementForm(Form):
         self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput)
         super(ManagementForm, self).__init__(*args, **kwargs)
 
+    def revalidate(self):
+        self._errors = None
+        if not self.is_valid():
+            raise ValidationError('ManagementForm data is missing or has been tampered with')
+
 class BaseFormSet(StrAndUnicode):
     """
     A collection of instances of the same Form class.
@@ -124,8 +132,38 @@ class BaseFormSet(StrAndUnicode):
     def _construct_forms(self):
         # instantiate all the forms and put them in self.forms
         self.forms = []
-        for i in xrange(self.total_form_count()):
-            self.forms.append(self._construct_form(i))
+        form_index = 0
+        initial_count = self.initial_form_count()
+        total_count = self.total_form_count()
+        orig_total_count = total_count
+        for i in xrange(orig_total_count):
+            try:
+                self.forms.append(self._construct_form(form_index))
+                form_index += 1
+            except BrokenFormError:
+                # wipe out all data of the broken form
+                key_prefix = '%s-%s' % (self.prefix, str(form_index))
+                for key in self.data.keys():
+                    if key.startswith(key_prefix):
+                        del self.data[key]
+                # shift the data IDs of the remaining forms in the formset by one
+                n = 0
+                start = i + 1
+                for j in range(start, total_count):
+                    key_prefix = '%s-%s' % (self.prefix, str(j))
+                    new_key_prefix = '%s-%s' % (self.prefix, str(form_index + n))
+                    for key in self.data.keys():
+                        if key.startswith(key_prefix):
+                            new_key = key.replace(key_prefix, new_key_prefix)
+                            self.data[new_key] = self.data[key]
+                            del self.data[key]
+                    n += 1
+                # decrement the number of forms in the formset by one
+                total_count -= 1
+                initial_count -= 1
+                self.data['%s-%s' % (self.prefix, INITIAL_FORM_COUNT)] = initial_count
+                self.data['%s-%s' % (self.prefix, TOTAL_FORM_COUNT)] = total_count 
+                self.management_form.revalidate()
 
     def _construct_form(self, i, **kwargs):
         """
diff --git a/django/forms/models.py b/django/forms/models.py
index 88a5511..84eb276 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -9,7 +9,7 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
 from django.core.validators import EMPTY_VALUES
 from django.forms.fields import Field, ChoiceField
 from django.forms.forms import BaseForm, get_declared_fields
-from django.forms.formsets import BaseFormSet, formset_factory
+from django.forms.formsets import BaseFormSet, formset_factory, BrokenFormError
 from django.forms.util import ErrorList
 from django.forms.widgets import (SelectMultiple, HiddenInput,
     MultipleHiddenInput, media_property)
@@ -434,6 +434,7 @@ class BaseModelFormSet(BaseFormSet):
         self.initial_extra = kwargs.pop('initial', None)
         defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
         defaults.update(kwargs)
+        self._object_deleted = False
         super(BaseModelFormSet, self).__init__(**defaults)
 
     def initial_form_count(self):
@@ -460,6 +461,9 @@ class BaseModelFormSet(BaseFormSet):
             if isinstance(pk, list):
                 pk = pk[0]
             kwargs['instance'] = self._existing_object(pk)
+            if kwargs['instance'] is None:
+                self._object_deleted = True
+                raise BrokenFormError()
         if i < self.initial_form_count() and not kwargs.get('instance'):
             kwargs['instance'] = self.get_queryset()[i]
         if i >= self.initial_form_count() and self.initial_extra:
@@ -511,6 +515,8 @@ class BaseModelFormSet(BaseFormSet):
 
     def clean(self):
         self.validate_unique()
+        if self._object_deleted:
+            raise ValidationError(ugettext(u'Some object on this formset was already deleted by another transaction. Please re-submit the form.'))
 
     def validate_unique(self):
         # Collect unique_checks and date_checks to run from all the forms.
diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py
index f2add00..40d5235 100644
--- a/tests/regressiontests/admin_inlines/models.py
+++ b/tests/regressiontests/admin_inlines/models.py
@@ -5,6 +5,7 @@ Testing of admin inline formsets.
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
+from django.contrib import admin
 
 
 class Parent(models.Model):
@@ -56,6 +57,24 @@ class Holder2(models.Model):
     dummy = models.IntegerField()
 
 
+# Objects for bug #15574 Test
+class Thing(models.Model):
+    description = models.CharField(max_length=256)
+
+class ThingItem(models.Model):
+    description = models.CharField(max_length=256)
+    thing = models.ForeignKey(Thing)
+
+class ThingItemInline(admin.TabularInline):
+    model = ThingItem
+    extra = 0
+
+class ThingAdmin(admin.ModelAdmin):
+    inlines = [ThingItemInline,]
+
+admin.site.register(Thing, ThingAdmin)
+
+
 class Inner2(models.Model):
     dummy = models.IntegerField()
     holder = models.ForeignKey(Holder2)
diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
index 029b032..fdf4ec6 100644
--- a/tests/regressiontests/admin_inlines/tests.py
+++ b/tests/regressiontests/admin_inlines/tests.py
@@ -4,13 +4,14 @@ from tlp.admin.tests import AdminSeleniumWebDriverTestCase
 from tlp.admin.helpers import InlineAdminForm
 from django.contrib.auth.models import User, Permission
 from django.contrib.contenttypes.models import ContentType
+from django.core.urlresolvers import reverse
 from django.test import TestCase
 
 # local test models
 from .admin import InnerInline
 from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
     OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
-    ProfileCollection)
+    ProfileCollection, Thing, ThingItem)
 
 
 class TestInline(TestCase):
@@ -200,6 +201,309 @@ class TestInlineAdminForm(TestCase):
         parent_ct = ContentType.objects.get_for_model(Parent)
         self.assertEqual(iaf.original.content_type, parent_ct)
 
+class TestEditingInlineViews(TestCase):
+    """
+    Make sure that concurrent editing does not result in an error for the user
+
+    Refs #15574
+
+    These tests simulate two test clients by submitting slightly different data
+    twice. This would be equivalent to working with two tabs open in a browser,
+    or two different users working separately. Individually each of these forms
+    is valid but the second one is invalid when submitted one after the other. 
+    """    
+    fixtures = ['admin-views-users.xml']
+
+    def setUp(self):
+        result = self.client.login(username='super', password='secret')
+        self.failUnlessEqual(result, True)
+        # Set up a thing to be modified in the test
+        self.thing = Thing.objects.create(description="Parent object")
+        self.thing_item_0 = ThingItem.objects.create(description="Item #0", thing=self.thing)
+        self.thing_item_1 = ThingItem.objects.create(description="Item #1", thing=self.thing)
+
+    def tearDown(self):
+        self.client.logout()
+        self.thing_item_0.delete()
+        self.thing_item_1.delete()
+        self.thing.delete()
+
+    def test_concurrent_editing_first(self):
+        """
+        Test concurrent editing when a user deletes the first inline in a list;
+        second user is simply editing the same Thing object.
+        """
+        data = {
+            'description': [u'A new description #2'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'2'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [u''],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [u''],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+        }
+        self._do_test_concurrent_editing_views(deleted_idx=0, data=data)
+        self.assertEqual(self.thing.thingitem_set.count(), 1)
+
+    def test_concurrent_editing_first_delete_twice(self):
+        """
+        Test concurrent editing when a user deletes the first inline in a list;
+        second user is also deleting the same inline.
+        """
+        data = {
+            'description': [u'A new description #3'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'2'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [u'on'],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [u''],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+        }
+        self._do_test_concurrent_editing_views(deleted_idx=0, data=data)
+        self.assertEqual(self.thing.thingitem_set.count(), 1)
+
+    def test_concurrent_editing_first_add_another(self):
+        """
+        Test concurrent editing when a user deletes the first inline in a list;
+        second user is adding another inline on the same change_view.
+        """
+        data = {
+            'description': [u'A new description #3'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'2'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [u'on'],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [u'on'],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+        }
+        self._do_test_concurrent_editing_views(deleted_idx=0, data=data)
+        self.assertEqual(self.thing.thingitem_set.count(), 0)
+
+    def test_concurrent_editing_first_delete_different(self):
+        """
+        Test concurrent editing when a user deletes the first inline in a list;
+        second user is also deleting another inline on the same change_view.
+        """
+        data = {
+            'description': [u'A new description #3'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'3'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [u''],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [u''],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+            'thingitem_set-2-DELETE': [u''],
+            'thingitem_set-2-id': [u''],
+            'thingitem_set-2-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-2-description': [u'New item #2 description'],
+        }
+        self._do_test_concurrent_editing_views(deleted_idx=0, data=data)
+        self.assertEqual(self.thing.thingitem_set.count(), 2)
+
+    def test_concurrent_editing_last(self):
+        """
+        Test concurrent editing when a user deletes the last inline in a list;
+        second user is simply editing the same Thing object.
+        """
+        data = {
+            'description': [u'A new description #2'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'2'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [u''],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [u''],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+        }
+        self._do_test_concurrent_editing_views(deleted_idx=1, data=data)
+        self.assertEqual(self.thing.thingitem_set.count(), 1)
+
+    def test_concurrent_editing_last_delete_twice(self):
+        """
+        Test concurrent editing when a user deletes the last inline in a list;
+        second user is also deleting the same inline.
+        """
+        data = {
+            'description': [u'A new description #3'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'2'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [u''],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [u'on'],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+        }
+        self._do_test_concurrent_editing_views(deleted_idx=1, data=data)
+        self.assertEqual(self.thing.thingitem_set.count(), 1)
+
+    def test_concurrent_editing_last_add_another(self):
+        """
+        Test concurrent editing when a user deletes the last inline in a list;
+        second user is adding another inline on the same change_view.
+        """
+        data = {
+            'description': [u'A new description #3'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'2'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [u'on'],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [u'on'],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+        }
+        self._do_test_concurrent_editing_views(deleted_idx=1, data=data)
+        self.assertEqual(self.thing.thingitem_set.count(), 0)
+
+    def test_concurrent_editing_last_delete_different(self):
+        """
+        Test concurrent editing when a user deletes the last inline in a list;
+        second user is also deleting another inline on the same change_view.
+        """
+        data = {
+            'description': [u'A new description #3'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'3'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [u''],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [u''],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+            'thingitem_set-2-DELETE': [u''],
+            'thingitem_set-2-id': [u''],
+            'thingitem_set-2-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-2-description': [u'New item #2 description'],
+        }
+        self._do_test_concurrent_editing_views(deleted_idx=1, data=data)
+        self.assertEqual(self.thing.thingitem_set.count(), 2)
+
+    def _do_test_concurrent_editing_views(self, deleted_idx=0, data=None):
+        thingitem_set_0_DELETE = u''
+        thingitem_set_1_DELETE = u''
+
+        if deleted_idx == 0:
+            thingitem_set_0_DELETE = u'on'
+        elif deleted_idx == 1:
+            thingitem_set_1_DELETE = u'on'
+
+        first_form_data = {
+            'description': [u'A new description'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [u'2'],
+            'thingitem_set-INITIAL_FORMS': [u'2'],
+            'thingitem_set-0-DELETE': [thingitem_set_0_DELETE],
+            'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)],
+            'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-0-description': [u'New item #0 description'],
+            'thingitem_set-1-DELETE': [thingitem_set_1_DELETE],
+            'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)],
+            'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)],
+            'thingitem_set-1-description': [u'Deleted item #1 description'],
+        }
+        edit_url = 'admin:%s_%s_change' %(self.thing._meta.app_label,  self.thing._meta.module_name)
+        view_url = 'admin:%s_%s_changelist' %(self.thing._meta.app_label,  self.thing._meta.module_name)
+
+        # Sanity check
+        self.assertEqual(self.thing.thingitem_set.count(), 2)
+
+        # Submit first form
+        response = self.client.post(reverse(edit_url, args=[self.thing.pk]), first_form_data)
+
+        # Check our first edit was accepted
+        self.thing = Thing.objects.get(pk=self.thing.pk)
+        self.assertRedirects(response, reverse(view_url), 302, 200)
+        self.assertEqual(self.thing.thingitem_set.count(), 1)
+        self.assertEqual(self.thing.description, first_form_data['description'][0])
+
+        # Submit the second form (this represents either the same user submitting
+        # another tab that he had loaded before, or a second user).
+        response = self.client.post(reverse(edit_url, args=[self.thing.pk]), data)
+
+        self.assertContains(response, 'Please correct the error below.')
+
+        total_forms = int(data['thingitem_set-TOTAL_FORMS'][0]) - 1
+        initial_forms = int(data['thingitem_set-INITIAL_FORMS'][0]) - 1
+
+        invalid_form_data = {
+            'description': data['description'],
+            'thingitem_set-MAX_NUM_FORMS': [u'0'],
+            'thingitem_set-TOTAL_FORMS': [str(total_forms)],
+            'thingitem_set-INITIAL_FORMS': [str(initial_forms)],
+        }
+        for key, value in data.iteritems():
+            current_form_id = key.split('-')
+            if len(current_form_id) < 3:
+                continue
+            current_form_id = int(current_form_id[1])
+            if deleted_idx == 0 and '-0-' in key:
+                continue
+            if deleted_idx == 1 and '-1-' in key:
+                continue
+            new_form_id = current_form_id
+            if current_form_id > 0:
+                new_form_id -= 1
+            new_key = key.replace(str(current_form_id), str(new_form_id))
+            invalid_form_data[new_key] = data[key]
+
+        if deleted_idx == 0:
+            preserved_item_id = self.thing_item_1.pk
+            deleted_item_id = self.thing_item_0.pk
+        elif deleted_idx == 1:
+            preserved_item_id = self.thing_item_0.pk
+            deleted_item_id = self.thing_item_1.pk
+
+        self.assertContains(response, 'name="thingitem_set-TOTAL_FORMS" value="%s"' % total_forms)
+        self.assertContains(response, 'name="thingitem_set-INITIAL_FORMS" value="%s"' % initial_forms)
+        self.assertContains(response, '<ul class="errorlist"><li>Some object on this formset was already deleted by another transaction. Please re-submit the form.</li>')
+
+        if initial_forms > 0:
+            self.assertContains(response, 'name="thingitem_set-0-id" value="%s" id="id_thingitem_set-0-id"' % preserved_item_id)
+        self.assertNotContains(response, '-id" value="%s" id="id_thingitem_set' % deleted_item_id)
+        self.assertNotContains(response, 'name="thingitem_set-%s-id"' % str(total_forms + 1))
+
+        response = self.client.post(reverse(edit_url, args=[self.thing.pk]), invalid_form_data)
+
+        # Check our second edit was handled
+        self.thing = Thing.objects.get(pk=self.thing.pk)
+        self.assertRedirects(response, reverse(view_url), 302, 200)
+        self.assertEqual(self.thing.description, data['description'][0])
+
+
 class TestInlinePermissions(TestCase):
     """
     Make sure the admin respects permissions for objects that are edited
