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">&lt;script type=&quot;text/javascript&quot;&gt;doSomethingBad();&lt;/script&gt;</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 &lt;b&gt;really&lt;/b&gt; like <span class="deletion">original&lt;i&gt;ish&lt;/i&gt; content</span><span class="addition">evil code &lt;script type=&quot;text/javascript&quot;&gt;doSomethingBad();&lt;/script&gt;</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 &lt;em&gt;and unchanged&lt;/em&gt; content</div>\n<div class="comparison__child-object addition">&lt;script type=&quot;text/javascript&quot;&gt;doSomethingBad();&lt;/script&gt;</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 &lt;em&gt;and unchanged&lt;/em&gt; content</div>\n<div class="comparison__child-object deletion">&lt;script type=&quot;text/javascript&quot;&gt;doSomethingBad();&lt;/script&gt;</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 &lt;3", '1'),
307            ])),
308            StreamPage(body=StreamValue(field.stream_block, [
309                ('rich_text', 'I <b>really</b> like evil code &gt;_&lt; <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 &lt;3</span><span class="addition">evil code &gt;_&lt; 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 &gt;_&lt; <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 &gt;_&lt; 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 &gt;_&lt; <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 &gt;_&lt; 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&lt;i&gt;ish&lt;/i&gt; content</span><span class="addition">&lt;script type=&quot;text/javascript&quot;&gt;doSomethingBad();&lt;/script&gt;</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 &lt;em&gt;and unchanged&lt;/em&gt; content</div>\n<div class="comparison__child-object addition">&lt;script type=&quot;text/javascript&quot;&gt;doSomethingBad();&lt;/script&gt;</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 &lt;em&gt;and unchanged&lt;/em&gt; content</div>\n<div class="comparison__child-object deletion">&lt;script type=&quot;text/javascript&quot;&gt;doSomethingBad();&lt;/script&gt;</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