diff --git a/AUTHORS b/AUTHORS
index e3aec98..7195154 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -397,6 +397,7 @@ answer newbie questions, and generally made Django that much better:
     Todd O'Bryan <toddobryan@mac.com>
     Selwin Ong <selwin@ui.co.id>
     Gerardo Orozco <gerardo.orozco.mosqueda@gmail.com>
+    Paul Oswald <pauloswald@gmail.com> 
     Christian Oudard <christian.oudard@gmail.com>
     oggie rob <oz.robharvey@gmail.com>
     oggy <ognjen.maric@gmail.com>
@@ -413,6 +414,7 @@ answer newbie questions, and generally made Django that much better:
     phil@produxion.net
     phil.h.smith@gmail.com
     Gustavo Picon
+    Mathieu Pillard <django@virgule.net>
     Michael Placentra II <someone@michaelplacentra2.net>
     plisk
     Daniel Poelzleithner <http://poelzi.org/>
diff --git a/django/forms/models.py b/django/forms/models.py
index cd8f027..c53a8a1 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -421,19 +421,43 @@ 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._ignored_forms_count = 0
         super(BaseModelFormSet, self).__init__(**defaults)
 
     def initial_form_count(self):
         """Returns the number of forms that are required in this FormSet."""
         if not (self.data or self.files):
             return len(self.get_queryset())
-        return super(BaseModelFormSet, self).initial_form_count()
+        # Because the super total_form_count method will use the management_form
+        # data to return the total, we need to compensate by removing the number
+        # of ignored forms.
+        return super(BaseModelFormSet, self).initial_form_count() - self._ignored_forms_count
+
+    def total_form_count(self):
+        # Because the super total_form_count() method will use the management_form
+        # data to return the total, we need to compensate by removing the number
+        # of ignored forms.        
+        return super(BaseModelFormSet, self).total_form_count() - self._ignored_forms_count
 
     def _existing_object(self, pk):
         if not hasattr(self, '_object_dict'):
             self._object_dict = dict([(o.pk, o) for o in self.get_queryset()])
         return self._object_dict.get(pk)
 
+    def _construct_forms(self):
+        self.forms = []
+        ignored_forms_count = 0
+        for i in xrange(self.total_form_count()):
+            try:
+                form = self._construct_form(i)
+                self.forms.append(form)
+            except ValidationError:
+                ignored_forms_count += 1
+
+        # Now that we finished looping over the forms, we can set
+        # ignored_forms - that will affect all *_form_count() methods,
+        self._ignored_forms_count = ignored_forms_count
+
     def _construct_form(self, i, **kwargs):
         if self.is_bound and i < self.initial_form_count():
             # Import goes here instead of module-level because importing
@@ -448,7 +472,15 @@ class BaseModelFormSet(BaseFormSet):
                 pk = pk[0]
             kwargs['instance'] = self._existing_object(pk)
         if i < self.initial_form_count() and not kwargs.get('instance'):
-            kwargs['instance'] = self.get_queryset()[i]
+            if not self.is_bound:
+                kwargs['instance'] = self.get_queryset()[i]
+            else:
+                # This shouldn't happen: bound forms, because they contain the data,
+                # should already have a pk and therefore an instance.
+                # This means something is wrong, either the form was tampered with
+                # or the object has been deleted since. We can't build the form
+                # with the data we have, so immediately raise an error
+                raise ValidationError('Invalid pk when constructing form from data')
         if i >= self.initial_form_count() and self.initial_extra:
             # Set initial values for extra forms
             try:
diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py
index f2add00..91f17f5 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 8b620cc..296a8f4 100644
--- a/tests/regressiontests/admin_inlines/tests.py
+++ b/tests/regressiontests/admin_inlines/tests.py
@@ -4,13 +4,14 @@ from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
 from django.contrib.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,265 @@ 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)
+
+        # 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
