| | 204 | class TestEditingInlineViews(TestCase): |
| | 205 | """ |
| | 206 | Make sure that concurrent editing does not result in an error for the user |
| | 207 | |
| | 208 | Refs #15574 |
| | 209 | |
| | 210 | These tests simulate two test clients by submitting slightly different data |
| | 211 | twice. This would be equivalent to working with two tabs open in a browser, |
| | 212 | or two different users working separately. Individually each of these forms |
| | 213 | is valid but the second one is invalid when submitted one after the other. |
| | 214 | """ |
| | 215 | fixtures = ['admin-views-users.xml'] |
| | 216 | |
| | 217 | def setUp(self): |
| | 218 | result = self.client.login(username='super', password='secret') |
| | 219 | self.failUnlessEqual(result, True) |
| | 220 | # Set up a thing to be modified in the test |
| | 221 | self.thing = Thing.objects.create(description="Parent object") |
| | 222 | self.thing_item_0 = ThingItem.objects.create(description="Item #0", thing=self.thing) |
| | 223 | self.thing_item_1 = ThingItem.objects.create(description="Item #1", thing=self.thing) |
| | 224 | |
| | 225 | def tearDown(self): |
| | 226 | self.client.logout() |
| | 227 | self.thing_item_0.delete() |
| | 228 | self.thing_item_1.delete() |
| | 229 | self.thing.delete() |
| | 230 | |
| | 231 | def test_concurrent_editing_first(self): |
| | 232 | """ |
| | 233 | Test concurrent editing when a user deletes the first inline in a list; |
| | 234 | second user is simply editing the same Thing object. |
| | 235 | """ |
| | 236 | data = { |
| | 237 | 'description': [u'A new description #2'], |
| | 238 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 239 | 'thingitem_set-TOTAL_FORMS': [u'2'], |
| | 240 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 241 | 'thingitem_set-0-DELETE': [u''], |
| | 242 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 243 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 244 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 245 | 'thingitem_set-1-DELETE': [u''], |
| | 246 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 247 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 248 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 249 | } |
| | 250 | self._do_test_concurrent_editing_views(deleted_idx=0, data=data) |
| | 251 | self.assertEqual(self.thing.thingitem_set.count(), 1) |
| | 252 | |
| | 253 | def test_concurrent_editing_first_delete_twice(self): |
| | 254 | """ |
| | 255 | Test concurrent editing when a user deletes the first inline in a list; |
| | 256 | second user is also deleting the same inline. |
| | 257 | """ |
| | 258 | data = { |
| | 259 | 'description': [u'A new description #3'], |
| | 260 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 261 | 'thingitem_set-TOTAL_FORMS': [u'2'], |
| | 262 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 263 | 'thingitem_set-0-DELETE': [u'on'], |
| | 264 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 265 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 266 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 267 | 'thingitem_set-1-DELETE': [u''], |
| | 268 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 269 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 270 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 271 | } |
| | 272 | self._do_test_concurrent_editing_views(deleted_idx=0, data=data) |
| | 273 | self.assertEqual(self.thing.thingitem_set.count(), 1) |
| | 274 | |
| | 275 | def test_concurrent_editing_first_add_another(self): |
| | 276 | """ |
| | 277 | Test concurrent editing when a user deletes the first inline in a list; |
| | 278 | second user is adding another inline on the same change_view. |
| | 279 | """ |
| | 280 | data = { |
| | 281 | 'description': [u'A new description #3'], |
| | 282 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 283 | 'thingitem_set-TOTAL_FORMS': [u'2'], |
| | 284 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 285 | 'thingitem_set-0-DELETE': [u'on'], |
| | 286 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 287 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 288 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 289 | 'thingitem_set-1-DELETE': [u'on'], |
| | 290 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 291 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 292 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 293 | } |
| | 294 | self._do_test_concurrent_editing_views(deleted_idx=0, data=data) |
| | 295 | self.assertEqual(self.thing.thingitem_set.count(), 0) |
| | 296 | |
| | 297 | def test_concurrent_editing_first_delete_different(self): |
| | 298 | """ |
| | 299 | Test concurrent editing when a user deletes the first inline in a list; |
| | 300 | second user is also deleting another inline on the same change_view. |
| | 301 | """ |
| | 302 | data = { |
| | 303 | 'description': [u'A new description #3'], |
| | 304 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 305 | 'thingitem_set-TOTAL_FORMS': [u'3'], |
| | 306 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 307 | 'thingitem_set-0-DELETE': [u''], |
| | 308 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 309 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 310 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 311 | 'thingitem_set-1-DELETE': [u''], |
| | 312 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 313 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 314 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 315 | 'thingitem_set-2-DELETE': [u''], |
| | 316 | 'thingitem_set-2-id': [u''], |
| | 317 | 'thingitem_set-2-thing': [u'%s' % (self.thing.pk,)], |
| | 318 | 'thingitem_set-2-description': [u'New item #2 description'], |
| | 319 | } |
| | 320 | self._do_test_concurrent_editing_views(deleted_idx=0, data=data) |
| | 321 | self.assertEqual(self.thing.thingitem_set.count(), 2) |
| | 322 | |
| | 323 | def test_concurrent_editing_last(self): |
| | 324 | """ |
| | 325 | Test concurrent editing when a user deletes the last inline in a list; |
| | 326 | second user is simply editing the same Thing object. |
| | 327 | """ |
| | 328 | data = { |
| | 329 | 'description': [u'A new description #2'], |
| | 330 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 331 | 'thingitem_set-TOTAL_FORMS': [u'2'], |
| | 332 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 333 | 'thingitem_set-0-DELETE': [u''], |
| | 334 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 335 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 336 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 337 | 'thingitem_set-1-DELETE': [u''], |
| | 338 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 339 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 340 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 341 | } |
| | 342 | self._do_test_concurrent_editing_views(deleted_idx=1, data=data) |
| | 343 | self.assertEqual(self.thing.thingitem_set.count(), 1) |
| | 344 | |
| | 345 | def test_concurrent_editing_last_delete_twice(self): |
| | 346 | """ |
| | 347 | Test concurrent editing when a user deletes the last inline in a list; |
| | 348 | second user is also deleting the same inline. |
| | 349 | """ |
| | 350 | data = { |
| | 351 | 'description': [u'A new description #3'], |
| | 352 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 353 | 'thingitem_set-TOTAL_FORMS': [u'2'], |
| | 354 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 355 | 'thingitem_set-0-DELETE': [u''], |
| | 356 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 357 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 358 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 359 | 'thingitem_set-1-DELETE': [u'on'], |
| | 360 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 361 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 362 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 363 | } |
| | 364 | self._do_test_concurrent_editing_views(deleted_idx=1, data=data) |
| | 365 | self.assertEqual(self.thing.thingitem_set.count(), 1) |
| | 366 | |
| | 367 | def test_concurrent_editing_last_add_another(self): |
| | 368 | """ |
| | 369 | Test concurrent editing when a user deletes the last inline in a list; |
| | 370 | second user is adding another inline on the same change_view. |
| | 371 | """ |
| | 372 | data = { |
| | 373 | 'description': [u'A new description #3'], |
| | 374 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 375 | 'thingitem_set-TOTAL_FORMS': [u'2'], |
| | 376 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 377 | 'thingitem_set-0-DELETE': [u'on'], |
| | 378 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 379 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 380 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 381 | 'thingitem_set-1-DELETE': [u'on'], |
| | 382 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 383 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 384 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 385 | } |
| | 386 | self._do_test_concurrent_editing_views(deleted_idx=1, data=data) |
| | 387 | self.assertEqual(self.thing.thingitem_set.count(), 0) |
| | 388 | |
| | 389 | def test_concurrent_editing_last_delete_different(self): |
| | 390 | """ |
| | 391 | Test concurrent editing when a user deletes the last inline in a list; |
| | 392 | second user is also deleting another inline on the same change_view. |
| | 393 | """ |
| | 394 | data = { |
| | 395 | 'description': [u'A new description #3'], |
| | 396 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 397 | 'thingitem_set-TOTAL_FORMS': [u'3'], |
| | 398 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 399 | 'thingitem_set-0-DELETE': [u''], |
| | 400 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 401 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 402 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 403 | 'thingitem_set-1-DELETE': [u''], |
| | 404 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 405 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 406 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 407 | 'thingitem_set-2-DELETE': [u''], |
| | 408 | 'thingitem_set-2-id': [u''], |
| | 409 | 'thingitem_set-2-thing': [u'%s' % (self.thing.pk,)], |
| | 410 | 'thingitem_set-2-description': [u'New item #2 description'], |
| | 411 | } |
| | 412 | self._do_test_concurrent_editing_views(deleted_idx=1, data=data) |
| | 413 | self.assertEqual(self.thing.thingitem_set.count(), 2) |
| | 414 | |
| | 415 | def _do_test_concurrent_editing_views(self, deleted_idx=0, data=None): |
| | 416 | thingitem_set_0_DELETE = u'' |
| | 417 | thingitem_set_1_DELETE = u'' |
| | 418 | |
| | 419 | if deleted_idx == 0: |
| | 420 | thingitem_set_0_DELETE = u'on' |
| | 421 | elif deleted_idx == 1: |
| | 422 | thingitem_set_1_DELETE = u'on' |
| | 423 | |
| | 424 | first_form_data = { |
| | 425 | 'description': [u'A new description'], |
| | 426 | 'thingitem_set-MAX_NUM_FORMS': [u'0'], |
| | 427 | 'thingitem_set-TOTAL_FORMS': [u'2'], |
| | 428 | 'thingitem_set-INITIAL_FORMS': [u'2'], |
| | 429 | 'thingitem_set-0-DELETE': [thingitem_set_0_DELETE], |
| | 430 | 'thingitem_set-0-id': [u'%s' % (self.thing_item_0.pk,)], |
| | 431 | 'thingitem_set-0-thing': [u'%s' % (self.thing.pk,)], |
| | 432 | 'thingitem_set-0-description': [u'New item #0 description'], |
| | 433 | 'thingitem_set-1-DELETE': [thingitem_set_1_DELETE], |
| | 434 | 'thingitem_set-1-id': [u'%s' % (self.thing_item_1.pk,)], |
| | 435 | 'thingitem_set-1-thing': [u'%s' % (self.thing.pk,)], |
| | 436 | 'thingitem_set-1-description': [u'Deleted item #1 description'], |
| | 437 | } |
| | 438 | edit_url = 'admin:%s_%s_change' %(self.thing._meta.app_label, self.thing._meta.module_name) |
| | 439 | view_url = 'admin:%s_%s_changelist' %(self.thing._meta.app_label, self.thing._meta.module_name) |
| | 440 | |
| | 441 | # Sanity check |
| | 442 | self.assertEqual(self.thing.thingitem_set.count(), 2) |
| | 443 | |
| | 444 | # Submit first form |
| | 445 | response = self.client.post(reverse(edit_url, args=[self.thing.pk]), first_form_data) |
| | 446 | |
| | 447 | # Check our first edit was accepted |
| | 448 | self.thing = Thing.objects.get(pk=self.thing.pk) |
| | 449 | self.assertRedirects(response, reverse(view_url), 302, 200) |
| | 450 | self.assertEqual(self.thing.thingitem_set.count(), 1) |
| | 451 | self.assertEqual(self.thing.description, first_form_data['description'][0]) |
| | 452 | |
| | 453 | # Submit the second form (this represents either the same user submitting |
| | 454 | # another tab that he had loaded before, or a second user). |
| | 455 | response = self.client.post(reverse(edit_url, args=[self.thing.pk]), data) |
| | 456 | |
| | 457 | # Check our second edit was handled |
| | 458 | self.thing = Thing.objects.get(pk=self.thing.pk) |
| | 459 | self.assertRedirects(response, reverse(view_url), 302, 200) |
| | 460 | self.assertEqual(self.thing.description, data['description'][0]) |
| | 461 | |
| | 462 | |