1from functools import partial 2 3from django.test import TestCase 4from django.utils.safestring import SafeString 5 6from wagtail.admin import compare 7from wagtail.core.blocks import StreamValue 8from wagtail.images import get_image_model 9from wagtail.images.tests.utils import get_test_image_file 10from wagtail.tests.testapp.models import ( 11 AdvertWithCustomPrimaryKey, EventCategory, EventPage, EventPageSpeaker, 12 HeadCountRelatedModelUsingPK, SimplePage, SnippetChooserModelWithCustomPrimaryKey, StreamPage, 13 TaggedPage) 14 15 16class TestFieldComparison(TestCase): 17 comparison_class = compare.FieldComparison 18 19 def test_hasnt_changed(self): 20 comparison = self.comparison_class( 21 SimplePage._meta.get_field('content'), 22 SimplePage(content="Content"), 23 SimplePage(content="Content"), 24 ) 25 26 self.assertTrue(comparison.is_field) 27 self.assertFalse(comparison.is_child_relation) 28 self.assertEqual(comparison.field_label(), "Content") 29 self.assertEqual(comparison.htmldiff(), 'Content') 30 self.assertIsInstance(comparison.htmldiff(), SafeString) 31 self.assertFalse(comparison.has_changed()) 32 33 def test_has_changed(self): 34 comparison = self.comparison_class( 35 SimplePage._meta.get_field('content'), 36 SimplePage(content="Original content"), 37 SimplePage(content="Modified content"), 38 ) 39 40 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Original content</span><span class="addition">Modified content</span>') 41 self.assertIsInstance(comparison.htmldiff(), SafeString) 42 self.assertTrue(comparison.has_changed()) 43 44 def test_htmldiff_escapes_value(self): 45 comparison = self.comparison_class( 46 SimplePage._meta.get_field('content'), 47 SimplePage(content='Original content'), 48 SimplePage(content='<script type="text/javascript">doSomethingBad();</script>'), 49 ) 50 51 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Original content</span><span class="addition"><script type="text/javascript">doSomethingBad();</script></span>') 52 self.assertIsInstance(comparison.htmldiff(), SafeString) 53 54 55class TestTextFieldComparison(TestFieldComparison): 56 comparison_class = compare.TextFieldComparison 57 58 # Only change from FieldComparison is the HTML diff is performed on words 59 # instead of the whole field value. 60 def test_has_changed(self): 61 comparison = self.comparison_class( 62 SimplePage._meta.get_field('content'), 63 SimplePage(content="Original content"), 64 SimplePage(content="Modified content"), 65 ) 66 67 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Original</span><span class="addition">Modified</span> content') 68 self.assertIsInstance(comparison.htmldiff(), SafeString) 69 self.assertTrue(comparison.has_changed()) 70 71 def test_from_none_to_value_only_shows_addition(self): 72 comparison = self.comparison_class( 73 SimplePage._meta.get_field('content'), 74 SimplePage(content=None), 75 SimplePage(content="Added content") 76 ) 77 78 self.assertEqual(comparison.htmldiff(), '<span class="addition">Added content</span>') 79 self.assertIsInstance(comparison.htmldiff(), SafeString) 80 self.assertTrue(comparison.has_changed()) 81 82 def test_from_value_to_none_only_shows_deletion(self): 83 comparison = self.comparison_class( 84 SimplePage._meta.get_field('content'), 85 SimplePage(content="Removed content"), 86 SimplePage(content=None) 87 ) 88 89 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Removed content</span>') 90 self.assertIsInstance(comparison.htmldiff(), SafeString) 91 self.assertTrue(comparison.has_changed()) 92 93 94class TestRichTextFieldComparison(TestFieldComparison): 95 comparison_class = compare.RichTextFieldComparison 96 97 # Only change from FieldComparison is the HTML diff is performed on words 98 # instead of the whole field value. 99 def test_has_changed(self): 100 comparison = self.comparison_class( 101 SimplePage._meta.get_field('content'), 102 SimplePage(content="Original content"), 103 SimplePage(content="Modified content"), 104 ) 105 106 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Original</span><span class="addition">Modified</span> content') 107 self.assertIsInstance(comparison.htmldiff(), SafeString) 108 self.assertTrue(comparison.has_changed()) 109 110 # Only change from FieldComparison is that this comparison disregards HTML tags 111 def test_has_changed_html(self): 112 comparison = self.comparison_class( 113 SimplePage._meta.get_field('content'), 114 SimplePage(content="<b>Original</b> content"), 115 SimplePage(content="Modified <i>content</i>"), 116 ) 117 118 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Original</span><span class="addition">Modified</span> content') 119 self.assertIsInstance(comparison.htmldiff(), SafeString) 120 self.assertTrue(comparison.has_changed()) 121 122 def test_htmldiff_escapes_value(self): 123 # Need to override this one as the HTML tags are stripped by RichTextFieldComparison 124 comparison = self.comparison_class( 125 SimplePage._meta.get_field('content'), 126 SimplePage(content='Original content'), 127 SimplePage(content='<script type="text/javascript">doSomethingBad();</script>'), 128 ) 129 130 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Original content</span><span class="addition">doSomethingBad();</span>') 131 self.assertIsInstance(comparison.htmldiff(), SafeString) 132 133 134class TestStreamFieldComparison(TestCase): 135 comparison_class = compare.StreamFieldComparison 136 137 def test_hasnt_changed(self): 138 field = StreamPage._meta.get_field('body') 139 140 comparison = self.comparison_class( 141 field, 142 StreamPage(body=StreamValue(field.stream_block, [ 143 ('text', "Content", '1'), 144 ])), 145 StreamPage(body=StreamValue(field.stream_block, [ 146 ('text', "Content", '1'), 147 ])), 148 ) 149 150 self.assertTrue(comparison.is_field) 151 self.assertFalse(comparison.is_child_relation) 152 self.assertEqual(comparison.field_label(), "Body") 153 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Content</div>') 154 self.assertIsInstance(comparison.htmldiff(), SafeString) 155 self.assertFalse(comparison.has_changed()) 156 157 def test_has_changed(self): 158 field = StreamPage._meta.get_field('body') 159 160 comparison = self.comparison_class( 161 field, 162 StreamPage(body=StreamValue(field.stream_block, [ 163 ('text', "Original content", '1'), 164 ])), 165 StreamPage(body=StreamValue(field.stream_block, [ 166 ('text', "Modified content", '1'), 167 ])), 168 ) 169 170 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object"><span class="deletion">Original</span><span class="addition">Modified</span> content</div>') 171 self.assertIsInstance(comparison.htmldiff(), SafeString) 172 self.assertTrue(comparison.has_changed()) 173 174 def test_add_block(self): 175 field = StreamPage._meta.get_field('body') 176 177 comparison = self.comparison_class( 178 field, 179 StreamPage(body=StreamValue(field.stream_block, [ 180 ('text', "Content", '1'), 181 ])), 182 StreamPage(body=StreamValue(field.stream_block, [ 183 ('text', "Content", '1'), 184 ('text', "New Content", '2'), 185 ])), 186 ) 187 188 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Content</div>\n<div class="comparison__child-object addition">New Content</div>') 189 self.assertIsInstance(comparison.htmldiff(), SafeString) 190 self.assertTrue(comparison.has_changed()) 191 192 def test_delete_block(self): 193 field = StreamPage._meta.get_field('body') 194 195 comparison = self.comparison_class( 196 field, 197 StreamPage(body=StreamValue(field.stream_block, [ 198 ('text', "Content", '1'), 199 ('text', "Content Foo", '2'), 200 ('text', "Content Bar", '3'), 201 ])), 202 StreamPage(body=StreamValue(field.stream_block, [ 203 ('text', "Content", '1'), 204 ('text', "Content Bar", '3'), 205 ])), 206 ) 207 208 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Content</div>\n<div class="comparison__child-object deletion">Content Foo</div>\n<div class="comparison__child-object">Content Bar</div>') 209 self.assertIsInstance(comparison.htmldiff(), SafeString) 210 self.assertTrue(comparison.has_changed()) 211 212 def test_edit_block(self): 213 field = StreamPage._meta.get_field('body') 214 215 comparison = self.comparison_class( 216 field, 217 StreamPage(body=StreamValue(field.stream_block, [ 218 ('text', "Content", '1'), 219 ('text', "Content Foo", '2'), 220 ('text', "Content Bar", '3'), 221 ])), 222 StreamPage(body=StreamValue(field.stream_block, [ 223 ('text', "Content", '1'), 224 ('text', "Content Baz", '2'), 225 ('text', "Content Bar", '3'), 226 ])), 227 ) 228 229 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Content</div>\n<div class="comparison__child-object">Content <span class="deletion">Foo</span><span class="addition">Baz</span></div>\n<div class="comparison__child-object">Content Bar</div>') 230 self.assertIsInstance(comparison.htmldiff(), SafeString) 231 self.assertTrue(comparison.has_changed()) 232 233 def test_has_changed_richtext(self): 234 field = StreamPage._meta.get_field('body') 235 236 comparison = self.comparison_class( 237 field, 238 StreamPage(body=StreamValue(field.stream_block, [ 239 ('rich_text', "<b>Original</b> content", '1'), 240 ])), 241 StreamPage(body=StreamValue(field.stream_block, [ 242 ('rich_text', "Modified <i>content</i>", '1'), 243 ])), 244 ) 245 246 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object"><span class="deletion">Original</span><span class="addition">Modified</span> content</div>') 247 self.assertIsInstance(comparison.htmldiff(), SafeString) 248 self.assertTrue(comparison.has_changed()) 249 250 def test_htmldiff_escapes_value_on_change(self): 251 field = StreamPage._meta.get_field('body') 252 253 comparison = self.comparison_class( 254 field, 255 StreamPage(body=StreamValue(field.stream_block, [ 256 ('text', "I <b>really</b> like original<i>ish</i> content", '1'), 257 ])), 258 StreamPage(body=StreamValue(field.stream_block, [ 259 ('text', 'I <b>really</b> like evil code <script type="text/javascript">doSomethingBad();</script>', '1'), 260 ])), 261 ) 262 263 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">I <b>really</b> like <span class="deletion">original<i>ish</i> content</span><span class="addition">evil code <script type="text/javascript">doSomethingBad();</script></span></div>') 264 self.assertIsInstance(comparison.htmldiff(), SafeString) 265 266 def test_htmldiff_escapes_value_on_addition(self): 267 field = StreamPage._meta.get_field('body') 268 269 comparison = self.comparison_class( 270 field, 271 StreamPage(body=StreamValue(field.stream_block, [ 272 ('text', "Original <em>and unchanged</em> content", '1'), 273 ])), 274 StreamPage(body=StreamValue(field.stream_block, [ 275 ('text', "Original <em>and unchanged</em> content", '1'), 276 ('text', '<script type="text/javascript">doSomethingBad();</script>', '2'), 277 ])), 278 ) 279 280 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Original <em>and unchanged</em> content</div>\n<div class="comparison__child-object addition"><script type="text/javascript">doSomethingBad();</script></div>') 281 self.assertIsInstance(comparison.htmldiff(), SafeString) 282 283 def test_htmldiff_escapes_value_on_deletion(self): 284 field = StreamPage._meta.get_field('body') 285 286 comparison = self.comparison_class( 287 field, 288 StreamPage(body=StreamValue(field.stream_block, [ 289 ('text', "Original <em>and unchanged</em> content", '1'), 290 ('text', '<script type="text/javascript">doSomethingBad();</script>', '2'), 291 ])), 292 StreamPage(body=StreamValue(field.stream_block, [ 293 ('text', "Original <em>and unchanged</em> content", '1'), 294 ])), 295 ) 296 297 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Original <em>and unchanged</em> content</div>\n<div class="comparison__child-object deletion"><script type="text/javascript">doSomethingBad();</script></div>') 298 self.assertIsInstance(comparison.htmldiff(), SafeString) 299 300 def test_htmldiff_richtext_strips_tags_on_change(self): 301 field = StreamPage._meta.get_field('body') 302 303 comparison = self.comparison_class( 304 field, 305 StreamPage(body=StreamValue(field.stream_block, [ 306 ('rich_text', "I <b>really</b> like Wagtail <3", '1'), 307 ])), 308 StreamPage(body=StreamValue(field.stream_block, [ 309 ('rich_text', 'I <b>really</b> like evil code >_< <script type="text/javascript">doSomethingBad();</script>', '1'), 310 ])), 311 ) 312 313 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">I really like <span class="deletion">Wagtail <3</span><span class="addition">evil code >_< doSomethingBad();</span></div>') 314 self.assertIsInstance(comparison.htmldiff(), SafeString) 315 316 def test_htmldiff_richtext_strips_tags_on_addition(self): 317 field = StreamPage._meta.get_field('body') 318 319 comparison = self.comparison_class( 320 field, 321 StreamPage(body=StreamValue(field.stream_block, [ 322 ('rich_text', "Original <em>and unchanged</em> content", '1'), 323 ])), 324 StreamPage(body=StreamValue(field.stream_block, [ 325 ('rich_text', "Original <em>and unchanged</em> content", '1'), 326 ('rich_text', 'I <b>really</b> like evil code >_< <script type="text/javascript">doSomethingBad();</script>', '2'), 327 ])), 328 ) 329 330 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Original and unchanged content</div>\n<div class="comparison__child-object addition">I really like evil code >_< doSomethingBad();</div>') 331 self.assertIsInstance(comparison.htmldiff(), SafeString) 332 333 def test_htmldiff_richtext_strips_tags_on_deletion(self): 334 field = StreamPage._meta.get_field('body') 335 336 comparison = self.comparison_class( 337 field, 338 StreamPage(body=StreamValue(field.stream_block, [ 339 ('rich_text', "Original <em>and unchanged</em> content", '1'), 340 ('rich_text', 'I <b>really</b> like evil code >_< <script type="text/javascript">doSomethingBad();</script>', '2'), 341 ])), 342 StreamPage(body=StreamValue(field.stream_block, [ 343 ('rich_text', "Original <em>and unchanged</em> content", '1'), 344 ])), 345 ) 346 347 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Original and unchanged content</div>\n<div class="comparison__child-object deletion">I really like evil code >_< doSomethingBad();</div>') 348 self.assertIsInstance(comparison.htmldiff(), SafeString) 349 350 def test_htmldiff_raw_html_escapes_value_on_change(self): 351 field = StreamPage._meta.get_field('body') 352 353 comparison = self.comparison_class( 354 field, 355 StreamPage(body=StreamValue(field.stream_block, [ 356 ('raw_html', "Original<i>ish</i> content", '1'), 357 ])), 358 StreamPage(body=StreamValue(field.stream_block, [ 359 ('raw_html', '<script type="text/javascript">doSomethingBad();</script>', '1'), 360 ])), 361 ) 362 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object"><span class="deletion">Original<i>ish</i> content</span><span class="addition"><script type="text/javascript">doSomethingBad();</script></span></div>') 363 self.assertIsInstance(comparison.htmldiff(), SafeString) 364 365 def test_htmldiff_raw_html_escapes_value_on_addition(self): 366 field = StreamPage._meta.get_field('body') 367 368 comparison = self.comparison_class( 369 field, 370 StreamPage(body=StreamValue(field.stream_block, [ 371 ('raw_html', "Original <em>and unchanged</em> content", '1'), 372 ])), 373 StreamPage(body=StreamValue(field.stream_block, [ 374 ('raw_html', "Original <em>and unchanged</em> content", '1'), 375 ('raw_html', '<script type="text/javascript">doSomethingBad();</script>', '2'), 376 ])), 377 ) 378 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Original <em>and unchanged</em> content</div>\n<div class="comparison__child-object addition"><script type="text/javascript">doSomethingBad();</script></div>') 379 self.assertIsInstance(comparison.htmldiff(), SafeString) 380 381 def test_htmldiff_raw_html_escapes_value_on_deletion(self): 382 field = StreamPage._meta.get_field('body') 383 384 comparison = self.comparison_class( 385 field, 386 StreamPage(body=StreamValue(field.stream_block, [ 387 ('raw_html', "Original <em>and unchanged</em> content", '1'), 388 ('raw_html', '<script type="text/javascript">doSomethingBad();</script>', '2'), 389 ])), 390 StreamPage(body=StreamValue(field.stream_block, [ 391 ('raw_html', "Original <em>and unchanged</em> content", '1'), 392 ])), 393 ) 394 self.assertEqual(comparison.htmldiff(), '<div class="comparison__child-object">Original <em>and unchanged</em> content</div>\n<div class="comparison__child-object deletion"><script type="text/javascript">doSomethingBad();</script></div>') 395 self.assertIsInstance(comparison.htmldiff(), SafeString) 396 397 def test_compare_structblock(self): 398 field = StreamPage._meta.get_field('body') 399 400 comparison = self.comparison_class( 401 field, 402 StreamPage(body=StreamValue(field.stream_block, [ 403 ('product', {'name': 'a packet of rolos', 'price': '75p'}, '1'), 404 ])), 405 StreamPage(body=StreamValue(field.stream_block, [ 406 ('product', {'name': 'a packet of rolos', 'price': '85p'}, '1'), 407 ])), 408 ) 409 410 expected = """ 411 <div class="comparison__child-object"><dl> 412 <dt>Name</dt> 413 <dd>a packet of rolos</dd> 414 <dt>Price</dt> 415 <dd><span class="deletion">75p</span><span class="addition">85p</span></dd> 416 </dl></div> 417 """ 418 self.assertHTMLEqual(comparison.htmldiff(), expected) 419 self.assertIsInstance(comparison.htmldiff(), SafeString) 420 self.assertTrue(comparison.has_changed()) 421 422 def test_compare_nested_streamblock_uses_comparison_class(self): 423 field = StreamPage._meta.get_field('body') 424 stream_block = field.stream_block.child_blocks['books'] 425 comparison = self.comparison_class( 426 field, 427 StreamPage(body=StreamValue(field.stream_block, [ 428 ('books', StreamValue(stream_block, [('title', 'The Old Man and the Sea', '10')]), '1'), 429 ])), 430 StreamPage(body=StreamValue(field.stream_block, [ 431 ('books', StreamValue(stream_block, [('author', 'Oscar Wilde', '11')]), '1'), 432 ])), 433 ) 434 expected = """ 435 <div class="comparison__child-object"> 436 <div class="comparison__child-object addition">Oscar Wilde</div>\n 437 <div class="comparison__child-object deletion">The Old Man and the Sea</div> 438 </div> 439 """ 440 self.assertHTMLEqual(comparison.htmldiff(), expected) 441 self.assertIsInstance(comparison.htmldiff(), SafeString) 442 self.assertTrue(comparison.has_changed()) 443 444 def test_compare_imagechooserblock(self): 445 image_model = get_image_model() 446 test_image_1 = image_model.objects.create( 447 title="Test image 1", 448 file=get_test_image_file(), 449 ) 450 test_image_2 = image_model.objects.create( 451 title="Test image 2", 452 file=get_test_image_file(), 453 ) 454 455 field = StreamPage._meta.get_field('body') 456 457 comparison = self.comparison_class( 458 field, 459 StreamPage(body=StreamValue(field.stream_block, [ 460 ('image', test_image_1, '1'), 461 ])), 462 StreamPage(body=StreamValue(field.stream_block, [ 463 ('image', test_image_2, '1'), 464 ])), 465 ) 466 467 result = comparison.htmldiff() 468 self.assertIn('<div class="preview-image deletion">', result) 469 self.assertIn('alt="Test image 1"', result) 470 self.assertIn('<div class="preview-image addition">', result) 471 self.assertIn('alt="Test image 2"', result) 472 473 self.assertIsInstance(result, SafeString) 474 self.assertTrue(comparison.has_changed()) 475 476 477class TestChoiceFieldComparison(TestCase): 478 comparison_class = compare.ChoiceFieldComparison 479 480 def test_hasnt_changed(self): 481 comparison = self.comparison_class( 482 EventPage._meta.get_field('audience'), 483 EventPage(audience="public"), 484 EventPage(audience="public"), 485 ) 486 487 self.assertTrue(comparison.is_field) 488 self.assertFalse(comparison.is_child_relation) 489 self.assertEqual(comparison.field_label(), "Audience") 490 self.assertEqual(comparison.htmldiff(), 'Public') 491 self.assertIsInstance(comparison.htmldiff(), SafeString) 492 self.assertFalse(comparison.has_changed()) 493 494 def test_has_changed(self): 495 comparison = self.comparison_class( 496 EventPage._meta.get_field('audience'), 497 EventPage(audience="public"), 498 EventPage(audience="private"), 499 ) 500 501 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Public</span><span class="addition">Private</span>') 502 self.assertIsInstance(comparison.htmldiff(), SafeString) 503 self.assertTrue(comparison.has_changed()) 504 505 def test_from_none_to_value_only_shows_addition(self): 506 comparison = self.comparison_class( 507 EventPage._meta.get_field('audience'), 508 EventPage(audience=None), 509 EventPage(audience="private"), 510 ) 511 512 self.assertEqual(comparison.htmldiff(), '<span class="addition">Private</span>') 513 self.assertIsInstance(comparison.htmldiff(), SafeString) 514 self.assertTrue(comparison.has_changed()) 515 516 def test_from_value_to_none_only_shows_deletion(self): 517 comparison = self.comparison_class( 518 EventPage._meta.get_field('audience'), 519 EventPage(audience="public"), 520 EventPage(audience=None), 521 ) 522 523 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Public</span>') 524 self.assertIsInstance(comparison.htmldiff(), SafeString) 525 self.assertTrue(comparison.has_changed()) 526 527 528class TestTagsFieldComparison(TestCase): 529 comparison_class = compare.TagsFieldComparison 530 531 def test_hasnt_changed(self): 532 a = TaggedPage() 533 a.tags.add('wagtail') 534 a.tags.add('bird') 535 536 b = TaggedPage() 537 b.tags.add('wagtail') 538 b.tags.add('bird') 539 540 comparison = self.comparison_class(TaggedPage._meta.get_field('tags'), a, b) 541 542 self.assertTrue(comparison.is_field) 543 self.assertFalse(comparison.is_child_relation) 544 self.assertEqual(comparison.field_label(), "Tags") 545 self.assertEqual(comparison.htmldiff(), 'wagtail, bird') 546 self.assertIsInstance(comparison.htmldiff(), SafeString) 547 self.assertFalse(comparison.has_changed()) 548 549 def test_has_changed(self): 550 a = TaggedPage() 551 a.tags.add('wagtail') 552 a.tags.add('bird') 553 554 b = TaggedPage() 555 b.tags.add('wagtail') 556 b.tags.add('motacilla') 557 558 comparison = self.comparison_class(TaggedPage._meta.get_field('tags'), a, b) 559 560 self.assertEqual(comparison.htmldiff(), 'wagtail, <span class="deletion">bird</span>, <span class="addition">motacilla</span>') 561 self.assertIsInstance(comparison.htmldiff(), SafeString) 562 self.assertTrue(comparison.has_changed()) 563 564 565class TestM2MFieldComparison(TestCase): 566 fixtures = ['test.json'] 567 comparison_class = compare.M2MFieldComparison 568 569 def setUp(self): 570 self.meetings_category = EventCategory.objects.create(name='Meetings') 571 self.parties_category = EventCategory.objects.create(name='Parties') 572 self.holidays_category = EventCategory.objects.create(name='Holidays') 573 574 def test_hasnt_changed(self): 575 christmas_event = EventPage.objects.get(url_path='/home/events/christmas/') 576 saint_patrick_event = EventPage.objects.get(url_path='/home/events/saint-patrick/') 577 578 christmas_event.categories = [self.meetings_category, self.parties_category] 579 saint_patrick_event.categories = [self.meetings_category, self.parties_category] 580 581 comparison = self.comparison_class( 582 EventPage._meta.get_field('categories'), christmas_event, saint_patrick_event 583 ) 584 585 self.assertTrue(comparison.is_field) 586 self.assertFalse(comparison.is_child_relation) 587 self.assertEqual(comparison.field_label(), "Categories") 588 self.assertFalse(comparison.has_changed()) 589 self.assertEqual(comparison.htmldiff(), 'Meetings, Parties') 590 self.assertIsInstance(comparison.htmldiff(), SafeString) 591 592 def test_has_changed(self): 593 christmas_event = EventPage.objects.get(url_path='/home/events/christmas/') 594 saint_patrick_event = EventPage.objects.get(url_path='/home/events/saint-patrick/') 595 596 christmas_event.categories = [self.meetings_category, self.parties_category] 597 saint_patrick_event.categories = [self.meetings_category, self.holidays_category] 598 599 comparison = self.comparison_class( 600 EventPage._meta.get_field('categories'), christmas_event, saint_patrick_event 601 ) 602 603 self.assertTrue(comparison.has_changed()) 604 self.assertEqual(comparison.htmldiff(), 'Meetings, <span class="deletion">Parties</span>, <span class="addition">Holidays</span>') 605 self.assertIsInstance(comparison.htmldiff(), SafeString) 606 607 608class TestForeignObjectComparison(TestCase): 609 comparison_class = compare.ForeignObjectComparison 610 611 @classmethod 612 def setUpTestData(cls): 613 image_model = get_image_model() 614 cls.test_image_1 = image_model.objects.create( 615 title="Test image 1", 616 file=get_test_image_file(), 617 ) 618 cls.test_image_2 = image_model.objects.create( 619 title="Test image 2", 620 file=get_test_image_file(), 621 ) 622 623 def test_hasnt_changed(self): 624 comparison = self.comparison_class( 625 EventPage._meta.get_field('feed_image'), 626 EventPage(feed_image=self.test_image_1), 627 EventPage(feed_image=self.test_image_1), 628 ) 629 630 self.assertTrue(comparison.is_field) 631 self.assertFalse(comparison.is_child_relation) 632 self.assertEqual(comparison.field_label(), "Feed image") 633 self.assertEqual(comparison.htmldiff(), 'Test image 1') 634 self.assertIsInstance(comparison.htmldiff(), SafeString) 635 self.assertFalse(comparison.has_changed()) 636 637 def test_has_changed(self): 638 comparison = self.comparison_class( 639 EventPage._meta.get_field('feed_image'), 640 EventPage(feed_image=self.test_image_1), 641 EventPage(feed_image=self.test_image_2), 642 ) 643 644 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Test image 1</span><span class="addition">Test image 2</span>') 645 self.assertIsInstance(comparison.htmldiff(), SafeString) 646 self.assertTrue(comparison.has_changed()) 647 648 649class TestForeignObjectComparisonWithCustomPK(TestCase): 650 """ForeignObjectComparison works with models declaring a custom primary key field""" 651 652 comparison_class = compare.ForeignObjectComparison 653 654 @classmethod 655 def setUpTestData(cls): 656 ad1 = AdvertWithCustomPrimaryKey.objects.create( 657 advert_id='ad1', 658 text='Advert 1' 659 ) 660 ad2 = AdvertWithCustomPrimaryKey.objects.create( 661 advert_id='ad2', 662 text='Advert 2' 663 ) 664 cls.test_obj_1 = SnippetChooserModelWithCustomPrimaryKey.objects.create( 665 advertwithcustomprimarykey=ad1 666 ) 667 cls.test_obj_2 = SnippetChooserModelWithCustomPrimaryKey.objects.create( 668 advertwithcustomprimarykey=ad2 669 ) 670 671 def test_hasnt_changed(self): 672 comparison = self.comparison_class( 673 SnippetChooserModelWithCustomPrimaryKey._meta.get_field('advertwithcustomprimarykey'), 674 self.test_obj_1, 675 self.test_obj_1, 676 ) 677 678 self.assertTrue(comparison.is_field) 679 self.assertFalse(comparison.is_child_relation) 680 self.assertEqual(comparison.field_label(), 'Advertwithcustomprimarykey') 681 self.assertEqual(comparison.htmldiff(), 'Advert 1') 682 self.assertIsInstance(comparison.htmldiff(), SafeString) 683 self.assertFalse(comparison.has_changed()) 684 685 def test_has_changed(self): 686 comparison = self.comparison_class( 687 SnippetChooserModelWithCustomPrimaryKey._meta.get_field('advertwithcustomprimarykey'), 688 self.test_obj_1, 689 self.test_obj_2, 690 ) 691 692 self.assertEqual(comparison.htmldiff(), '<span class="deletion">Advert 1</span><span class="addition">Advert 2</span>') 693 self.assertIsInstance(comparison.htmldiff(), SafeString) 694 self.assertTrue(comparison.has_changed()) 695 696 697class TestChildRelationComparison(TestCase): 698 field_comparison_class = compare.FieldComparison 699 comparison_class = compare.ChildRelationComparison 700 701 def test_hasnt_changed(self): 702 # Two event pages with speaker called "Father Christmas". Neither of 703 # the speaker objects have an ID so this tests that the code can match 704 # the two together by field content. 705 event_page = EventPage(title="Event page", slug="event") 706 event_page.speakers.add(EventPageSpeaker( 707 first_name="Father", 708 last_name="Christmas", 709 )) 710 711 modified_event_page = EventPage(title="Event page", slug="event") 712 modified_event_page.speakers.add(EventPageSpeaker( 713 first_name="Father", 714 last_name="Christmas", 715 )) 716 717 comparison = self.comparison_class( 718 EventPage._meta.get_field('speaker'), 719 [ 720 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 721 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 722 ], 723 event_page, 724 modified_event_page, 725 ) 726 727 self.assertFalse(comparison.is_field) 728 self.assertTrue(comparison.is_child_relation) 729 self.assertEqual(comparison.field_label(), "Speaker") 730 self.assertFalse(comparison.has_changed()) 731 732 # Check mapping 733 objs_a = list(comparison.val_a.all()) 734 objs_b = list(comparison.val_b.all()) 735 map_forwards, map_backwards, added, deleted = comparison.get_mapping(objs_a, objs_b) 736 self.assertEqual(map_forwards, {0: 0}) 737 self.assertEqual(map_backwards, {0: 0}) 738 self.assertEqual(added, []) 739 self.assertEqual(deleted, []) 740 741 def test_has_changed(self): 742 # Father Christmas renamed to Santa Claus. And Father Ted added. 743 # Father Christmas should be mapped to Father Ted because they 744 # are most alike. Santa claus should be displayed as "new" 745 event_page = EventPage(title="Event page", slug="event") 746 event_page.speakers.add(EventPageSpeaker( 747 first_name="Father", 748 last_name="Christmas", 749 sort_order=0, 750 )) 751 752 modified_event_page = EventPage(title="Event page", slug="event") 753 modified_event_page.speakers.add(EventPageSpeaker( 754 first_name="Santa", 755 last_name="Claus", 756 sort_order=0, 757 )) 758 modified_event_page.speakers.add(EventPageSpeaker( 759 first_name="Father", 760 last_name="Ted", 761 sort_order=1, 762 )) 763 764 comparison = self.comparison_class( 765 EventPage._meta.get_field('speaker'), 766 [ 767 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 768 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 769 ], 770 event_page, 771 modified_event_page, 772 ) 773 774 self.assertFalse(comparison.is_field) 775 self.assertTrue(comparison.is_child_relation) 776 self.assertEqual(comparison.field_label(), "Speaker") 777 self.assertTrue(comparison.has_changed()) 778 779 # Check mapping 780 objs_a = list(comparison.val_a.all()) 781 objs_b = list(comparison.val_b.all()) 782 map_forwards, map_backwards, added, deleted = comparison.get_mapping(objs_a, objs_b) 783 self.assertEqual(map_forwards, {0: 1}) # Map Father Christmas to Father Ted 784 self.assertEqual(map_backwards, {1: 0}) # Map Father Ted ot Father Christmas 785 self.assertEqual(added, [0]) # Add Santa Claus 786 self.assertEqual(deleted, []) 787 788 def test_has_changed_with_same_id(self): 789 # Father Christmas renamed to Santa Claus, but this time the ID of the 790 # child object remained the same. It should now be detected as the same 791 # object 792 event_page = EventPage(title="Event page", slug="event") 793 event_page.speakers.add(EventPageSpeaker( 794 id=1, 795 first_name="Father", 796 last_name="Christmas", 797 sort_order=0, 798 )) 799 800 modified_event_page = EventPage(title="Event page", slug="event") 801 modified_event_page.speakers.add(EventPageSpeaker( 802 id=1, 803 first_name="Santa", 804 last_name="Claus", 805 sort_order=0, 806 )) 807 modified_event_page.speakers.add(EventPageSpeaker( 808 first_name="Father", 809 last_name="Ted", 810 sort_order=1, 811 )) 812 813 comparison = self.comparison_class( 814 EventPage._meta.get_field('speaker'), 815 [ 816 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 817 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 818 ], 819 event_page, 820 modified_event_page, 821 ) 822 823 self.assertFalse(comparison.is_field) 824 self.assertTrue(comparison.is_child_relation) 825 self.assertEqual(comparison.field_label(), "Speaker") 826 self.assertTrue(comparison.has_changed()) 827 828 # Check mapping 829 objs_a = list(comparison.val_a.all()) 830 objs_b = list(comparison.val_b.all()) 831 map_forwards, map_backwards, added, deleted = comparison.get_mapping(objs_a, objs_b) 832 self.assertEqual(map_forwards, {0: 0}) # Map Father Christmas to Santa Claus 833 self.assertEqual(map_backwards, {0: 0}) # Map Santa Claus to Father Christmas 834 self.assertEqual(added, [1]) # Add Father Ted 835 self.assertEqual(deleted, []) 836 837 def test_hasnt_changed_with_different_id(self): 838 # Both of the child objects have the same field content but have a 839 # different ID so they should be detected as separate objects 840 event_page = EventPage(title="Event page", slug="event") 841 event_page.speakers.add(EventPageSpeaker( 842 id=1, 843 first_name="Father", 844 last_name="Christmas", 845 )) 846 847 modified_event_page = EventPage(title="Event page", slug="event") 848 modified_event_page.speakers.add(EventPageSpeaker( 849 id=2, 850 first_name="Father", 851 last_name="Christmas", 852 )) 853 854 comparison = self.comparison_class( 855 EventPage._meta.get_field('speaker'), 856 [ 857 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 858 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 859 ], 860 event_page, 861 modified_event_page, 862 ) 863 864 self.assertFalse(comparison.is_field) 865 self.assertTrue(comparison.is_child_relation) 866 self.assertEqual(comparison.field_label(), "Speaker") 867 self.assertTrue(comparison.has_changed()) 868 869 # Check mapping 870 objs_a = list(comparison.val_a.all()) 871 objs_b = list(comparison.val_b.all()) 872 map_forwards, map_backwards, added, deleted = comparison.get_mapping(objs_a, objs_b) 873 self.assertEqual(map_forwards, {}) 874 self.assertEqual(map_backwards, {}) 875 self.assertEqual(added, [0]) # Add new Father Christmas 876 self.assertEqual(deleted, [0]) # Delete old Father Christmas 877 878 879class TestChildObjectComparison(TestCase): 880 field_comparison_class = compare.FieldComparison 881 comparison_class = compare.ChildObjectComparison 882 883 def test_same_object(self): 884 obj_a = EventPageSpeaker( 885 first_name="Father", 886 last_name="Christmas", 887 ) 888 889 obj_b = EventPageSpeaker( 890 first_name="Father", 891 last_name="Christmas", 892 ) 893 894 comparison = self.comparison_class( 895 EventPageSpeaker, 896 [ 897 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 898 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 899 ], 900 obj_a, 901 obj_b, 902 ) 903 904 self.assertFalse(comparison.is_addition()) 905 self.assertFalse(comparison.is_deletion()) 906 self.assertFalse(comparison.has_changed()) 907 self.assertEqual(comparison.get_position_change(), 0) 908 self.assertEqual(comparison.get_num_differences(), 0) 909 910 def test_different_object(self): 911 obj_a = EventPageSpeaker( 912 first_name="Father", 913 last_name="Christmas", 914 ) 915 916 obj_b = EventPageSpeaker( 917 first_name="Santa", 918 last_name="Claus", 919 ) 920 921 comparison = self.comparison_class( 922 EventPageSpeaker, 923 [ 924 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 925 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 926 ], 927 obj_a, 928 obj_b, 929 ) 930 931 self.assertFalse(comparison.is_addition()) 932 self.assertFalse(comparison.is_deletion()) 933 self.assertTrue(comparison.has_changed()) 934 self.assertEqual(comparison.get_position_change(), 0) 935 self.assertEqual(comparison.get_num_differences(), 2) 936 937 def test_moved_object(self): 938 obj_a = EventPageSpeaker( 939 first_name="Father", 940 last_name="Christmas", 941 sort_order=1, 942 ) 943 944 obj_b = EventPageSpeaker( 945 first_name="Father", 946 last_name="Christmas", 947 sort_order=5, 948 ) 949 950 comparison = self.comparison_class( 951 EventPageSpeaker, 952 [ 953 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 954 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 955 ], 956 obj_a, 957 obj_b, 958 ) 959 960 self.assertFalse(comparison.is_addition()) 961 self.assertFalse(comparison.is_deletion()) 962 self.assertFalse(comparison.has_changed()) 963 self.assertEqual(comparison.get_position_change(), 4) 964 self.assertEqual(comparison.get_num_differences(), 0) 965 966 def test_addition(self): 967 obj = EventPageSpeaker( 968 first_name="Father", 969 last_name="Christmas", 970 ) 971 972 comparison = self.comparison_class( 973 EventPageSpeaker, 974 [ 975 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 976 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 977 ], 978 None, 979 obj, 980 ) 981 982 self.assertTrue(comparison.is_addition()) 983 self.assertFalse(comparison.is_deletion()) 984 self.assertFalse(comparison.has_changed()) 985 self.assertIsNone(comparison.get_position_change(), 0) 986 self.assertEqual(comparison.get_num_differences(), 0) 987 988 def test_deletion(self): 989 obj = EventPageSpeaker( 990 first_name="Father", 991 last_name="Christmas", 992 ) 993 994 comparison = self.comparison_class( 995 EventPageSpeaker, 996 [ 997 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('first_name')), 998 partial(self.field_comparison_class, EventPageSpeaker._meta.get_field('last_name')), 999 ], 1000 obj, 1001 None, 1002 ) 1003 1004 self.assertFalse(comparison.is_addition()) 1005 self.assertTrue(comparison.is_deletion()) 1006 self.assertFalse(comparison.has_changed()) 1007 self.assertIsNone(comparison.get_position_change()) 1008 self.assertEqual(comparison.get_num_differences(), 0) 1009 1010 1011class TestChildRelationComparisonUsingPK(TestCase): 1012 """Test related objects can be compred if they do not use id for primary key""" 1013 1014 field_comparison_class = compare.FieldComparison 1015 comparison_class = compare.ChildRelationComparison 1016 1017 def test_has_changed_with_same_id(self): 1018 # Head Count was changed but the PK of the child object remained the same. 1019 # It should be detected as the same object 1020 1021 event_page = EventPage(title="Semi Finals", slug="semi-finals-2018") 1022 event_page.head_counts.add(HeadCountRelatedModelUsingPK( 1023 custom_id=1, 1024 head_count=22, 1025 )) 1026 1027 modified_event_page = EventPage(title="Semi Finals", slug="semi-finals-2018") 1028 modified_event_page.head_counts.add(HeadCountRelatedModelUsingPK( 1029 custom_id=1, 1030 head_count=23, 1031 )) 1032 modified_event_page.head_counts.add(HeadCountRelatedModelUsingPK( 1033 head_count=25, 1034 )) 1035 1036 comparison = self.comparison_class( 1037 EventPage._meta.get_field('head_counts'), 1038 [partial(self.field_comparison_class, HeadCountRelatedModelUsingPK._meta.get_field('head_count'))], 1039 event_page, 1040 modified_event_page, 1041 ) 1042 1043 self.assertFalse(comparison.is_field) 1044 self.assertTrue(comparison.is_child_relation) 1045 self.assertEqual(comparison.field_label(), 'Head counts') 1046 self.assertTrue(comparison.has_changed()) 1047 1048 # Check mapping 1049 objs_a = list(comparison.val_a.all()) 1050 objs_b = list(comparison.val_b.all()) 1051 map_forwards, map_backwards, added, deleted = comparison.get_mapping(objs_a, objs_b) 1052 self.assertEqual(map_forwards, {0: 0}) # map head count 22 to 23 1053 self.assertEqual(map_backwards, {0: 0}) # map head count 23 to 22 1054 self.assertEqual(added, [1]) # add second head count 1055 self.assertEqual(deleted, []) 1056 1057 def test_hasnt_changed_with_different_id(self): 1058 # Both of the child objects have the same field content but have a 1059 # different PK (ID) so they should be detected as separate objects 1060 event_page = EventPage(title="Finals", slug="finals-event-abc") 1061 event_page.head_counts.add(HeadCountRelatedModelUsingPK( 1062 custom_id=1, 1063 head_count=220 1064 )) 1065 1066 modified_event_page = EventPage(title="Finals", slug="finals-event-abc") 1067 modified_event_page.head_counts.add(HeadCountRelatedModelUsingPK( 1068 custom_id=2, 1069 head_count=220 1070 )) 1071 1072 comparison = self.comparison_class( 1073 EventPage._meta.get_field('head_counts'), 1074 [partial(self.field_comparison_class, HeadCountRelatedModelUsingPK._meta.get_field('head_count'))], 1075 event_page, 1076 modified_event_page, 1077 ) 1078 1079 self.assertFalse(comparison.is_field) 1080 self.assertTrue(comparison.is_child_relation) 1081 self.assertEqual(comparison.field_label(), "Head counts") 1082 self.assertTrue(comparison.has_changed()) 1083 1084 # Check mapping 1085 objs_a = list(comparison.val_a.all()) 1086 objs_b = list(comparison.val_b.all()) 1087 map_forwards, map_backwards, added, deleted = comparison.get_mapping(objs_a, objs_b) 1088 self.assertEqual(map_forwards, {}) 1089 self.assertEqual(map_backwards, {}) 1090 self.assertEqual(added, [0]) # Add new head count 1091 self.assertEqual(deleted, [0]) # Delete old head count 1092