| 139 | |
| 140 | class TestEditingInlineViews(TestCase): |
| 141 | fixtures = ['admin-views-users.xml'] |
| 142 | |
| 143 | def setUp(self): |
| 144 | result = self.client.login(username='super', password='secret') |
| 145 | self.failUnlessEqual(result, True) |
| 146 | # Set up a thing to be modified in the test |
| 147 | self.thing = Thing.objects.create(description="Parent object") |
| 148 | self.thing_item_1 = ThingItem.objects.create(description="Item #1", thing=self.thing) |
| 149 | self.thing_item_2 = ThingItem.objects.create(description="Item #2", thing=self.thing) |
| 150 | |
| 151 | def tearDown(self): |
| 152 | self.client.logout() |
| 153 | self.thing_item_1.delete() |
| 154 | self.thing_item_2.delete() |
| 155 | self.thing.delete() |
| 156 | |
| 157 | def test_concurrent_editing_views(self): |
| 158 | """ |
| 159 | A ``POST`` to the edit view by two clients simultaneously is properly handled |
| 160 | |
| 161 | See issue #15574 |
| 162 | |
| 163 | We are going to simulate two test clients by submitting slightly different data twice. |
| 164 | This would be equivalent to working with two tabs open in a browser. Individually each of |
| 165 | these forms is valid but the second one is invalid when submitted one after the other. The |
| 166 | server needs to handle this case. |
| 167 | """ |
| 168 | |
| 169 | data = [ |
| 170 | # An existing item has been deleted. |
| 171 | { |
| 172 | u'description': [u'A new description'], |
| 173 | u'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| 174 | u'thingitem_set-TOTAL_FORMS': [u'2'], |
| 175 | u'thingitem_set-INITIAL_FORMS': [u'2'], |
| 176 | u'thingitem_set-0-DELETE': [u''], |
| 177 | u'thingitem_set-0-id': [u'%s' % (self.thing_item_1.pk,)], |
| 178 | u'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| 179 | u'thingitem_set-0-description': [u'New item #1 description'], |
| 180 | u'thingitem_set-1-DELETE': [u'on'], |
| 181 | u'thingitem_set-1-id': [u'%s' % (self.thing_item_2.pk,)], |
| 182 | u'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| 183 | u'thingitem_set-1-description': [u'Deleted item #2 description'], |
| 184 | }, |
| 185 | |
| 186 | # In this second form, we are now attempting to edit the deleted item. This form is |
| 187 | # invalid but the user submitting it doesn't know that yet. |
| 188 | { |
| 189 | u'description': [u'A new description #2'], |
| 190 | u'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| 191 | u'thingitem_set-TOTAL_FORMS': [u'2'], |
| 192 | u'thingitem_set-INITIAL_FORMS': [u'2'], |
| 193 | u'thingitem_set-0-DELETE': [u''], |
| 194 | u'thingitem_set-0-id': [u'%s' % (self.thing_item_1.pk,)], |
| 195 | u'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| 196 | u'thingitem_set-0-description': [u'New item #1 description'], |
| 197 | u'thingitem_set-1-DELETE': [u''], |
| 198 | u'thingitem_set-1-id': [u'%s' % (self.thing_item_2.pk,)], |
| 199 | u'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| 200 | u'thingitem_set-1-description': [u'Deleted item #2 description'], |
| 201 | }, |
| 202 | ] |
| 203 | edit_url = 'admin:%s_%s_change' %(self.thing._meta.app_label, self.thing._meta.module_name) |
| 204 | view_url = 'admin:%s_%s_changelist' %(self.thing._meta.app_label, self.thing._meta.module_name) |
| 205 | |
| 206 | count = Thing.objects.count() |
| 207 | response = self.client.post(reverse(edit_url, args=[self.thing.pk]), data[0]) |
| 208 | |
| 209 | # Check our first edit was accepted |
| 210 | self.assertRedirects(response, reverse(view_url), 302, 200) |
| 211 | |
| 212 | # Submit the second form. (this might be a second tab in the browser, or a second user) |
| 213 | response = self.client.post(reverse(edit_url, args=[self.thing.pk]), data[1]) |
| 214 | |
| 215 | # Check our second edit was handled |
| 216 | self.assertRedirects(response, reverse(view_url), 302, 200) |
| 217 | |
| 218 | |