test_compare.py 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690
  1. from functools import partial
  2. from django.test import TestCase
  3. from django.utils.safestring import SafeString
  4. from wagtail.admin import compare
  5. from wagtail.blocks import StreamValue
  6. from wagtail.images import get_image_model
  7. from wagtail.images.tests.utils import get_test_image_file
  8. from wagtail.test.testapp.models import (
  9. AdvertWithCustomPrimaryKey,
  10. EventCategory,
  11. EventPage,
  12. EventPageSpeaker,
  13. HeadCountRelatedModelUsingPK,
  14. SimplePage,
  15. SnippetChooserModelWithCustomPrimaryKey,
  16. StreamPage,
  17. TaggedPage,
  18. )
  19. class TestFieldComparison(TestCase):
  20. comparison_class = compare.FieldComparison
  21. def test_hasnt_changed(self):
  22. comparison = self.comparison_class(
  23. SimplePage._meta.get_field("content"),
  24. SimplePage(content="Content"),
  25. SimplePage(content="Content"),
  26. )
  27. self.assertTrue(comparison.is_field)
  28. self.assertFalse(comparison.is_child_relation)
  29. self.assertEqual(comparison.field_label(), "Content")
  30. self.assertEqual(comparison.htmldiff(), "Content")
  31. self.assertIsInstance(comparison.htmldiff(), SafeString)
  32. self.assertFalse(comparison.has_changed())
  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. self.assertEqual(
  40. comparison.htmldiff(),
  41. '<span class="deletion">Original content</span><span class="addition">Modified content</span>',
  42. )
  43. self.assertIsInstance(comparison.htmldiff(), SafeString)
  44. self.assertTrue(comparison.has_changed())
  45. def test_htmldiff_escapes_value(self):
  46. comparison = self.comparison_class(
  47. SimplePage._meta.get_field("content"),
  48. SimplePage(content="Original content"),
  49. SimplePage(
  50. content='<script type="text/javascript">doSomethingBad();</script>'
  51. ),
  52. )
  53. self.assertEqual(
  54. comparison.htmldiff(),
  55. '<span class="deletion">Original content</span><span class="addition">&lt;script type=&quot;text/javascript&quot;&gt;doSomethingBad();&lt;/script&gt;</span>',
  56. )
  57. self.assertIsInstance(comparison.htmldiff(), SafeString)
  58. class TestTextFieldComparison(TestFieldComparison):
  59. comparison_class = compare.TextFieldComparison
  60. # Only change from FieldComparison is the HTML diff is performed on words
  61. # instead of the whole field value.
  62. def test_has_changed(self):
  63. comparison = self.comparison_class(
  64. SimplePage._meta.get_field("content"),
  65. SimplePage(content="Original content"),
  66. SimplePage(content="Modified content"),
  67. )
  68. self.assertEqual(
  69. comparison.htmldiff(),
  70. '<span class="deletion">Original</span><span class="addition">Modified</span> content',
  71. )
  72. self.assertIsInstance(comparison.htmldiff(), SafeString)
  73. self.assertTrue(comparison.has_changed())
  74. def test_from_none_to_value_only_shows_addition(self):
  75. comparison = self.comparison_class(
  76. SimplePage._meta.get_field("content"),
  77. SimplePage(content=None),
  78. SimplePage(content="Added content"),
  79. )
  80. self.assertEqual(
  81. comparison.htmldiff(), '<span class="addition">Added content</span>'
  82. )
  83. self.assertIsInstance(comparison.htmldiff(), SafeString)
  84. self.assertTrue(comparison.has_changed())
  85. def test_from_value_to_none_only_shows_deletion(self):
  86. comparison = self.comparison_class(
  87. SimplePage._meta.get_field("content"),
  88. SimplePage(content="Removed content"),
  89. SimplePage(content=None),
  90. )
  91. self.assertEqual(
  92. comparison.htmldiff(), '<span class="deletion">Removed content</span>'
  93. )
  94. self.assertIsInstance(comparison.htmldiff(), SafeString)
  95. self.assertTrue(comparison.has_changed())
  96. class TestRichTextFieldComparison(TestFieldComparison):
  97. comparison_class = compare.RichTextFieldComparison
  98. # Only change from FieldComparison is the HTML diff is performed on words
  99. # instead of the whole field value.
  100. def test_has_changed(self):
  101. comparison = self.comparison_class(
  102. SimplePage._meta.get_field("content"),
  103. SimplePage(content="Original content"),
  104. SimplePage(content="Modified content"),
  105. )
  106. self.assertEqual(
  107. comparison.htmldiff(),
  108. '<span class="deletion">Original</span><span class="addition">Modified</span> content',
  109. )
  110. self.assertIsInstance(comparison.htmldiff(), SafeString)
  111. self.assertTrue(comparison.has_changed())
  112. # Only change from FieldComparison is that this comparison disregards HTML tags
  113. def test_has_changed_html(self):
  114. comparison = self.comparison_class(
  115. SimplePage._meta.get_field("content"),
  116. SimplePage(content="<b>Original</b> content"),
  117. SimplePage(content="Modified <i>content</i>"),
  118. )
  119. self.assertEqual(
  120. comparison.htmldiff(),
  121. '<span class="deletion">Original</span><span class="addition">Modified</span> content',
  122. )
  123. self.assertIsInstance(comparison.htmldiff(), SafeString)
  124. self.assertTrue(comparison.has_changed())
  125. def test_htmldiff_escapes_value(self):
  126. # Need to override this one as the HTML tags are stripped by RichTextFieldComparison
  127. comparison = self.comparison_class(
  128. SimplePage._meta.get_field("content"),
  129. SimplePage(content="Original content"),
  130. SimplePage(
  131. content='Do something good. <script type="text/javascript">doSomethingBad();</script>'
  132. ),
  133. )
  134. self.assertEqual(
  135. comparison.htmldiff(),
  136. '<span class="deletion">Original content</span><span class="addition">Do something good.</span>',
  137. )
  138. self.assertIsInstance(comparison.htmldiff(), SafeString)
  139. class TestStreamFieldComparison(TestCase):
  140. comparison_class = compare.StreamFieldComparison
  141. def test_hasnt_changed(self):
  142. field = StreamPage._meta.get_field("body")
  143. comparison = self.comparison_class(
  144. field,
  145. StreamPage(
  146. body=StreamValue(
  147. field.stream_block,
  148. [
  149. ("text", "Content", "1"),
  150. ],
  151. )
  152. ),
  153. StreamPage(
  154. body=StreamValue(
  155. field.stream_block,
  156. [
  157. ("text", "Content", "1"),
  158. ],
  159. )
  160. ),
  161. )
  162. self.assertTrue(comparison.is_field)
  163. self.assertFalse(comparison.is_child_relation)
  164. self.assertEqual(comparison.field_label(), "Body")
  165. self.assertEqual(
  166. comparison.htmldiff(), '<div class="comparison__child-object">Content</div>'
  167. )
  168. self.assertIsInstance(comparison.htmldiff(), SafeString)
  169. self.assertFalse(comparison.has_changed())
  170. def test_has_changed(self):
  171. field = StreamPage._meta.get_field("body")
  172. comparison = self.comparison_class(
  173. field,
  174. StreamPage(
  175. body=StreamValue(
  176. field.stream_block,
  177. [
  178. ("text", "Original content", "1"),
  179. ],
  180. )
  181. ),
  182. StreamPage(
  183. body=StreamValue(
  184. field.stream_block,
  185. [
  186. ("text", "Modified content", "1"),
  187. ],
  188. )
  189. ),
  190. )
  191. self.assertEqual(
  192. comparison.htmldiff(),
  193. '<div class="comparison__child-object"><span class="deletion">Original</span><span class="addition">Modified</span> content</div>',
  194. )
  195. self.assertIsInstance(comparison.htmldiff(), SafeString)
  196. self.assertTrue(comparison.has_changed())
  197. def test_add_block(self):
  198. field = StreamPage._meta.get_field("body")
  199. comparison = self.comparison_class(
  200. field,
  201. StreamPage(
  202. body=StreamValue(
  203. field.stream_block,
  204. [
  205. ("text", "Content", "1"),
  206. ],
  207. )
  208. ),
  209. StreamPage(
  210. body=StreamValue(
  211. field.stream_block,
  212. [
  213. ("text", "Content", "1"),
  214. ("text", "New Content", "2"),
  215. ],
  216. )
  217. ),
  218. )
  219. self.assertEqual(
  220. comparison.htmldiff(),
  221. '<div class="comparison__child-object">Content</div>\n<div class="comparison__child-object addition">New Content</div>',
  222. )
  223. self.assertIsInstance(comparison.htmldiff(), SafeString)
  224. self.assertTrue(comparison.has_changed())
  225. def test_delete_block(self):
  226. field = StreamPage._meta.get_field("body")
  227. comparison = self.comparison_class(
  228. field,
  229. StreamPage(
  230. body=StreamValue(
  231. field.stream_block,
  232. [
  233. ("text", "Content", "1"),
  234. ("text", "Content Foo", "2"),
  235. ("text", "Content Bar", "3"),
  236. ],
  237. )
  238. ),
  239. StreamPage(
  240. body=StreamValue(
  241. field.stream_block,
  242. [
  243. ("text", "Content", "1"),
  244. ("text", "Content Bar", "3"),
  245. ],
  246. )
  247. ),
  248. )
  249. self.assertEqual(
  250. comparison.htmldiff(),
  251. '<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>',
  252. )
  253. self.assertIsInstance(comparison.htmldiff(), SafeString)
  254. self.assertTrue(comparison.has_changed())
  255. def test_edit_block(self):
  256. field = StreamPage._meta.get_field("body")
  257. comparison = self.comparison_class(
  258. field,
  259. StreamPage(
  260. body=StreamValue(
  261. field.stream_block,
  262. [
  263. ("text", "Content", "1"),
  264. ("text", "Content Foo", "2"),
  265. ("text", "Content Bar", "3"),
  266. ],
  267. )
  268. ),
  269. StreamPage(
  270. body=StreamValue(
  271. field.stream_block,
  272. [
  273. ("text", "Content", "1"),
  274. ("text", "Content Baz", "2"),
  275. ("text", "Content Bar", "3"),
  276. ],
  277. )
  278. ),
  279. )
  280. self.assertEqual(
  281. comparison.htmldiff(),
  282. '<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>',
  283. )
  284. self.assertIsInstance(comparison.htmldiff(), SafeString)
  285. self.assertTrue(comparison.has_changed())
  286. def test_has_changed_richtext(self):
  287. field = StreamPage._meta.get_field("body")
  288. comparison = self.comparison_class(
  289. field,
  290. StreamPage(
  291. body=StreamValue(
  292. field.stream_block,
  293. [
  294. ("rich_text", "<b>Original</b> content", "1"),
  295. ],
  296. )
  297. ),
  298. StreamPage(
  299. body=StreamValue(
  300. field.stream_block,
  301. [
  302. ("rich_text", "Modified <i>content</i>", "1"),
  303. ],
  304. )
  305. ),
  306. )
  307. self.assertEqual(
  308. comparison.htmldiff(),
  309. '<div class="comparison__child-object"><span class="deletion">Original</span><span class="addition">Modified</span> content</div>',
  310. )
  311. self.assertIsInstance(comparison.htmldiff(), SafeString)
  312. self.assertTrue(comparison.has_changed())
  313. def test_htmldiff_escapes_value_on_change(self):
  314. field = StreamPage._meta.get_field("body")
  315. comparison = self.comparison_class(
  316. field,
  317. StreamPage(
  318. body=StreamValue(
  319. field.stream_block,
  320. [
  321. (
  322. "text",
  323. "I <b>really</b> like original<i>ish</i> content",
  324. "1",
  325. ),
  326. ],
  327. )
  328. ),
  329. StreamPage(
  330. body=StreamValue(
  331. field.stream_block,
  332. [
  333. (
  334. "text",
  335. 'I <b>really</b> like evil code <script type="text/javascript">doSomethingBad();</script>',
  336. "1",
  337. ),
  338. ],
  339. )
  340. ),
  341. )
  342. self.assertEqual(
  343. comparison.htmldiff(),
  344. '<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>',
  345. )
  346. self.assertIsInstance(comparison.htmldiff(), SafeString)
  347. def test_htmldiff_escapes_value_on_addition(self):
  348. field = StreamPage._meta.get_field("body")
  349. comparison = self.comparison_class(
  350. field,
  351. StreamPage(
  352. body=StreamValue(
  353. field.stream_block,
  354. [
  355. ("text", "Original <em>and unchanged</em> content", "1"),
  356. ],
  357. )
  358. ),
  359. StreamPage(
  360. body=StreamValue(
  361. field.stream_block,
  362. [
  363. ("text", "Original <em>and unchanged</em> content", "1"),
  364. (
  365. "text",
  366. '<script type="text/javascript">doSomethingBad();</script>',
  367. "2",
  368. ),
  369. ],
  370. )
  371. ),
  372. )
  373. self.assertEqual(
  374. comparison.htmldiff(),
  375. '<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>',
  376. )
  377. self.assertIsInstance(comparison.htmldiff(), SafeString)
  378. def test_htmldiff_escapes_value_on_deletion(self):
  379. field = StreamPage._meta.get_field("body")
  380. comparison = self.comparison_class(
  381. field,
  382. StreamPage(
  383. body=StreamValue(
  384. field.stream_block,
  385. [
  386. ("text", "Original <em>and unchanged</em> content", "1"),
  387. (
  388. "text",
  389. '<script type="text/javascript">doSomethingBad();</script>',
  390. "2",
  391. ),
  392. ],
  393. )
  394. ),
  395. StreamPage(
  396. body=StreamValue(
  397. field.stream_block,
  398. [
  399. ("text", "Original <em>and unchanged</em> content", "1"),
  400. ],
  401. )
  402. ),
  403. )
  404. self.assertEqual(
  405. comparison.htmldiff(),
  406. '<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>',
  407. )
  408. self.assertIsInstance(comparison.htmldiff(), SafeString)
  409. def test_htmldiff_richtext_strips_tags_on_change(self):
  410. field = StreamPage._meta.get_field("body")
  411. comparison = self.comparison_class(
  412. field,
  413. StreamPage(
  414. body=StreamValue(
  415. field.stream_block,
  416. [
  417. ("rich_text", "I <b>really</b> like Wagtail &lt;3", "1"),
  418. ],
  419. )
  420. ),
  421. StreamPage(
  422. body=StreamValue(
  423. field.stream_block,
  424. [
  425. (
  426. "rich_text",
  427. 'I <b>really</b> like evil code &gt;_&lt; <script type="text/javascript">doSomethingBad();</script>',
  428. "1",
  429. ),
  430. ],
  431. )
  432. ),
  433. )
  434. self.assertEqual(
  435. comparison.htmldiff(),
  436. '<div class="comparison__child-object">I really like <span class="deletion">Wagtail &lt;3</span><span class="addition">evil code &gt;_&lt;</span></div>',
  437. )
  438. self.assertIsInstance(comparison.htmldiff(), SafeString)
  439. def test_htmldiff_richtext_strips_tags_on_addition(self):
  440. field = StreamPage._meta.get_field("body")
  441. comparison = self.comparison_class(
  442. field,
  443. StreamPage(
  444. body=StreamValue(
  445. field.stream_block,
  446. [
  447. ("rich_text", "Original <em>and unchanged</em> content", "1"),
  448. ],
  449. )
  450. ),
  451. StreamPage(
  452. body=StreamValue(
  453. field.stream_block,
  454. [
  455. ("rich_text", "Original <em>and unchanged</em> content", "1"),
  456. (
  457. "rich_text",
  458. 'I <b>really</b> like evil code &gt;_&lt; <script type="text/javascript">doSomethingBad();</script>',
  459. "2",
  460. ),
  461. ],
  462. )
  463. ),
  464. )
  465. self.assertEqual(
  466. comparison.htmldiff(),
  467. '<div class="comparison__child-object">Original and unchanged content</div>\n<div class="comparison__child-object addition">I really like evil code &gt;_&lt;</div>',
  468. )
  469. self.assertIsInstance(comparison.htmldiff(), SafeString)
  470. def test_htmldiff_richtext_strips_tags_on_deletion(self):
  471. field = StreamPage._meta.get_field("body")
  472. comparison = self.comparison_class(
  473. field,
  474. StreamPage(
  475. body=StreamValue(
  476. field.stream_block,
  477. [
  478. ("rich_text", "Original <em>and unchanged</em> content", "1"),
  479. (
  480. "rich_text",
  481. 'I <b>really</b> like evil code &gt;_&lt; <script type="text/javascript">doSomethingBad();</script>',
  482. "2",
  483. ),
  484. ],
  485. )
  486. ),
  487. StreamPage(
  488. body=StreamValue(
  489. field.stream_block,
  490. [
  491. ("rich_text", "Original <em>and unchanged</em> content", "1"),
  492. ],
  493. )
  494. ),
  495. )
  496. self.assertEqual(
  497. comparison.htmldiff(),
  498. '<div class="comparison__child-object">Original and unchanged content</div>\n<div class="comparison__child-object deletion">I really like evil code &gt;_&lt;</div>',
  499. )
  500. self.assertIsInstance(comparison.htmldiff(), SafeString)
  501. def test_htmldiff_raw_html_escapes_value_on_change(self):
  502. field = StreamPage._meta.get_field("body")
  503. comparison = self.comparison_class(
  504. field,
  505. StreamPage(
  506. body=StreamValue(
  507. field.stream_block,
  508. [
  509. ("raw_html", "Original<i>ish</i> content", "1"),
  510. ],
  511. )
  512. ),
  513. StreamPage(
  514. body=StreamValue(
  515. field.stream_block,
  516. [
  517. (
  518. "raw_html",
  519. '<script type="text/javascript">doSomethingBad();</script>',
  520. "1",
  521. ),
  522. ],
  523. )
  524. ),
  525. )
  526. self.assertEqual(
  527. comparison.htmldiff(),
  528. '<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>',
  529. )
  530. self.assertIsInstance(comparison.htmldiff(), SafeString)
  531. def test_htmldiff_raw_html_escapes_value_on_addition(self):
  532. field = StreamPage._meta.get_field("body")
  533. comparison = self.comparison_class(
  534. field,
  535. StreamPage(
  536. body=StreamValue(
  537. field.stream_block,
  538. [
  539. ("raw_html", "Original <em>and unchanged</em> content", "1"),
  540. ],
  541. )
  542. ),
  543. StreamPage(
  544. body=StreamValue(
  545. field.stream_block,
  546. [
  547. ("raw_html", "Original <em>and unchanged</em> content", "1"),
  548. (
  549. "raw_html",
  550. '<script type="text/javascript">doSomethingBad();</script>',
  551. "2",
  552. ),
  553. ],
  554. )
  555. ),
  556. )
  557. self.assertEqual(
  558. comparison.htmldiff(),
  559. '<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>',
  560. )
  561. self.assertIsInstance(comparison.htmldiff(), SafeString)
  562. def test_htmldiff_raw_html_escapes_value_on_deletion(self):
  563. field = StreamPage._meta.get_field("body")
  564. comparison = self.comparison_class(
  565. field,
  566. StreamPage(
  567. body=StreamValue(
  568. field.stream_block,
  569. [
  570. ("raw_html", "Original <em>and unchanged</em> content", "1"),
  571. (
  572. "raw_html",
  573. '<script type="text/javascript">doSomethingBad();</script>',
  574. "2",
  575. ),
  576. ],
  577. )
  578. ),
  579. StreamPage(
  580. body=StreamValue(
  581. field.stream_block,
  582. [
  583. ("raw_html", "Original <em>and unchanged</em> content", "1"),
  584. ],
  585. )
  586. ),
  587. )
  588. self.assertEqual(
  589. comparison.htmldiff(),
  590. '<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>',
  591. )
  592. self.assertIsInstance(comparison.htmldiff(), SafeString)
  593. def test_compare_structblock(self):
  594. field = StreamPage._meta.get_field("body")
  595. comparison = self.comparison_class(
  596. field,
  597. StreamPage(
  598. body=StreamValue(
  599. field.stream_block,
  600. [
  601. ("product", {"name": "a packet of rolos", "price": "75p"}, "1"),
  602. ],
  603. )
  604. ),
  605. StreamPage(
  606. body=StreamValue(
  607. field.stream_block,
  608. [
  609. ("product", {"name": "a packet of rolos", "price": "85p"}, "1"),
  610. ],
  611. )
  612. ),
  613. )
  614. expected = """
  615. <div class="comparison__child-object"><dl>
  616. <dt>Name</dt>
  617. <dd>a packet of rolos</dd>
  618. <dt>Price</dt>
  619. <dd><span class="deletion">75p</span><span class="addition">85p</span></dd>
  620. </dl></div>
  621. """
  622. self.assertHTMLEqual(comparison.htmldiff(), expected)
  623. self.assertIsInstance(comparison.htmldiff(), SafeString)
  624. self.assertTrue(comparison.has_changed())
  625. def test_compare_listblock(self):
  626. field = StreamPage._meta.get_field("body")
  627. block = field.stream_block.child_blocks["title_list"]
  628. block_val = block.to_python(
  629. [
  630. {
  631. "type": "item",
  632. "value": "foo",
  633. "id": "11111111-1111-1111-1111-111111111111",
  634. },
  635. {
  636. "type": "item",
  637. "value": "bar",
  638. "id": "22222222-2222-2222-2222-222222222222",
  639. },
  640. ]
  641. )
  642. block_val_2 = block.to_python(
  643. [
  644. {
  645. "type": "item",
  646. "value": "bard",
  647. "id": "22222222-2222-2222-2222-222222222222",
  648. },
  649. {
  650. "type": "item",
  651. "value": "food",
  652. "id": "11111111-1111-1111-1111-111111111111",
  653. },
  654. ]
  655. )
  656. comparison = self.comparison_class(
  657. field,
  658. StreamPage(
  659. body=StreamValue(
  660. field.stream_block,
  661. [
  662. ("title_list", block_val, "1"),
  663. ],
  664. )
  665. ),
  666. StreamPage(
  667. body=StreamValue(
  668. field.stream_block,
  669. [
  670. ("title_list", block_val_2, "1"),
  671. ],
  672. )
  673. ),
  674. )
  675. htmldiff = comparison.htmldiff()
  676. expected = """
  677. <div class="comparison__child-object">
  678. <div class="comparison__child-object">
  679. <span class="deletion">bar</span>
  680. <span class="addition">bard</span>
  681. </div>\n
  682. <div class="comparison__child-object">
  683. <span class="deletion">foo</span>
  684. <span class="addition">food</span>
  685. </div>
  686. </div>
  687. """
  688. self.assertHTMLEqual(htmldiff, expected)
  689. self.assertIsInstance(htmldiff, SafeString)
  690. self.assertTrue(comparison.has_changed())
  691. def test_compare_listblock_old_format(self):
  692. field = StreamPage._meta.get_field("body")
  693. block = field.stream_block.child_blocks["title_list"]
  694. no_diff = """
  695. <div class="comparison__child-object">
  696. <div class="comparison__child-object">foo</div>\n
  697. <div class="comparison__child-object">bar</div>
  698. </div>
  699. """
  700. edit_and_add_diff = """
  701. <div class="comparison__child-object">
  702. <div class="comparison__child-object">
  703. foo
  704. </div>\n
  705. <div class="comparison__child-object">
  706. <span class="deletion">bar</span>
  707. <span class="addition">bap</span>
  708. </div>\n
  709. <div class="comparison__child-object addition">baz</div>
  710. </div>
  711. """
  712. edit_and_add_diff_reversed = """
  713. <div class="comparison__child-object">
  714. <div class="comparison__child-object">
  715. <span class="deletion">foo</span>
  716. <span class="addition">fo</span>
  717. </div>\n
  718. <div class="comparison__child-object">bar</div>\n
  719. <div class="comparison__child-object deletion">baz</div>
  720. </div>
  721. """
  722. old_format_listblock_fixtures = [
  723. (["foo", "bar"], ["foo", "bar"], no_diff),
  724. (["foo", "bar"], ["foo", "bap", "baz"], edit_and_add_diff),
  725. (["foo", "bar", "baz"], ["fo", "bar"], edit_and_add_diff_reversed),
  726. ]
  727. for list_1, list_2, expected_diff in old_format_listblock_fixtures:
  728. with self.subTest(list_1=list_1, list_2=list_2):
  729. block_val = block.to_python(list_1)
  730. block_val_2 = block.to_python(list_2)
  731. comparison = self.comparison_class(
  732. field,
  733. StreamPage(
  734. body=StreamValue(
  735. field.stream_block,
  736. [
  737. ("title_list", block_val, "1"),
  738. ],
  739. )
  740. ),
  741. StreamPage(
  742. body=StreamValue(
  743. field.stream_block,
  744. [
  745. ("title_list", block_val_2, "1"),
  746. ],
  747. )
  748. ),
  749. )
  750. htmldiff = comparison.htmldiff()
  751. self.assertHTMLEqual(htmldiff, expected_diff)
  752. self.assertIsInstance(htmldiff, SafeString)
  753. self.assertTrue(comparison.has_changed())
  754. def test_compare_nested_streamblock_uses_comparison_class(self):
  755. field = StreamPage._meta.get_field("body")
  756. stream_block = field.stream_block.child_blocks["books"]
  757. comparison = self.comparison_class(
  758. field,
  759. StreamPage(
  760. body=StreamValue(
  761. field.stream_block,
  762. [
  763. (
  764. "books",
  765. StreamValue(
  766. stream_block,
  767. [("title", "The Old Man and the Sea", "10")],
  768. ),
  769. "1",
  770. ),
  771. ],
  772. )
  773. ),
  774. StreamPage(
  775. body=StreamValue(
  776. field.stream_block,
  777. [
  778. (
  779. "books",
  780. StreamValue(
  781. stream_block, [("author", "Oscar Wilde", "11")]
  782. ),
  783. "1",
  784. ),
  785. ],
  786. )
  787. ),
  788. )
  789. expected = """
  790. <div class="comparison__child-object">
  791. <div class="comparison__child-object addition">Oscar Wilde</div>\n
  792. <div class="comparison__child-object deletion">The Old Man and the Sea</div>
  793. </div>
  794. """
  795. self.assertHTMLEqual(comparison.htmldiff(), expected)
  796. self.assertIsInstance(comparison.htmldiff(), SafeString)
  797. self.assertTrue(comparison.has_changed())
  798. def test_compare_imagechooserblock(self):
  799. image_model = get_image_model()
  800. test_image_1 = image_model.objects.create(
  801. title="Test image 1",
  802. file=get_test_image_file(),
  803. )
  804. test_image_2 = image_model.objects.create(
  805. title="Test image 2",
  806. file=get_test_image_file(),
  807. )
  808. field = StreamPage._meta.get_field("body")
  809. comparison = self.comparison_class(
  810. field,
  811. StreamPage(
  812. body=StreamValue(
  813. field.stream_block,
  814. [
  815. ("image", test_image_1, "1"),
  816. ],
  817. )
  818. ),
  819. StreamPage(
  820. body=StreamValue(
  821. field.stream_block,
  822. [
  823. ("image", test_image_2, "1"),
  824. ],
  825. )
  826. ),
  827. )
  828. result = comparison.htmldiff()
  829. self.assertIn('<div class="preview-image deletion">', result)
  830. self.assertIn('alt="Test image 1"', result)
  831. self.assertIn('<div class="preview-image addition">', result)
  832. self.assertIn('alt="Test image 2"', result)
  833. self.assertIsInstance(result, SafeString)
  834. self.assertTrue(comparison.has_changed())
  835. class TestChoiceFieldComparison(TestCase):
  836. comparison_class = compare.ChoiceFieldComparison
  837. def test_hasnt_changed(self):
  838. comparison = self.comparison_class(
  839. EventPage._meta.get_field("audience"),
  840. EventPage(audience="public"),
  841. EventPage(audience="public"),
  842. )
  843. self.assertTrue(comparison.is_field)
  844. self.assertFalse(comparison.is_child_relation)
  845. self.assertEqual(comparison.field_label(), "Audience")
  846. self.assertEqual(comparison.htmldiff(), "Public")
  847. self.assertIsInstance(comparison.htmldiff(), SafeString)
  848. self.assertFalse(comparison.has_changed())
  849. def test_has_changed(self):
  850. comparison = self.comparison_class(
  851. EventPage._meta.get_field("audience"),
  852. EventPage(audience="public"),
  853. EventPage(audience="private"),
  854. )
  855. self.assertEqual(
  856. comparison.htmldiff(),
  857. '<span class="deletion">Public</span><span class="addition">Private</span>',
  858. )
  859. self.assertIsInstance(comparison.htmldiff(), SafeString)
  860. self.assertTrue(comparison.has_changed())
  861. def test_from_none_to_value_only_shows_addition(self):
  862. comparison = self.comparison_class(
  863. EventPage._meta.get_field("audience"),
  864. EventPage(audience=None),
  865. EventPage(audience="private"),
  866. )
  867. self.assertEqual(comparison.htmldiff(), '<span class="addition">Private</span>')
  868. self.assertIsInstance(comparison.htmldiff(), SafeString)
  869. self.assertTrue(comparison.has_changed())
  870. def test_from_value_to_none_only_shows_deletion(self):
  871. comparison = self.comparison_class(
  872. EventPage._meta.get_field("audience"),
  873. EventPage(audience="public"),
  874. EventPage(audience=None),
  875. )
  876. self.assertEqual(comparison.htmldiff(), '<span class="deletion">Public</span>')
  877. self.assertIsInstance(comparison.htmldiff(), SafeString)
  878. self.assertTrue(comparison.has_changed())
  879. class TestTagsFieldComparison(TestCase):
  880. comparison_class = compare.TagsFieldComparison
  881. def test_hasnt_changed(self):
  882. a = TaggedPage()
  883. a.tags.add("wagtail")
  884. a.tags.add("bird")
  885. b = TaggedPage()
  886. b.tags.add("wagtail")
  887. b.tags.add("bird")
  888. comparison = self.comparison_class(TaggedPage._meta.get_field("tags"), a, b)
  889. self.assertTrue(comparison.is_field)
  890. self.assertFalse(comparison.is_child_relation)
  891. self.assertEqual(comparison.field_label(), "Tags")
  892. self.assertEqual(comparison.htmldiff(), "wagtail, bird")
  893. self.assertIsInstance(comparison.htmldiff(), SafeString)
  894. self.assertFalse(comparison.has_changed())
  895. def test_has_changed(self):
  896. a = TaggedPage()
  897. a.tags.add("wagtail")
  898. a.tags.add("bird")
  899. b = TaggedPage()
  900. b.tags.add("wagtail")
  901. b.tags.add("motacilla")
  902. comparison = self.comparison_class(TaggedPage._meta.get_field("tags"), a, b)
  903. self.assertEqual(
  904. comparison.htmldiff(),
  905. 'wagtail, <span class="deletion">bird</span>, <span class="addition">motacilla</span>',
  906. )
  907. self.assertIsInstance(comparison.htmldiff(), SafeString)
  908. self.assertTrue(comparison.has_changed())
  909. class TestM2MFieldComparison(TestCase):
  910. fixtures = ["test.json"]
  911. comparison_class = compare.M2MFieldComparison
  912. def setUp(self):
  913. self.meetings_category = EventCategory.objects.create(name="Meetings")
  914. self.parties_category = EventCategory.objects.create(name="Parties")
  915. self.holidays_category = EventCategory.objects.create(name="Holidays")
  916. def test_hasnt_changed(self):
  917. christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
  918. saint_patrick_event = EventPage.objects.get(
  919. url_path="/home/events/saint-patrick/"
  920. )
  921. christmas_event.categories = [self.meetings_category, self.parties_category]
  922. saint_patrick_event.categories = [self.meetings_category, self.parties_category]
  923. comparison = self.comparison_class(
  924. EventPage._meta.get_field("categories"),
  925. christmas_event,
  926. saint_patrick_event,
  927. )
  928. self.assertTrue(comparison.is_field)
  929. self.assertFalse(comparison.is_child_relation)
  930. self.assertEqual(comparison.field_label(), "Categories")
  931. self.assertFalse(comparison.has_changed())
  932. self.assertEqual(comparison.htmldiff(), "Meetings, Parties")
  933. self.assertIsInstance(comparison.htmldiff(), SafeString)
  934. def test_has_changed(self):
  935. christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
  936. saint_patrick_event = EventPage.objects.get(
  937. url_path="/home/events/saint-patrick/"
  938. )
  939. christmas_event.categories = [self.meetings_category, self.parties_category]
  940. saint_patrick_event.categories = [
  941. self.meetings_category,
  942. self.holidays_category,
  943. ]
  944. comparison = self.comparison_class(
  945. EventPage._meta.get_field("categories"),
  946. christmas_event,
  947. saint_patrick_event,
  948. )
  949. self.assertTrue(comparison.has_changed())
  950. self.assertEqual(
  951. comparison.htmldiff(),
  952. 'Meetings, <span class="deletion">Parties</span>, <span class="addition">Holidays</span>',
  953. )
  954. self.assertIsInstance(comparison.htmldiff(), SafeString)
  955. class TestForeignObjectComparison(TestCase):
  956. comparison_class = compare.ForeignObjectComparison
  957. @classmethod
  958. def setUpTestData(cls):
  959. image_model = get_image_model()
  960. cls.test_image_1 = image_model.objects.create(
  961. title="Test image 1",
  962. file=get_test_image_file(),
  963. )
  964. cls.test_image_2 = image_model.objects.create(
  965. title="Test image 2",
  966. file=get_test_image_file(),
  967. )
  968. def test_hasnt_changed(self):
  969. comparison = self.comparison_class(
  970. EventPage._meta.get_field("feed_image"),
  971. EventPage(feed_image=self.test_image_1),
  972. EventPage(feed_image=self.test_image_1),
  973. )
  974. self.assertTrue(comparison.is_field)
  975. self.assertFalse(comparison.is_child_relation)
  976. self.assertEqual(comparison.field_label(), "Feed image")
  977. self.assertEqual(comparison.htmldiff(), "Test image 1")
  978. self.assertIsInstance(comparison.htmldiff(), SafeString)
  979. self.assertFalse(comparison.has_changed())
  980. def test_has_changed(self):
  981. comparison = self.comparison_class(
  982. EventPage._meta.get_field("feed_image"),
  983. EventPage(feed_image=self.test_image_1),
  984. EventPage(feed_image=self.test_image_2),
  985. )
  986. self.assertEqual(
  987. comparison.htmldiff(),
  988. '<span class="deletion">Test image 1</span><span class="addition">Test image 2</span>',
  989. )
  990. self.assertIsInstance(comparison.htmldiff(), SafeString)
  991. self.assertTrue(comparison.has_changed())
  992. class TestForeignObjectComparisonWithCustomPK(TestCase):
  993. """ForeignObjectComparison works with models declaring a custom primary key field"""
  994. comparison_class = compare.ForeignObjectComparison
  995. @classmethod
  996. def setUpTestData(cls):
  997. ad1 = AdvertWithCustomPrimaryKey.objects.create(
  998. advert_id="ad1", text="Advert 1"
  999. )
  1000. ad2 = AdvertWithCustomPrimaryKey.objects.create(
  1001. advert_id="ad2", text="Advert 2"
  1002. )
  1003. cls.test_obj_1 = SnippetChooserModelWithCustomPrimaryKey.objects.create(
  1004. advertwithcustomprimarykey=ad1
  1005. )
  1006. cls.test_obj_2 = SnippetChooserModelWithCustomPrimaryKey.objects.create(
  1007. advertwithcustomprimarykey=ad2
  1008. )
  1009. def test_hasnt_changed(self):
  1010. comparison = self.comparison_class(
  1011. SnippetChooserModelWithCustomPrimaryKey._meta.get_field(
  1012. "advertwithcustomprimarykey"
  1013. ),
  1014. self.test_obj_1,
  1015. self.test_obj_1,
  1016. )
  1017. self.assertTrue(comparison.is_field)
  1018. self.assertFalse(comparison.is_child_relation)
  1019. self.assertEqual(comparison.field_label(), "Advertwithcustomprimarykey")
  1020. self.assertEqual(comparison.htmldiff(), "Advert 1")
  1021. self.assertIsInstance(comparison.htmldiff(), SafeString)
  1022. self.assertFalse(comparison.has_changed())
  1023. def test_has_changed(self):
  1024. comparison = self.comparison_class(
  1025. SnippetChooserModelWithCustomPrimaryKey._meta.get_field(
  1026. "advertwithcustomprimarykey"
  1027. ),
  1028. self.test_obj_1,
  1029. self.test_obj_2,
  1030. )
  1031. self.assertEqual(
  1032. comparison.htmldiff(),
  1033. '<span class="deletion">Advert 1</span><span class="addition">Advert 2</span>',
  1034. )
  1035. self.assertIsInstance(comparison.htmldiff(), SafeString)
  1036. self.assertTrue(comparison.has_changed())
  1037. class TestChildRelationComparison(TestCase):
  1038. field_comparison_class = compare.FieldComparison
  1039. comparison_class = compare.ChildRelationComparison
  1040. def test_hasnt_changed(self):
  1041. # Two event pages with speaker called "Father Christmas". Neither of
  1042. # the speaker objects have an ID so this tests that the code can match
  1043. # the two together by field content.
  1044. event_page = EventPage(title="Event page", slug="event")
  1045. event_page.speakers.add(
  1046. EventPageSpeaker(
  1047. first_name="Father",
  1048. last_name="Christmas",
  1049. )
  1050. )
  1051. modified_event_page = EventPage(title="Event page", slug="event")
  1052. modified_event_page.speakers.add(
  1053. EventPageSpeaker(
  1054. first_name="Father",
  1055. last_name="Christmas",
  1056. )
  1057. )
  1058. comparison = self.comparison_class(
  1059. EventPage._meta.get_field("speaker"),
  1060. [
  1061. partial(
  1062. self.field_comparison_class,
  1063. EventPageSpeaker._meta.get_field("first_name"),
  1064. ),
  1065. partial(
  1066. self.field_comparison_class,
  1067. EventPageSpeaker._meta.get_field("last_name"),
  1068. ),
  1069. ],
  1070. event_page,
  1071. modified_event_page,
  1072. )
  1073. self.assertFalse(comparison.is_field)
  1074. self.assertTrue(comparison.is_child_relation)
  1075. self.assertEqual(comparison.field_label(), "Speaker")
  1076. self.assertFalse(comparison.has_changed())
  1077. # Check mapping
  1078. objs_a = list(comparison.val_a.all())
  1079. objs_b = list(comparison.val_b.all())
  1080. map_forwards, map_backwards, added, deleted = comparison.get_mapping(
  1081. objs_a, objs_b
  1082. )
  1083. self.assertEqual(map_forwards, {0: 0})
  1084. self.assertEqual(map_backwards, {0: 0})
  1085. self.assertEqual(added, [])
  1086. self.assertEqual(deleted, [])
  1087. def test_has_changed(self):
  1088. # Father Christmas renamed to Santa Claus. And Father Ted added.
  1089. # Father Christmas should be mapped to Father Ted because they
  1090. # are most alike. Santa Claus should be displayed as "new"
  1091. event_page = EventPage(title="Event page", slug="event")
  1092. event_page.speakers.add(
  1093. EventPageSpeaker(
  1094. first_name="Father",
  1095. last_name="Christmas",
  1096. sort_order=0,
  1097. )
  1098. )
  1099. modified_event_page = EventPage(title="Event page", slug="event")
  1100. modified_event_page.speakers.add(
  1101. EventPageSpeaker(
  1102. first_name="Santa",
  1103. last_name="Claus",
  1104. sort_order=0,
  1105. )
  1106. )
  1107. modified_event_page.speakers.add(
  1108. EventPageSpeaker(
  1109. first_name="Father",
  1110. last_name="Ted",
  1111. sort_order=1,
  1112. )
  1113. )
  1114. comparison = self.comparison_class(
  1115. EventPage._meta.get_field("speaker"),
  1116. [
  1117. partial(
  1118. self.field_comparison_class,
  1119. EventPageSpeaker._meta.get_field("first_name"),
  1120. ),
  1121. partial(
  1122. self.field_comparison_class,
  1123. EventPageSpeaker._meta.get_field("last_name"),
  1124. ),
  1125. ],
  1126. event_page,
  1127. modified_event_page,
  1128. )
  1129. self.assertFalse(comparison.is_field)
  1130. self.assertTrue(comparison.is_child_relation)
  1131. self.assertEqual(comparison.field_label(), "Speaker")
  1132. self.assertTrue(comparison.has_changed())
  1133. # Check mapping
  1134. objs_a = list(comparison.val_a.all())
  1135. objs_b = list(comparison.val_b.all())
  1136. map_forwards, map_backwards, added, deleted = comparison.get_mapping(
  1137. objs_a, objs_b
  1138. )
  1139. self.assertEqual(map_forwards, {0: 1}) # Map Father Christmas to Father Ted
  1140. self.assertEqual(map_backwards, {1: 0}) # Map Father Ted to Father Christmas
  1141. self.assertEqual(added, [0]) # Add Santa Claus
  1142. self.assertEqual(deleted, [])
  1143. def test_has_changed_with_same_id(self):
  1144. # Father Christmas renamed to Santa Claus, but this time the ID of the
  1145. # child object remained the same. It should now be detected as the same
  1146. # object
  1147. event_page = EventPage(title="Event page", slug="event")
  1148. event_page.speakers.add(
  1149. EventPageSpeaker(
  1150. id=1,
  1151. first_name="Father",
  1152. last_name="Christmas",
  1153. sort_order=0,
  1154. )
  1155. )
  1156. modified_event_page = EventPage(title="Event page", slug="event")
  1157. modified_event_page.speakers.add(
  1158. EventPageSpeaker(
  1159. id=1,
  1160. first_name="Santa",
  1161. last_name="Claus",
  1162. sort_order=0,
  1163. )
  1164. )
  1165. modified_event_page.speakers.add(
  1166. EventPageSpeaker(
  1167. first_name="Father",
  1168. last_name="Ted",
  1169. sort_order=1,
  1170. )
  1171. )
  1172. comparison = self.comparison_class(
  1173. EventPage._meta.get_field("speaker"),
  1174. [
  1175. partial(
  1176. self.field_comparison_class,
  1177. EventPageSpeaker._meta.get_field("first_name"),
  1178. ),
  1179. partial(
  1180. self.field_comparison_class,
  1181. EventPageSpeaker._meta.get_field("last_name"),
  1182. ),
  1183. ],
  1184. event_page,
  1185. modified_event_page,
  1186. )
  1187. self.assertFalse(comparison.is_field)
  1188. self.assertTrue(comparison.is_child_relation)
  1189. self.assertEqual(comparison.field_label(), "Speaker")
  1190. self.assertTrue(comparison.has_changed())
  1191. # Check mapping
  1192. objs_a = list(comparison.val_a.all())
  1193. objs_b = list(comparison.val_b.all())
  1194. map_forwards, map_backwards, added, deleted = comparison.get_mapping(
  1195. objs_a, objs_b
  1196. )
  1197. self.assertEqual(map_forwards, {0: 0}) # Map Father Christmas to Santa Claus
  1198. self.assertEqual(map_backwards, {0: 0}) # Map Santa Claus to Father Christmas
  1199. self.assertEqual(added, [1]) # Add Father Ted
  1200. self.assertEqual(deleted, [])
  1201. def test_hasnt_changed_with_different_id(self):
  1202. # Both of the child objects have the same field content but have a
  1203. # different ID so they should be detected as separate objects
  1204. event_page = EventPage(title="Event page", slug="event")
  1205. event_page.speakers.add(
  1206. EventPageSpeaker(
  1207. id=1,
  1208. first_name="Father",
  1209. last_name="Christmas",
  1210. )
  1211. )
  1212. modified_event_page = EventPage(title="Event page", slug="event")
  1213. modified_event_page.speakers.add(
  1214. EventPageSpeaker(
  1215. id=2,
  1216. first_name="Father",
  1217. last_name="Christmas",
  1218. )
  1219. )
  1220. comparison = self.comparison_class(
  1221. EventPage._meta.get_field("speaker"),
  1222. [
  1223. partial(
  1224. self.field_comparison_class,
  1225. EventPageSpeaker._meta.get_field("first_name"),
  1226. ),
  1227. partial(
  1228. self.field_comparison_class,
  1229. EventPageSpeaker._meta.get_field("last_name"),
  1230. ),
  1231. ],
  1232. event_page,
  1233. modified_event_page,
  1234. )
  1235. self.assertFalse(comparison.is_field)
  1236. self.assertTrue(comparison.is_child_relation)
  1237. self.assertEqual(comparison.field_label(), "Speaker")
  1238. self.assertTrue(comparison.has_changed())
  1239. # Check mapping
  1240. objs_a = list(comparison.val_a.all())
  1241. objs_b = list(comparison.val_b.all())
  1242. map_forwards, map_backwards, added, deleted = comparison.get_mapping(
  1243. objs_a, objs_b
  1244. )
  1245. self.assertEqual(map_forwards, {})
  1246. self.assertEqual(map_backwards, {})
  1247. self.assertEqual(added, [0]) # Add new Father Christmas
  1248. self.assertEqual(deleted, [0]) # Delete old Father Christmas
  1249. def test_panel_label_as_field_label(self):
  1250. # Just to check whether passing `label` changes field_label
  1251. event_page = EventPage(title="Event page", slug="event")
  1252. event_page.speakers.add(
  1253. EventPageSpeaker(
  1254. first_name="Father",
  1255. )
  1256. )
  1257. comparison = self.comparison_class(
  1258. EventPage._meta.get_field("speaker"),
  1259. [
  1260. partial(
  1261. self.field_comparison_class,
  1262. EventPageSpeaker._meta.get_field("first_name"),
  1263. )
  1264. ],
  1265. event_page,
  1266. event_page,
  1267. label="Speakers",
  1268. )
  1269. self.assertEqual(comparison.field_label(), "Speakers")
  1270. class TestChildObjectComparison(TestCase):
  1271. field_comparison_class = compare.FieldComparison
  1272. comparison_class = compare.ChildObjectComparison
  1273. def test_same_object(self):
  1274. obj_a = EventPageSpeaker(
  1275. first_name="Father",
  1276. last_name="Christmas",
  1277. )
  1278. obj_b = EventPageSpeaker(
  1279. first_name="Father",
  1280. last_name="Christmas",
  1281. )
  1282. comparison = self.comparison_class(
  1283. EventPageSpeaker,
  1284. [
  1285. partial(
  1286. self.field_comparison_class,
  1287. EventPageSpeaker._meta.get_field("first_name"),
  1288. ),
  1289. partial(
  1290. self.field_comparison_class,
  1291. EventPageSpeaker._meta.get_field("last_name"),
  1292. ),
  1293. ],
  1294. obj_a,
  1295. obj_b,
  1296. )
  1297. self.assertFalse(comparison.is_addition())
  1298. self.assertFalse(comparison.is_deletion())
  1299. self.assertFalse(comparison.has_changed())
  1300. self.assertEqual(comparison.get_position_change(), 0)
  1301. self.assertEqual(comparison.get_num_differences(), 0)
  1302. def test_different_object(self):
  1303. obj_a = EventPageSpeaker(
  1304. first_name="Father",
  1305. last_name="Christmas",
  1306. )
  1307. obj_b = EventPageSpeaker(
  1308. first_name="Santa",
  1309. last_name="Claus",
  1310. )
  1311. comparison = self.comparison_class(
  1312. EventPageSpeaker,
  1313. [
  1314. partial(
  1315. self.field_comparison_class,
  1316. EventPageSpeaker._meta.get_field("first_name"),
  1317. ),
  1318. partial(
  1319. self.field_comparison_class,
  1320. EventPageSpeaker._meta.get_field("last_name"),
  1321. ),
  1322. ],
  1323. obj_a,
  1324. obj_b,
  1325. )
  1326. self.assertFalse(comparison.is_addition())
  1327. self.assertFalse(comparison.is_deletion())
  1328. self.assertTrue(comparison.has_changed())
  1329. self.assertEqual(comparison.get_position_change(), 0)
  1330. self.assertEqual(comparison.get_num_differences(), 2)
  1331. def test_moved_object(self):
  1332. obj_a = EventPageSpeaker(
  1333. first_name="Father",
  1334. last_name="Christmas",
  1335. sort_order=1,
  1336. )
  1337. obj_b = EventPageSpeaker(
  1338. first_name="Father",
  1339. last_name="Christmas",
  1340. sort_order=5,
  1341. )
  1342. comparison = self.comparison_class(
  1343. EventPageSpeaker,
  1344. [
  1345. partial(
  1346. self.field_comparison_class,
  1347. EventPageSpeaker._meta.get_field("first_name"),
  1348. ),
  1349. partial(
  1350. self.field_comparison_class,
  1351. EventPageSpeaker._meta.get_field("last_name"),
  1352. ),
  1353. ],
  1354. obj_a,
  1355. obj_b,
  1356. )
  1357. self.assertFalse(comparison.is_addition())
  1358. self.assertFalse(comparison.is_deletion())
  1359. self.assertFalse(comparison.has_changed())
  1360. self.assertEqual(comparison.get_position_change(), 4)
  1361. self.assertEqual(comparison.get_num_differences(), 0)
  1362. def test_addition(self):
  1363. obj = EventPageSpeaker(
  1364. first_name="Father",
  1365. last_name="Christmas",
  1366. )
  1367. comparison = self.comparison_class(
  1368. EventPageSpeaker,
  1369. [
  1370. partial(
  1371. self.field_comparison_class,
  1372. EventPageSpeaker._meta.get_field("first_name"),
  1373. ),
  1374. partial(
  1375. self.field_comparison_class,
  1376. EventPageSpeaker._meta.get_field("last_name"),
  1377. ),
  1378. ],
  1379. None,
  1380. obj,
  1381. )
  1382. self.assertTrue(comparison.is_addition())
  1383. self.assertFalse(comparison.is_deletion())
  1384. self.assertFalse(comparison.has_changed())
  1385. self.assertIsNone(comparison.get_position_change(), 0)
  1386. self.assertEqual(comparison.get_num_differences(), 0)
  1387. def test_deletion(self):
  1388. obj = EventPageSpeaker(
  1389. first_name="Father",
  1390. last_name="Christmas",
  1391. )
  1392. comparison = self.comparison_class(
  1393. EventPageSpeaker,
  1394. [
  1395. partial(
  1396. self.field_comparison_class,
  1397. EventPageSpeaker._meta.get_field("first_name"),
  1398. ),
  1399. partial(
  1400. self.field_comparison_class,
  1401. EventPageSpeaker._meta.get_field("last_name"),
  1402. ),
  1403. ],
  1404. obj,
  1405. None,
  1406. )
  1407. self.assertFalse(comparison.is_addition())
  1408. self.assertTrue(comparison.is_deletion())
  1409. self.assertFalse(comparison.has_changed())
  1410. self.assertIsNone(comparison.get_position_change())
  1411. self.assertEqual(comparison.get_num_differences(), 0)
  1412. class TestChildRelationComparisonUsingPK(TestCase):
  1413. """Test related objects can be compred if they do not use id for primary key"""
  1414. field_comparison_class = compare.FieldComparison
  1415. comparison_class = compare.ChildRelationComparison
  1416. def test_has_changed_with_same_id(self):
  1417. # Head Count was changed but the PK of the child object remained the same.
  1418. # It should be detected as the same object
  1419. event_page = EventPage(title="Semi Finals", slug="semi-finals-2018")
  1420. event_page.head_counts.add(
  1421. HeadCountRelatedModelUsingPK(
  1422. custom_id=1,
  1423. head_count=22,
  1424. )
  1425. )
  1426. modified_event_page = EventPage(title="Semi Finals", slug="semi-finals-2018")
  1427. modified_event_page.head_counts.add(
  1428. HeadCountRelatedModelUsingPK(
  1429. custom_id=1,
  1430. head_count=23,
  1431. )
  1432. )
  1433. modified_event_page.head_counts.add(
  1434. HeadCountRelatedModelUsingPK(
  1435. head_count=25,
  1436. )
  1437. )
  1438. comparison = self.comparison_class(
  1439. EventPage._meta.get_field("head_counts"),
  1440. [
  1441. partial(
  1442. self.field_comparison_class,
  1443. HeadCountRelatedModelUsingPK._meta.get_field("head_count"),
  1444. )
  1445. ],
  1446. event_page,
  1447. modified_event_page,
  1448. )
  1449. self.assertFalse(comparison.is_field)
  1450. self.assertTrue(comparison.is_child_relation)
  1451. self.assertEqual(comparison.field_label(), "Head counts")
  1452. self.assertTrue(comparison.has_changed())
  1453. # Check mapping
  1454. objs_a = list(comparison.val_a.all())
  1455. objs_b = list(comparison.val_b.all())
  1456. map_forwards, map_backwards, added, deleted = comparison.get_mapping(
  1457. objs_a, objs_b
  1458. )
  1459. self.assertEqual(map_forwards, {0: 0}) # map head count 22 to 23
  1460. self.assertEqual(map_backwards, {0: 0}) # map head count 23 to 22
  1461. self.assertEqual(added, [1]) # add second head count
  1462. self.assertEqual(deleted, [])
  1463. def test_hasnt_changed_with_different_id(self):
  1464. # Both of the child objects have the same field content but have a
  1465. # different PK (ID) so they should be detected as separate objects
  1466. event_page = EventPage(title="Finals", slug="finals-event-abc")
  1467. event_page.head_counts.add(
  1468. HeadCountRelatedModelUsingPK(custom_id=1, head_count=220)
  1469. )
  1470. modified_event_page = EventPage(title="Finals", slug="finals-event-abc")
  1471. modified_event_page.head_counts.add(
  1472. HeadCountRelatedModelUsingPK(custom_id=2, head_count=220)
  1473. )
  1474. comparison = self.comparison_class(
  1475. EventPage._meta.get_field("head_counts"),
  1476. [
  1477. partial(
  1478. self.field_comparison_class,
  1479. HeadCountRelatedModelUsingPK._meta.get_field("head_count"),
  1480. )
  1481. ],
  1482. event_page,
  1483. modified_event_page,
  1484. )
  1485. self.assertFalse(comparison.is_field)
  1486. self.assertTrue(comparison.is_child_relation)
  1487. self.assertEqual(comparison.field_label(), "Head counts")
  1488. self.assertTrue(comparison.has_changed())
  1489. # Check mapping
  1490. objs_a = list(comparison.val_a.all())
  1491. objs_b = list(comparison.val_b.all())
  1492. map_forwards, map_backwards, added, deleted = comparison.get_mapping(
  1493. objs_a, objs_b
  1494. )
  1495. self.assertEqual(map_forwards, {})
  1496. self.assertEqual(map_backwards, {})
  1497. self.assertEqual(added, [0]) # Add new head count
  1498. self.assertEqual(deleted, [0]) # Delete old head count