test_model_viewset.py 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425
  1. import datetime
  2. from io import BytesIO
  3. from django.conf import settings
  4. from django.contrib.admin.utils import quote
  5. from django.contrib.auth import get_permission_codename
  6. from django.contrib.auth.models import Permission
  7. from django.contrib.contenttypes.models import ContentType
  8. from django.test import TestCase
  9. from django.urls import NoReverseMatch, reverse
  10. from django.utils.formats import date_format, localize
  11. from django.utils.html import escape
  12. from django.utils.timezone import make_aware
  13. from openpyxl import load_workbook
  14. from wagtail.admin.admin_url_finder import AdminURLFinder
  15. from wagtail.models import ModelLogEntry
  16. from wagtail.test.testapp.models import (
  17. FeatureCompleteToy,
  18. JSONStreamModel,
  19. SearchTestModel,
  20. VariousOnDeleteModel,
  21. )
  22. from wagtail.test.utils.template_tests import AdminTemplateTestUtils
  23. from wagtail.test.utils.wagtail_tests import WagtailTestUtils
  24. from wagtail.utils.deprecation import RemovedInWagtail70Warning
  25. class TestModelViewSetGroup(WagtailTestUtils, TestCase):
  26. def setUp(self):
  27. self.user = self.login()
  28. def test_menu_items(self):
  29. response = self.client.get(reverse("wagtailadmin_home"))
  30. self.assertEqual(response.status_code, 200)
  31. # Menu label falls back to the title-cased app label
  32. self.assertContains(
  33. response,
  34. '"name": "tests", "label": "Tests", "icon_name": "folder-open-inverse"',
  35. )
  36. # Title-cased from verbose_name_plural
  37. self.assertContains(response, "Json Stream Models")
  38. self.assertContains(response, reverse("streammodel:index"))
  39. self.assertEqual(reverse("streammodel:index"), "/admin/streammodel/")
  40. # Set on class
  41. self.assertContains(response, "JSON MinMaxCount StreamModel")
  42. self.assertContains(response, reverse("minmaxcount_streammodel:index"))
  43. self.assertEqual(
  44. reverse("minmaxcount_streammodel:index"),
  45. "/admin/minmaxcount-streammodel/",
  46. )
  47. # Set on instance
  48. self.assertContains(response, "JSON BlockCounts StreamModel")
  49. self.assertContains(response, reverse("blockcounts_streammodel:index"))
  50. self.assertEqual(
  51. reverse("blockcounts_streammodel:index"),
  52. "/admin/blockcounts/streammodel/",
  53. )
  54. class TestTemplateConfiguration(WagtailTestUtils, TestCase):
  55. def setUp(self):
  56. self.user = self.login()
  57. @classmethod
  58. def setUpTestData(cls):
  59. cls.default = JSONStreamModel.objects.create(
  60. body='[{"type": "text", "value": "foo"}]',
  61. )
  62. cls.custom = FeatureCompleteToy.objects.create(name="Test Toy")
  63. def get_default_url(self, view_name, args=()):
  64. return reverse(f"streammodel:{view_name}", args=args)
  65. def get_custom_url(self, view_name, args=()):
  66. return reverse(f"feature_complete_toy:{view_name}", args=args)
  67. def test_default_templates(self):
  68. pk = quote(self.default.pk)
  69. cases = {
  70. "index": (
  71. [],
  72. "wagtailadmin/generic/index.html",
  73. ),
  74. "index_results": (
  75. [],
  76. "wagtailadmin/generic/listing_results.html",
  77. ),
  78. "add": (
  79. [],
  80. "wagtailadmin/generic/create.html",
  81. ),
  82. "edit": (
  83. [pk],
  84. "wagtailadmin/generic/edit.html",
  85. ),
  86. "delete": (
  87. [pk],
  88. "wagtailadmin/generic/confirm_delete.html",
  89. ),
  90. }
  91. for view_name, (args, template_name) in cases.items():
  92. with self.subTest(view_name=view_name):
  93. response = self.client.get(self.get_default_url(view_name, args=args))
  94. self.assertTemplateUsed(response, template_name)
  95. def test_custom_template_lookups(self):
  96. pk = quote(self.custom.pk)
  97. cases = {
  98. "override with index_template_name": (
  99. "index",
  100. [],
  101. "tests/fctoy_index.html",
  102. ),
  103. "with app label and model name": (
  104. "add",
  105. [],
  106. "customprefix/tests/featurecompletetoy/create.html",
  107. ),
  108. "with app label": (
  109. "edit",
  110. [pk],
  111. "customprefix/tests/edit.html",
  112. ),
  113. "without app label and model name": (
  114. "delete",
  115. [pk],
  116. "customprefix/confirm_delete.html",
  117. ),
  118. }
  119. for case, (view_name, args, template_name) in cases.items():
  120. with self.subTest(case=case):
  121. response = self.client.get(self.get_custom_url(view_name, args=args))
  122. self.assertTemplateUsed(response, template_name)
  123. self.assertContains(
  124. response, "<p>Some extra custom content</p>", html=True
  125. )
  126. def test_wagtail_admin_template_mixin_variables_with_legacy_header(self):
  127. pk = quote(self.custom.pk)
  128. cases = {
  129. "delete": ([pk], "Delete", str(self.custom)),
  130. }
  131. for view_name, (args, title, subtitle) in cases.items():
  132. with self.subTest(view_name=view_name):
  133. response = self.client.get(self.get_custom_url(view_name, args=args))
  134. soup = self.get_soup(response.content)
  135. h1 = soup.select_one("h1")
  136. self.assertIsNotNone(h1)
  137. self.assertEqual(
  138. "".join(h1.find_all(string=True, recursive=False)).strip(), title
  139. )
  140. subtitle_el = h1.select_one("span")
  141. if subtitle:
  142. self.assertIsNotNone(subtitle_el)
  143. self.assertEqual(subtitle_el.string, subtitle)
  144. else:
  145. self.assertIsNone(subtitle_el)
  146. icon = h1.select_one("svg use[href='#icon-media']")
  147. self.assertIsNotNone(icon)
  148. def test_wagtail_admin_template_mixin_variables(self):
  149. pk = quote(self.custom.pk)
  150. cases = {
  151. "index": ([], "Feature complete toys", None),
  152. "add": ([], "New", "Feature complete toy"),
  153. "edit": ([pk], "Editing", str(self.custom)),
  154. }
  155. for view_name, (args, title, subtitle) in cases.items():
  156. with self.subTest(view_name=view_name):
  157. response = self.client.get(self.get_custom_url(view_name, args=args))
  158. soup = self.get_soup(response.content)
  159. h1 = soup.select_one("h1")
  160. expected_h1 = title
  161. if subtitle:
  162. expected_h1 = f"{title}: {subtitle}"
  163. self.assertIsNotNone(h1)
  164. self.assertEqual(h1.get_text(strip=True), expected_h1)
  165. icon = h1.select_one("svg use[href='#icon-media']")
  166. # Icon is no longer rendered in the h1 with the slim header in place
  167. self.assertIsNone(icon)
  168. class TestCustomColumns(WagtailTestUtils, TestCase):
  169. def setUp(self):
  170. self.user = self.login()
  171. @classmethod
  172. def setUpTestData(cls):
  173. FeatureCompleteToy.objects.create(name="Racecar")
  174. FeatureCompleteToy.objects.create(name="level")
  175. FeatureCompleteToy.objects.create(name="Lotso")
  176. def test_list_display(self):
  177. index_url = reverse("feature_complete_toy:index")
  178. response = self.client.get(index_url)
  179. # "name" column
  180. self.assertContains(response, "Racecar")
  181. self.assertContains(response, "level")
  182. self.assertContains(response, "Lotso")
  183. # BooleanColumn("is_cool")
  184. soup = self.get_soup(response.content)
  185. help = soup.select_one("td:has(svg.icon-help)")
  186. self.assertIsNotNone(help)
  187. self.assertEqual(help.text.strip(), "None")
  188. success = soup.select_one("td:has(svg.icon-success.w-text-positive-100)")
  189. self.assertIsNotNone(success)
  190. self.assertEqual(success.text.strip(), "True")
  191. error = soup.select_one("td:has(svg.icon-error.w-text-critical-100)")
  192. self.assertIsNotNone(error)
  193. self.assertEqual(error.text.strip(), "False")
  194. updated_at = soup.select("th a")[-1]
  195. self.assertEqual(updated_at.text.strip(), "Updated")
  196. self.assertEqual(updated_at["href"], f"{index_url}?ordering=_updated_at")
  197. class TestListFilter(WagtailTestUtils, TestCase):
  198. cases = {
  199. "list": ("feature_complete_toy", "release_date", "Release date"),
  200. "dict": ("fctoy_alt1", "name__icontains", "Name contains"),
  201. "filterset_class": (
  202. "fctoy-alt2",
  203. "release_date__year__lte",
  204. "Release date year is less than or equal to",
  205. ),
  206. }
  207. def setUp(self):
  208. self.user = self.login()
  209. def get(self, url_namespace, params=None):
  210. return self.client.get(reverse(f"{url_namespace}:index"), params)
  211. @classmethod
  212. def setUpTestData(cls):
  213. FeatureCompleteToy.objects.create(
  214. name="Buzz Lightyear",
  215. release_date=datetime.date(1995, 11, 19),
  216. )
  217. FeatureCompleteToy.objects.create(
  218. name="Forky",
  219. release_date=datetime.date(2019, 6, 11),
  220. )
  221. def test_unfiltered_no_results(self):
  222. FeatureCompleteToy.objects.all().delete()
  223. for case, (url_namespace, lookup, label_text) in self.cases.items():
  224. with self.subTest(case=case):
  225. response = self.get(url_namespace)
  226. self.assertContains(
  227. response,
  228. "There are no feature complete toys to display",
  229. )
  230. self.assertNotContains(
  231. response,
  232. "No feature complete toys match your query",
  233. )
  234. self.assertNotContains(response, "Buzz Lightyear")
  235. self.assertNotContains(response, "Forky")
  236. soup = self.get_soup(response.content)
  237. label = soup.select_one(f"label#id_{lookup}-label")
  238. self.assertIsNotNone(label)
  239. self.assertEqual(label.string.strip(), label_text)
  240. input = soup.select_one(f"input#id_{lookup}")
  241. self.assertIsNotNone(input)
  242. def test_unfiltered_with_results(self):
  243. for case, (url_namespace, lookup, label_text) in self.cases.items():
  244. with self.subTest(case=case):
  245. response = self.get(url_namespace)
  246. self.assertContains(response, "Buzz Lightyear")
  247. self.assertContains(response, "Forky")
  248. self.assertNotContains(response, "There are 2 matches")
  249. self.assertNotContains(
  250. response,
  251. "There are no feature complete toys to display",
  252. )
  253. self.assertNotContains(
  254. response,
  255. "No feature complete toys match your query",
  256. )
  257. soup = self.get_soup(response.content)
  258. label = soup.select_one(f"label#id_{lookup}-label")
  259. self.assertIsNotNone(label)
  260. self.assertEqual(label.string.strip(), label_text)
  261. input = soup.select_one(f"input#id_{lookup}")
  262. self.assertIsNotNone(input)
  263. def test_empty_filter_with_results(self):
  264. for case, (url_namespace, lookup, label_text) in self.cases.items():
  265. with self.subTest(case=case):
  266. response = self.get(url_namespace, {lookup: ""})
  267. self.assertContains(response, "Buzz Lightyear")
  268. self.assertContains(response, "Forky")
  269. self.assertNotContains(response, "There are 2 matches")
  270. self.assertNotContains(
  271. response,
  272. "No feature complete toys match your query",
  273. )
  274. soup = self.get_soup(response.content)
  275. label = soup.select_one(f"label#id_{lookup}-label")
  276. self.assertIsNotNone(label)
  277. self.assertEqual(label.string.strip(), label_text)
  278. input = soup.select_one(f"input#id_{lookup}")
  279. self.assertIsNotNone(input)
  280. self.assertFalse(input.attrs.get("value"))
  281. def test_filtered_no_results(self):
  282. lookup_values = {
  283. "release_date": "1999-09-09",
  284. "name__icontains": "Woody",
  285. "release_date__year__lte": "1990",
  286. }
  287. for case, (url_namespace, lookup, label_text) in self.cases.items():
  288. with self.subTest(case=case):
  289. value = lookup_values[lookup]
  290. response = self.get(url_namespace, {lookup: value})
  291. self.assertContains(
  292. response,
  293. "No feature complete toys match your query",
  294. )
  295. self.assertNotContains(response, "Buzz Lightyear")
  296. self.assertNotContains(response, "Forky")
  297. self.assertNotContains(response, "There are 2 matches")
  298. soup = self.get_soup(response.content)
  299. label = soup.select_one(f"label#id_{lookup}-label")
  300. self.assertIsNotNone(label)
  301. self.assertEqual(label.string.strip(), label_text)
  302. input = soup.select_one(f"input#id_{lookup}")
  303. self.assertIsNotNone(input)
  304. self.assertEqual(input.attrs.get("value"), value)
  305. # Should render the active filters even when there are no results
  306. active_filters = soup.select_one(".w-active-filters")
  307. self.assertIsNotNone(active_filters)
  308. clear = active_filters.select_one(".w-pill__remove")
  309. self.assertIsNotNone(clear)
  310. url, params = clear.attrs.get("data-w-swap-src-value").split("?", 1)
  311. self.assertEqual(url, reverse(f"{url_namespace}:index_results"))
  312. self.assertNotIn(f"{lookup}={value}", params)
  313. def test_filtered_with_results(self):
  314. lookup_values = {
  315. "release_date": "1995-11-19",
  316. "name__icontains": "Ightyear",
  317. "release_date__year__lte": "2017",
  318. }
  319. for case, (url_namespace, lookup, label_text) in self.cases.items():
  320. with self.subTest(case=case):
  321. value = lookup_values[lookup]
  322. response = self.get(url_namespace, {lookup: value})
  323. self.assertContains(response, "Buzz Lightyear")
  324. self.assertContains(response, "There is 1 match")
  325. self.assertNotContains(response, "Forky")
  326. self.assertNotContains(
  327. response,
  328. "No feature complete toys match your query",
  329. )
  330. soup = self.get_soup(response.content)
  331. label = soup.select_one(f"label#id_{lookup}-label")
  332. self.assertIsNotNone(label)
  333. self.assertEqual(label.string.strip(), label_text)
  334. input = soup.select_one(f"input#id_{lookup}")
  335. self.assertIsNotNone(input)
  336. self.assertEqual(input.attrs.get("value"), value)
  337. # Should render the active filters
  338. active_filters = soup.select_one(".w-active-filters")
  339. self.assertIsNotNone(active_filters)
  340. clear = active_filters.select_one(".w-pill__remove")
  341. self.assertIsNotNone(clear)
  342. url, params = clear.attrs.get("data-w-swap-src-value").split("?", 1)
  343. self.assertEqual(url, reverse(f"{url_namespace}:index_results"))
  344. self.assertNotIn(f"{lookup}={value}", params)
  345. class TestSearchIndexView(WagtailTestUtils, TestCase):
  346. url_name = "index"
  347. cases = {
  348. # With the default search backend
  349. "default": ("feature_complete_toy", "release_date"),
  350. # With Django ORM
  351. None: ("fctoy-alt2", "release_date__year__lte"),
  352. }
  353. def setUp(self):
  354. self.user = self.login()
  355. @classmethod
  356. def setUpTestData(cls):
  357. FeatureCompleteToy.objects.create(
  358. name="Buzz Lightyear",
  359. release_date=datetime.date(1995, 11, 19),
  360. )
  361. FeatureCompleteToy.objects.create(
  362. name="Forky",
  363. release_date=datetime.date(2019, 6, 11),
  364. )
  365. def assertInputRendered(self, response, search_q):
  366. soup = self.get_soup(response.content)
  367. input = soup.select_one("input#id_q")
  368. self.assertIsNotNone(input)
  369. self.assertEqual(input.attrs.get("value"), search_q)
  370. def get(self, url_namespace, params=None):
  371. return self.client.get(reverse(f"{url_namespace}:{self.url_name}"), params)
  372. def test_search_disabled(self):
  373. response = self.get("fctoy_alt1", {"q": "ork"})
  374. self.assertContains(response, "Forky")
  375. self.assertContains(response, "Buzz Lightyear")
  376. self.assertNotContains(response, "There are 2 matches")
  377. soup = self.get_soup(response.content)
  378. input = soup.select_one("input#id_q")
  379. self.assertIsNone(input)
  380. def test_search_no_results(self):
  381. for backend, (url_namespace, _) in self.cases.items():
  382. with self.subTest(backend=backend):
  383. response = self.get(url_namespace, {"q": "Woody"})
  384. self.assertContains(
  385. response,
  386. "No feature complete toys match your query",
  387. )
  388. self.assertNotContains(response, "Buzz Lightyear")
  389. self.assertNotContains(response, "Forky")
  390. self.assertInputRendered(response, "Woody")
  391. def test_search_with_results(self):
  392. for backend, (url_namespace, _) in self.cases.items():
  393. with self.subTest(backend=backend):
  394. response = self.get(url_namespace, {"q": "ork"})
  395. self.assertContains(response, "Forky")
  396. self.assertNotContains(response, "Buzz Lightyear")
  397. self.assertContains(response, "There is 1 match")
  398. self.assertInputRendered(response, "ork")
  399. def test_filtered_searched_no_results(self):
  400. lookup_values = {
  401. "release_date": "2019-06-11",
  402. "release_date__year__lte": "2023",
  403. }
  404. for backend, (url_namespace, lookup) in self.cases.items():
  405. with self.subTest(backend=backend):
  406. value = lookup_values[lookup]
  407. response = self.get(url_namespace, {"q": "Woody", lookup: value})
  408. self.assertContains(
  409. response,
  410. "No feature complete toys match your query",
  411. )
  412. self.assertNotContains(response, "Buzz Lightyear")
  413. self.assertNotContains(response, "Forky")
  414. self.assertInputRendered(response, "Woody")
  415. def test_filtered_searched_with_results(self):
  416. lookup_values = {
  417. "release_date": "2019-06-11",
  418. "release_date__year__lte": "2023",
  419. }
  420. for backend, (url_namespace, lookup) in self.cases.items():
  421. with self.subTest(backend=backend):
  422. value = lookup_values[lookup]
  423. response = self.get(url_namespace, {"q": "ork", lookup: value})
  424. self.assertContains(response, "Forky")
  425. self.assertNotContains(response, "Buzz Lightyear")
  426. self.assertContains(response, "There is 1 match")
  427. self.assertInputRendered(response, "ork")
  428. class TestSearchIndexResultsView(TestSearchIndexView):
  429. url_name = "index_results"
  430. def assertInputRendered(self, response, search_q):
  431. # index_results view doesn't render the search input
  432. pass
  433. class TestSearchFields(WagtailTestUtils, TestCase):
  434. @classmethod
  435. def setUpTestData(cls):
  436. objects = [
  437. SearchTestModel(title="Hello World", body="This one is classic"),
  438. SearchTestModel(title="Hello Anime", body="We love anime (opinions vary)"),
  439. SearchTestModel(title="Food", body="I like food, do you?"),
  440. ]
  441. SearchTestModel.objects.bulk_create(objects)
  442. def setUp(self):
  443. self.login()
  444. def get(self, q):
  445. return self.client.get(reverse("searchtest:index"), {"q": q})
  446. def test_single_result_with_body(self):
  447. response = self.get("IkE")
  448. self.assertEqual(response.status_code, 200)
  449. self.assertNotContains(response, "Hello World")
  450. self.assertNotContains(response, "Hello Anime")
  451. self.assertContains(response, "Food")
  452. def test_multiple_results_with_title(self):
  453. response = self.get("ELlo")
  454. self.assertEqual(response.status_code, 200)
  455. self.assertContains(response, "Hello World")
  456. self.assertContains(response, "Hello Anime")
  457. self.assertNotContains(response, "Food")
  458. def test_no_results(self):
  459. response = self.get("Abra Kadabra")
  460. self.assertEqual(response.status_code, 200)
  461. self.assertNotContains(response, "Hello World")
  462. self.assertNotContains(response, "Hello Anime")
  463. self.assertNotContains(response, "Food")
  464. class TestListExport(WagtailTestUtils, TestCase):
  465. def setUp(self):
  466. self.user = self.login()
  467. @classmethod
  468. def setUpTestData(cls):
  469. FeatureCompleteToy.objects.create(
  470. name="Racecar",
  471. release_date=datetime.date(1995, 11, 19),
  472. )
  473. FeatureCompleteToy.objects.create(
  474. name="LEVEL",
  475. release_date=datetime.date(2010, 6, 18),
  476. )
  477. FeatureCompleteToy.objects.create(
  478. name="Catso",
  479. release_date=datetime.date(2010, 6, 18),
  480. )
  481. def test_export_disabled(self):
  482. index_url = reverse("fctoy_alt1:index")
  483. response = self.client.get(index_url)
  484. soup = self.get_soup(response.content)
  485. csv_link = soup.select_one(f"a[href='{index_url}?export=csv']")
  486. self.assertIsNone(csv_link)
  487. xlsx_link = soup.select_one(f"a[href='{index_url}?export=xlsx']")
  488. self.assertIsNone(xlsx_link)
  489. def test_get_not_export_shows_export_buttons(self):
  490. index_url = reverse("feature_complete_toy:index")
  491. response = self.client.get(index_url)
  492. soup = self.get_soup(response.content)
  493. csv_link = soup.select_one(f"a[href='{index_url}?export=csv']")
  494. self.assertIsNotNone(csv_link)
  495. self.assertEqual(csv_link.text.strip(), "Download CSV")
  496. xlsx_link = soup.select_one(f"a[href='{index_url}?export=xlsx']")
  497. self.assertIsNotNone(xlsx_link)
  498. self.assertEqual(xlsx_link.text.strip(), "Download XLSX")
  499. def test_get_filtered_shows_export_buttons_with_filters(self):
  500. index_url = reverse("feature_complete_toy:index")
  501. response = self.client.get(index_url, {"release_date": "2010-06-18"})
  502. soup = self.get_soup(response.content)
  503. csv_link = soup.select_one(
  504. f"a[href='{index_url}?release_date=2010-06-18&export=csv']"
  505. )
  506. self.assertIsNotNone(csv_link)
  507. self.assertEqual(csv_link.text.strip(), "Download CSV")
  508. xlsx_link = soup.select_one(
  509. f"a[href='{index_url}?release_date=2010-06-18&export=xlsx']"
  510. )
  511. self.assertIsNotNone(xlsx_link)
  512. self.assertEqual(xlsx_link.text.strip(), "Download XLSX")
  513. def test_csv_export(self):
  514. index_url = reverse("feature_complete_toy:index")
  515. response = self.client.get(index_url, {"export": "csv"})
  516. self.assertEqual(response.status_code, 200)
  517. self.assertEqual(
  518. response.get("Content-Disposition"),
  519. 'attachment; filename="feature-complete-toys.csv"',
  520. )
  521. data_lines = response.getvalue().decode().strip().split("\r\n")
  522. self.assertEqual(data_lines[0], "Name,Launch date,Is cool")
  523. self.assertEqual(data_lines[1], "Catso,2010-06-18,False")
  524. self.assertEqual(data_lines[2], "LEVEL,2010-06-18,True")
  525. self.assertEqual(data_lines[3], "Racecar,1995-11-19,")
  526. self.assertEqual(len(data_lines), 4)
  527. def test_csv_export_filtered(self):
  528. index_url = reverse("feature_complete_toy:index")
  529. response = self.client.get(
  530. index_url,
  531. {"release_date": "2010-06-18", "export": "csv"},
  532. )
  533. self.assertEqual(response.status_code, 200)
  534. self.assertEqual(
  535. response.get("Content-Disposition"),
  536. 'attachment; filename="feature-complete-toys.csv"',
  537. )
  538. data_lines = response.getvalue().decode().strip().split("\r\n")
  539. self.assertEqual(data_lines[0], "Name,Launch date,Is cool")
  540. self.assertEqual(data_lines[1], "Catso,2010-06-18,False")
  541. self.assertEqual(data_lines[2], "LEVEL,2010-06-18,True")
  542. self.assertEqual(len(data_lines), 3)
  543. def test_xlsx_export(self):
  544. index_url = reverse("feature_complete_toy:index")
  545. response = self.client.get(index_url, {"export": "xlsx"})
  546. self.assertEqual(response.status_code, 200)
  547. self.assertEqual(
  548. response.get("Content-Disposition"),
  549. 'attachment; filename="feature-complete-toys.xlsx"',
  550. )
  551. workbook_data = response.getvalue()
  552. worksheet = load_workbook(filename=BytesIO(workbook_data)).active
  553. cell_array = [[cell.value for cell in row] for row in worksheet.rows]
  554. self.assertEqual(cell_array[0], ["Name", "Launch date", "Is cool"])
  555. self.assertEqual(cell_array[1], ["Catso", datetime.date(2010, 6, 18), False])
  556. self.assertEqual(cell_array[2], ["LEVEL", datetime.date(2010, 6, 18), True])
  557. self.assertEqual(cell_array[3], ["Racecar", datetime.date(1995, 11, 19), None])
  558. self.assertEqual(len(cell_array), 4)
  559. def test_xlsx_export_filtered(self):
  560. index_url = reverse("feature_complete_toy:index")
  561. response = self.client.get(
  562. index_url,
  563. {"release_date": "2010-06-18", "export": "xlsx"},
  564. )
  565. self.assertEqual(response.status_code, 200)
  566. self.assertEqual(
  567. response.get("Content-Disposition"),
  568. 'attachment; filename="feature-complete-toys.xlsx"',
  569. )
  570. workbook_data = response.getvalue()
  571. worksheet = load_workbook(filename=BytesIO(workbook_data)).active
  572. cell_array = [[cell.value for cell in row] for row in worksheet.rows]
  573. self.assertEqual(cell_array[0], ["Name", "Launch date", "Is cool"])
  574. self.assertEqual(cell_array[1], ["Catso", datetime.date(2010, 6, 18), False])
  575. self.assertEqual(cell_array[2], ["LEVEL", datetime.date(2010, 6, 18), True])
  576. self.assertEqual(len(cell_array), 3)
  577. class TestPagination(WagtailTestUtils, TestCase):
  578. def setUp(self):
  579. self.user = self.login()
  580. @classmethod
  581. def setUpTestData(cls):
  582. objects = [FeatureCompleteToy(name=f"Frisbee {i}") for i in range(32)]
  583. FeatureCompleteToy.objects.bulk_create(objects)
  584. def test_default_list_pagination(self):
  585. list_url = reverse("fctoy_alt1:index")
  586. response = self.client.get(list_url)
  587. # Default is 20 per page
  588. self.assertEqual(FeatureCompleteToy.objects.all().count(), 32)
  589. self.assertContains(response, "Page 1 of 2")
  590. self.assertContains(response, "Next")
  591. self.assertContains(response, list_url + "?p=2")
  592. def test_custom_list_pagination(self):
  593. list_url = reverse("feature_complete_toy:index")
  594. response = self.client.get(list_url)
  595. # Custom is set to display 5 per page
  596. self.assertEqual(FeatureCompleteToy.objects.all().count(), 32)
  597. self.assertContains(response, "Page 1 of 7")
  598. self.assertContains(response, "Next")
  599. self.assertContains(response, list_url + "?p=2")
  600. class TestOrdering(WagtailTestUtils, TestCase):
  601. def setUp(self):
  602. self.user = self.login()
  603. @classmethod
  604. def setUpTestData(cls):
  605. objects = [
  606. FeatureCompleteToy(name="CCCCCCCCCC", strid="1"),
  607. FeatureCompleteToy(name="AAAAAAAAAA", strid="2"),
  608. FeatureCompleteToy(name="DDDDDDDDDD", strid="3"),
  609. FeatureCompleteToy(name="BBBBBBBBBB", strid="4"),
  610. ]
  611. FeatureCompleteToy.objects.bulk_create(objects)
  612. def test_default_order(self):
  613. response = self.client.get(reverse("fctoy_alt1:index"))
  614. # Without ordering on the model, should be ordered descending by pk
  615. self.assertFalse(FeatureCompleteToy._meta.ordering)
  616. self.assertEqual(
  617. [obj.name for obj in response.context["object_list"]],
  618. [
  619. "BBBBBBBBBB",
  620. "DDDDDDDDDD",
  621. "AAAAAAAAAA",
  622. "CCCCCCCCCC",
  623. ],
  624. )
  625. def test_custom_order_from_query_args(self):
  626. response = self.client.get(reverse("fctoy-alt3:index") + "?ordering=-name")
  627. self.assertFalse(FeatureCompleteToy._meta.ordering)
  628. self.assertEqual(
  629. [obj.name for obj in response.context["object_list"]],
  630. [
  631. "DDDDDDDDDD",
  632. "CCCCCCCCCC",
  633. "BBBBBBBBBB",
  634. "AAAAAAAAAA",
  635. ],
  636. )
  637. def test_custom_order_from_view(self):
  638. response = self.client.get(reverse("feature_complete_toy:index"))
  639. # Should respect the view's ordering
  640. self.assertFalse(FeatureCompleteToy._meta.ordering)
  641. self.assertEqual(
  642. [obj.name for obj in response.context["object_list"]],
  643. [
  644. "AAAAAAAAAA",
  645. "BBBBBBBBBB",
  646. "CCCCCCCCCC",
  647. "DDDDDDDDDD",
  648. ],
  649. )
  650. def test_custom_order_from_from_viewset(self):
  651. response = self.client.get(reverse("fctoy-alt3:index"))
  652. # The view has an ordering but it is overwritten by the viewset
  653. self.assertFalse(FeatureCompleteToy._meta.ordering)
  654. self.assertEqual(
  655. [obj.name for obj in response.context["object_list"]],
  656. [
  657. "CCCCCCCCCC",
  658. "AAAAAAAAAA",
  659. "DDDDDDDDDD",
  660. "BBBBBBBBBB",
  661. ],
  662. )
  663. class TestBreadcrumbs(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
  664. def setUp(self):
  665. self.user = self.login()
  666. @classmethod
  667. def setUpTestData(cls):
  668. cls.object = FeatureCompleteToy.objects.create(name="Test Toy")
  669. def test_index_view(self):
  670. response = self.client.get(reverse("feature_complete_toy:index"))
  671. items = [
  672. {
  673. "url": "",
  674. "label": "Feature complete toys",
  675. }
  676. ]
  677. self.assertBreadcrumbsItemsRendered(items, response.content)
  678. def test_add_view(self):
  679. response = self.client.get(reverse("feature_complete_toy:add"))
  680. items = [
  681. {
  682. "url": reverse("feature_complete_toy:index"),
  683. "label": "Feature complete toys",
  684. },
  685. {
  686. "url": "",
  687. "label": "New: Feature complete toy",
  688. },
  689. ]
  690. self.assertBreadcrumbsItemsRendered(items, response.content)
  691. def test_edit_view(self):
  692. edit_url = reverse("feature_complete_toy:edit", args=(quote(self.object.pk),))
  693. response = self.client.get(edit_url)
  694. items = [
  695. {
  696. "url": reverse("feature_complete_toy:index"),
  697. "label": "Feature complete toys",
  698. },
  699. {
  700. "url": "",
  701. "label": str(self.object),
  702. },
  703. ]
  704. self.assertBreadcrumbsItemsRendered(items, response.content)
  705. def test_delete_view(self):
  706. delete_url = reverse(
  707. "feature_complete_toy:delete",
  708. args=(quote(self.object.pk),),
  709. )
  710. response = self.client.get(delete_url)
  711. self.assertBreadcrumbsNotRendered(response.content)
  712. def test_history_view(self):
  713. history_url = reverse(
  714. "feature_complete_toy:history",
  715. args=(quote(self.object.pk),),
  716. )
  717. response = self.client.get(history_url)
  718. items = [
  719. {
  720. "url": reverse("feature_complete_toy:index"),
  721. "label": "Feature complete toys",
  722. },
  723. {
  724. "url": reverse(
  725. "feature_complete_toy:edit", args=(quote(self.object.pk),)
  726. ),
  727. "label": str(self.object),
  728. },
  729. {
  730. "url": "",
  731. "label": "History",
  732. "sublabel": str(self.object),
  733. },
  734. ]
  735. self.assertBreadcrumbsItemsRendered(items, response.content)
  736. def test_usage_view(self):
  737. usage_url = reverse(
  738. "feature_complete_toy:usage",
  739. args=(quote(self.object.pk),),
  740. )
  741. response = self.client.get(usage_url)
  742. items = [
  743. {
  744. "url": reverse("feature_complete_toy:index"),
  745. "label": "Feature complete toys",
  746. },
  747. {
  748. "url": reverse(
  749. "feature_complete_toy:edit", args=(quote(self.object.pk),)
  750. ),
  751. "label": str(self.object),
  752. },
  753. {
  754. "url": "",
  755. "label": "Usage",
  756. "sublabel": str(self.object),
  757. },
  758. ]
  759. self.assertBreadcrumbsItemsRendered(items, response.content)
  760. def test_inspect_view(self):
  761. inspect_url = reverse(
  762. "feature_complete_toy:inspect",
  763. args=(quote(self.object.pk),),
  764. )
  765. response = self.client.get(inspect_url)
  766. items = [
  767. {
  768. "url": reverse("feature_complete_toy:index"),
  769. "label": "Feature complete toys",
  770. },
  771. {
  772. "url": reverse(
  773. "feature_complete_toy:edit", args=(quote(self.object.pk),)
  774. ),
  775. "label": str(self.object),
  776. },
  777. {
  778. "url": "",
  779. "label": "Inspect",
  780. "sublabel": str(self.object),
  781. },
  782. ]
  783. self.assertBreadcrumbsItemsRendered(items, response.content)
  784. class TestLegacyPatterns(WagtailTestUtils, TestCase):
  785. # RemovedInWagtail70Warning: legacy integer pk-based URLs will be removed
  786. def setUp(self):
  787. self.user = self.login()
  788. @classmethod
  789. def setUpTestData(cls):
  790. cls.object = JSONStreamModel.objects.create(
  791. body='[{"type": "text", "value": "foo"}]',
  792. )
  793. def test_legacy_edit(self):
  794. edit_url = reverse("streammodel:edit", args=(quote(self.object.pk),))
  795. legacy_edit_url = "/admin/streammodel/1/"
  796. with self.assertWarnsRegex(
  797. RemovedInWagtail70Warning,
  798. "`/<pk>/` edit view URL pattern has been deprecated in favour of /edit/<pk>/.",
  799. ):
  800. response = self.client.get(legacy_edit_url)
  801. self.assertEqual(edit_url, "/admin/streammodel/edit/1/")
  802. self.assertRedirects(response, edit_url, 301)
  803. def test_legacy_delete(self):
  804. delete_url = reverse("streammodel:delete", args=(quote(self.object.pk),))
  805. legacy_delete_url = "/admin/streammodel/1/delete/"
  806. with self.assertWarnsRegex(
  807. RemovedInWagtail70Warning,
  808. "`/<pk>/delete/` delete view URL pattern has been deprecated in favour of /delete/<pk>/.",
  809. ):
  810. response = self.client.get(legacy_delete_url)
  811. self.assertEqual(delete_url, "/admin/streammodel/delete/1/")
  812. self.assertRedirects(response, delete_url, 301)
  813. class TestHistoryView(WagtailTestUtils, TestCase):
  814. @classmethod
  815. def setUpTestData(cls):
  816. cls.user = cls.create_test_user()
  817. cls.object = FeatureCompleteToy.objects.create(name="Buzz")
  818. cls.url = reverse(
  819. "feature_complete_toy:history",
  820. args=(quote(cls.object.pk),),
  821. )
  822. content_type = ContentType.objects.get_for_model(FeatureCompleteToy)
  823. cls.timestamp_1 = datetime.datetime(2021, 9, 30, 10, 1, 0)
  824. cls.timestamp_2 = datetime.datetime(2022, 5, 10, 12, 34, 0)
  825. if settings.USE_TZ:
  826. cls.timestamp_1 = make_aware(cls.timestamp_1)
  827. cls.timestamp_2 = make_aware(cls.timestamp_2)
  828. ModelLogEntry.objects.create(
  829. content_type=content_type,
  830. label="Test Buzz",
  831. action="wagtail.create",
  832. user=cls.user,
  833. timestamp=cls.timestamp_1,
  834. object_id=cls.object.pk,
  835. )
  836. ModelLogEntry.objects.create(
  837. content_type=content_type,
  838. label="Test Buzz Updated",
  839. action="wagtail.edit",
  840. user=cls.user,
  841. timestamp=cls.timestamp_2,
  842. object_id=cls.object.pk,
  843. )
  844. def setUp(self):
  845. self.login(self.user)
  846. def test_simple(self):
  847. expected = (
  848. ("Edited", str(self.user), date_format(self.timestamp_2, "c")),
  849. ("Created", str(self.user), date_format(self.timestamp_1, "c")),
  850. )
  851. response = self.client.get(self.url)
  852. soup = self.get_soup(response.content)
  853. rows = soup.select("tbody tr")
  854. self.assertEqual(response.status_code, 200)
  855. self.assertEqual(len(rows), 2)
  856. rendered_rows = []
  857. for row in rows:
  858. cells = []
  859. tds = row.select("td")
  860. self.assertEqual(len(tds), 3)
  861. cells.append(tds[0].text.strip())
  862. cells.append(tds[1].text.strip())
  863. cells.append(tds[2].select_one("time").attrs.get("datetime"))
  864. rendered_rows.append(cells)
  865. for rendered_row, expected_row in zip(rendered_rows, expected):
  866. self.assertSequenceEqual(rendered_row, expected_row)
  867. def test_filters(self):
  868. response = self.client.get(self.url, {"action": "wagtail.edit"})
  869. soup = self.get_soup(response.content)
  870. rows = soup.select("tbody tr")
  871. self.assertEqual(response.status_code, 200)
  872. self.assertEqual(len(rows), 1)
  873. self.assertEqual(rows[0].select_one("td").text.strip(), "Edited")
  874. response = self.client.get(self.url, {"action": "wagtail.create"})
  875. soup = self.get_soup(response.content)
  876. rows = soup.select("tbody tr")
  877. heading = soup.select_one('h2[role="alert"]')
  878. self.assertEqual(response.status_code, 200)
  879. self.assertEqual(heading.string.strip(), "There is 1 match")
  880. self.assertEqual(len(rows), 1)
  881. self.assertEqual(rows[0].select_one("td").text.strip(), "Created")
  882. def test_filtered_no_results(self):
  883. response = self.client.get(self.url, {"timestamp_before": "2020-01-01"})
  884. soup = self.get_soup(response.content)
  885. results = soup.select_one("#listing-results")
  886. table = soup.select_one("table")
  887. p = results.select_one("p")
  888. self.assertEqual(response.status_code, 200)
  889. self.assertIsNotNone(results)
  890. self.assertIsNone(table)
  891. self.assertIsNotNone(p)
  892. self.assertEqual(p.text.strip(), "No log entries match your query.")
  893. def test_empty(self):
  894. ModelLogEntry.objects.all().delete()
  895. response = self.client.get(self.url)
  896. soup = self.get_soup(response.content)
  897. results = soup.select_one("#listing-results")
  898. table = soup.select_one("table")
  899. self.assertEqual(response.status_code, 200)
  900. self.assertIsNotNone(results)
  901. self.assertEqual(results.text.strip(), "There are no log entries to display.")
  902. self.assertIsNone(table)
  903. def test_edit_view_links_to_history_view(self):
  904. edit_url = reverse("feature_complete_toy:edit", args=(quote(self.object.pk),))
  905. response = self.client.get(edit_url)
  906. soup = self.get_soup(response.content)
  907. header = soup.select_one(".w-slim-header")
  908. history_link = header.find("a", attrs={"href": self.url})
  909. self.assertIsNotNone(history_link)
  910. class TestUsageView(WagtailTestUtils, TestCase):
  911. @classmethod
  912. def setUpTestData(cls):
  913. cls.user = cls.create_test_user()
  914. cls.object = FeatureCompleteToy.objects.create(name="Buzz")
  915. cls.url = reverse(
  916. "feature_complete_toy:usage",
  917. args=(quote(cls.object.pk),),
  918. )
  919. cls.tbx = VariousOnDeleteModel.objects.create(
  920. text="Toybox", cascading_toy=cls.object
  921. )
  922. def setUp(self):
  923. self.user = self.login(self.user)
  924. def test_simple(self):
  925. response = self.client.get(self.url)
  926. self.assertEqual(response.status_code, 200)
  927. soup = self.get_soup(response.content)
  928. h1 = soup.select_one("h1")
  929. self.assertEqual(h1.text.strip(), f"Usage: {self.object}")
  930. tds = soup.select("tbody tr td")
  931. self.assertEqual(len(tds), 3)
  932. self.assertEqual(tds[0].text.strip(), str(self.tbx))
  933. self.assertEqual(tds[1].text.strip(), "Various on delete model")
  934. self.assertEqual(tds[2].text.strip(), "Cascading toy")
  935. tbx_edit_url = AdminURLFinder(self.user).get_edit_url(self.tbx)
  936. # Link to referrer's edit view
  937. link = tds[0].select_one("a")
  938. self.assertIsNotNone(link)
  939. self.assertEqual(link.attrs.get("href"), tbx_edit_url)
  940. # Link to referrer's edit view with parameters for the specific field
  941. link = tds[2].select_one("a")
  942. self.assertIsNotNone(link)
  943. self.assertIn(tbx_edit_url, link.attrs.get("href"))
  944. def test_usage_without_permission(self):
  945. self.user.is_superuser = False
  946. self.user.save()
  947. admin_permission = Permission.objects.get(
  948. content_type__app_label="wagtailadmin", codename="access_admin"
  949. )
  950. self.user.user_permissions.add(admin_permission)
  951. response = self.client.get(self.url)
  952. self.assertEqual(response.status_code, 302)
  953. self.assertRedirects(response, reverse("wagtailadmin_home"))
  954. def test_usage_without_permission_on_referrer(self):
  955. self.user.is_superuser = False
  956. self.user.save()
  957. admin_permission = Permission.objects.get(
  958. content_type__app_label="wagtailadmin", codename="access_admin"
  959. )
  960. toy_edit_permission = Permission.objects.get(
  961. content_type__app_label="tests", codename="change_featurecompletetoy"
  962. )
  963. self.user.user_permissions.add(admin_permission, toy_edit_permission)
  964. response = self.client.get(self.url)
  965. self.assertEqual(response.status_code, 200)
  966. soup = self.get_soup(response.content)
  967. h1 = soup.select_one("h1")
  968. self.assertEqual(h1.text.strip(), f"Usage: {self.object}")
  969. tds = soup.select("tbody tr td")
  970. self.assertEqual(len(tds), 3)
  971. self.assertEqual(tds[0].text.strip(), "(Private various on delete model)")
  972. self.assertEqual(tds[1].text.strip(), "Various on delete model")
  973. self.assertEqual(tds[2].text.strip(), "Cascading toy")
  974. # Not link to referrer's edit view
  975. link = tds[0].select_one("a")
  976. self.assertIsNone(link)
  977. # Not link to referrer's edit view
  978. link = tds[2].select_one("a")
  979. self.assertIsNone(link)
  980. def test_usage_with_describe_on_delete(self):
  981. response = self.client.get(self.url + "?describe_on_delete=1")
  982. self.assertEqual(response.status_code, 200)
  983. soup = self.get_soup(response.content)
  984. h1 = soup.select_one("h1")
  985. self.assertEqual(h1.text.strip(), f"Usage: {self.object}")
  986. tds = soup.select("tbody tr td")
  987. self.assertEqual(len(tds), 3)
  988. self.assertEqual(tds[0].text.strip(), str(self.tbx))
  989. self.assertEqual(tds[1].text.strip(), "Various on delete model")
  990. self.assertEqual(
  991. tds[2].text.strip(),
  992. "Cascading toy: the various on delete model will also be deleted",
  993. )
  994. tbx_edit_url = AdminURLFinder(self.user).get_edit_url(self.tbx)
  995. # Link to referrer's edit view
  996. link = tds[0].select_one("a")
  997. self.assertIsNotNone(link)
  998. self.assertEqual(link.attrs.get("href"), tbx_edit_url)
  999. # Link to referrer's edit view with parameters for the specific field
  1000. link = tds[2].select_one("a")
  1001. self.assertIsNotNone(link)
  1002. self.assertIn(tbx_edit_url, link.attrs.get("href"))
  1003. def test_empty(self):
  1004. self.tbx.delete()
  1005. response = self.client.get(self.url)
  1006. soup = self.get_soup(response.content)
  1007. results = soup.select_one("#listing-results")
  1008. table = soup.select_one("table")
  1009. self.assertEqual(response.status_code, 200)
  1010. self.assertIsNotNone(results)
  1011. self.assertEqual(results.text.strip(), "There are no results.")
  1012. self.assertIsNone(table)
  1013. def test_edit_view_links_to_usage_view(self):
  1014. edit_url = reverse("feature_complete_toy:edit", args=(quote(self.object.pk),))
  1015. response = self.client.get(edit_url)
  1016. soup = self.get_soup(response.content)
  1017. side_panel = soup.select_one("[data-side-panel='status']")
  1018. usage_link = side_panel.find("a", attrs={"href": self.url})
  1019. self.assertIsNotNone(usage_link)
  1020. def test_delete_view_links_to_usage_view(self):
  1021. edit_url = reverse("feature_complete_toy:delete", args=(quote(self.object.pk),))
  1022. response = self.client.get(edit_url)
  1023. soup = self.get_soup(response.content)
  1024. usage_link = soup.find("a", attrs={"href": self.url + "?describe_on_delete=1"})
  1025. self.assertIsNotNone(usage_link)
  1026. class TestInspectView(WagtailTestUtils, TestCase):
  1027. def setUp(self):
  1028. self.user = self.login()
  1029. @classmethod
  1030. def setUpTestData(cls):
  1031. cls.object = FeatureCompleteToy.objects.create(name="Test Toy")
  1032. cls.url = reverse("feature_complete_toy:inspect", args=(quote(cls.object.pk),))
  1033. cls.edit_url = reverse(
  1034. "feature_complete_toy:edit", args=(quote(cls.object.pk),)
  1035. )
  1036. cls.delete_url = reverse(
  1037. "feature_complete_toy:delete", args=(quote(cls.object.pk),)
  1038. )
  1039. def test_simple(self):
  1040. response = self.client.get(self.url)
  1041. expected_fields = ["Strid", "Release date"]
  1042. expected_values = [
  1043. # The pk may contain whitespace at the start/end, it's hard to
  1044. # distinguish from the whitespace in the HTML so just strip it
  1045. self.object.pk.strip(),
  1046. localize(self.object.release_date),
  1047. ]
  1048. self.assertEqual(response.status_code, 200)
  1049. self.assertTemplateUsed(response, "wagtailadmin/generic/inspect.html")
  1050. soup = self.get_soup(response.content)
  1051. fields = [dt.text.strip() for dt in soup.select("dt")]
  1052. values = [dd.text.strip() for dd in soup.select("dd")]
  1053. self.assertEqual(fields, expected_fields)
  1054. self.assertEqual(values, expected_values)
  1055. # One in the breadcrumb, one at the bottom
  1056. self.assertEqual(len(soup.find_all("a", attrs={"href": self.edit_url})), 2)
  1057. self.assertEqual(len(soup.find_all("a", attrs={"href": self.delete_url})), 1)
  1058. def test_inspect_view_fields(self):
  1059. # The alt1 viewset has a custom inspect_view_fields and inspect_view_fields_exclude
  1060. response = self.client.get(
  1061. reverse("fctoy_alt1:inspect", args=(quote(self.object.pk),))
  1062. )
  1063. expected_fields = ["Name"]
  1064. expected_values = ["Test Toy"]
  1065. self.assertEqual(response.status_code, 200)
  1066. self.assertTemplateUsed(response, "wagtailadmin/generic/inspect.html")
  1067. soup = self.get_soup(response.content)
  1068. fields = [dt.text.strip() for dt in soup.select("dt")]
  1069. values = [dd.text.strip() for dd in soup.select("dd")]
  1070. self.assertEqual(fields, expected_fields)
  1071. self.assertEqual(values, expected_values)
  1072. def test_disabled(self):
  1073. # An alternate viewset for the same model without inspect_view_enabled = True
  1074. with self.assertRaises(NoReverseMatch):
  1075. reverse("fctoy-alt2:inspect", args=(quote(self.object.pk),))
  1076. def test_without_permission(self):
  1077. self.user.is_superuser = False
  1078. self.user.save()
  1079. admin_permission = Permission.objects.get(
  1080. content_type__app_label="wagtailadmin", codename="access_admin"
  1081. )
  1082. self.user.user_permissions.add(admin_permission)
  1083. response = self.client.get(self.url)
  1084. self.assertEqual(response.status_code, 302)
  1085. self.assertRedirects(response, reverse("wagtailadmin_home"))
  1086. def test_only_add_permission(self):
  1087. self.user.is_superuser = False
  1088. self.user.user_permissions.add(
  1089. Permission.objects.get(
  1090. content_type__app_label="wagtailadmin", codename="access_admin"
  1091. ),
  1092. Permission.objects.get(
  1093. content_type__app_label=self.object._meta.app_label,
  1094. codename=get_permission_codename("add", self.object._meta),
  1095. ),
  1096. )
  1097. self.user.save()
  1098. response = self.client.get(self.url)
  1099. expected_fields = ["Strid", "Release date"]
  1100. expected_values = [
  1101. # The pk may contain whitespace at the start/end, it's hard to
  1102. # distinguish from the whitespace in the HTML so just strip it
  1103. self.object.pk.strip(),
  1104. localize(self.object.release_date),
  1105. ]
  1106. self.assertEqual(response.status_code, 200)
  1107. self.assertTemplateUsed(response, "wagtailadmin/generic/inspect.html")
  1108. soup = self.get_soup(response.content)
  1109. fields = [dt.text.strip() for dt in soup.select("dt")]
  1110. values = [dd.text.strip() for dd in soup.select("dd")]
  1111. self.assertEqual(fields, expected_fields)
  1112. self.assertEqual(values, expected_values)
  1113. self.assertEqual(len(soup.find_all("a", attrs={"href": self.edit_url})), 0)
  1114. self.assertEqual(len(soup.find_all("a", attrs={"href": self.delete_url})), 0)
  1115. class TestListingButtons(WagtailTestUtils, TestCase):
  1116. def setUp(self):
  1117. self.user = self.login()
  1118. @classmethod
  1119. def setUpTestData(cls):
  1120. cls.object = FeatureCompleteToy.objects.create(name="Test Toy")
  1121. def test_simple(self):
  1122. response = self.client.get(reverse("feature_complete_toy:index"))
  1123. self.assertEqual(response.status_code, 200)
  1124. self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html")
  1125. soup = self.get_soup(response.content)
  1126. actions = soup.select_one("tbody tr td ul.actions")
  1127. more_dropdown = actions.select_one("li [data-controller='w-dropdown']")
  1128. self.assertIsNotNone(more_dropdown)
  1129. more_button = more_dropdown.select_one("button")
  1130. self.assertEqual(
  1131. more_button.attrs.get("aria-label").strip(),
  1132. f"More options for '{self.object}'",
  1133. )
  1134. expected_buttons = [
  1135. (
  1136. "Edit",
  1137. f"Edit '{self.object}'",
  1138. reverse("feature_complete_toy:edit", args=[quote(self.object.pk)]),
  1139. ),
  1140. (
  1141. "Inspect",
  1142. f"Inspect '{self.object}'",
  1143. reverse("feature_complete_toy:inspect", args=[quote(self.object.pk)]),
  1144. ),
  1145. (
  1146. "Delete",
  1147. f"Delete '{self.object}'",
  1148. reverse("feature_complete_toy:delete", args=[quote(self.object.pk)]),
  1149. ),
  1150. ]
  1151. rendered_buttons = more_dropdown.select("a")
  1152. self.assertEqual(len(rendered_buttons), len(expected_buttons))
  1153. for rendered_button, (label, aria_label, url) in zip(
  1154. rendered_buttons, expected_buttons
  1155. ):
  1156. self.assertEqual(rendered_button.text.strip(), label)
  1157. self.assertEqual(rendered_button.attrs.get("aria-label"), aria_label)
  1158. self.assertEqual(rendered_button.attrs.get("href"), url)
  1159. class TestEditHandler(WagtailTestUtils, TestCase):
  1160. def setUp(self):
  1161. self.user = self.login()
  1162. @classmethod
  1163. def setUpTestData(cls):
  1164. cls.object = FeatureCompleteToy.objects.create(name="Test Toy")
  1165. cls.url = reverse("feature_complete_toy:edit", args=(quote(cls.object.pk),))
  1166. def test_edit_form_rendered_with_panels(self):
  1167. response = self.client.get(self.url)
  1168. self.assertEqual(response.status_code, 200)
  1169. self.assertTemplateUsed(response, "wagtailadmin/shared/panel.html")
  1170. soup = self.get_soup(response.content)
  1171. # Minimap should be rendered
  1172. minimap_container = soup.select_one("[data-minimap-container]")
  1173. self.assertIsNotNone(minimap_container)
  1174. # Form should be rendered using panels
  1175. panels = soup.select("[data-panel]")
  1176. self.assertEqual(len(panels), 2)
  1177. headings = ["Name", "Release date"]
  1178. for expected_heading, panel in zip(headings, panels):
  1179. rendered_heading = panel.select_one("[data-panel-heading-text]")
  1180. self.assertIsNotNone(rendered_heading)
  1181. self.assertEqual(rendered_heading.text.strip(), expected_heading)
  1182. class TestDefaultMessages(WagtailTestUtils, TestCase):
  1183. def setUp(self):
  1184. self.user = self.login()
  1185. @classmethod
  1186. def setUpTestData(cls):
  1187. cls.object = FeatureCompleteToy.objects.create(name="Test Toy")
  1188. cls.create_url = reverse("feature_complete_toy:add")
  1189. cls.edit_url = reverse(
  1190. "feature_complete_toy:edit", args=(quote(cls.object.pk),)
  1191. )
  1192. cls.delete_url = reverse(
  1193. "feature_complete_toy:delete", args=(quote(cls.object.pk),)
  1194. )
  1195. def test_create_error(self):
  1196. response = self.client.post(
  1197. self.create_url,
  1198. data={"name": "", "release_date": "2024-01-11"},
  1199. )
  1200. self.assertEqual(response.status_code, 200)
  1201. self.assertContains(
  1202. response,
  1203. escape("The feature complete toy could not be created due to errors."),
  1204. )
  1205. def test_create_success(self):
  1206. response = self.client.post(
  1207. self.create_url,
  1208. data={"name": "Pink Flamingo", "release_date": "2024-01-11"},
  1209. follow=True,
  1210. )
  1211. self.assertEqual(response.status_code, 200)
  1212. self.assertContains(
  1213. response,
  1214. escape("Feature complete toy 'Pink Flamingo (2024-01-11)' created."),
  1215. )
  1216. def test_edit_error(self):
  1217. response = self.client.post(
  1218. self.edit_url, data={"name": "", "release_date": "2024-01-11"}
  1219. )
  1220. self.assertEqual(response.status_code, 200)
  1221. self.assertContains(
  1222. response,
  1223. escape("The feature complete toy could not be saved due to errors."),
  1224. )
  1225. def test_edit_success(self):
  1226. response = self.client.post(
  1227. self.edit_url,
  1228. data={"name": "rubberduck", "release_date": "2024-02-01"},
  1229. follow=True,
  1230. )
  1231. self.assertEqual(response.status_code, 200)
  1232. self.assertContains(
  1233. response,
  1234. escape("Feature complete toy 'rubberduck (2024-02-01)' updated."),
  1235. )
  1236. def test_delete_success(self):
  1237. response = self.client.post(self.delete_url, follow=True)
  1238. self.assertEqual(response.status_code, 200)
  1239. self.assertContains(
  1240. response,
  1241. escape(f"Feature complete toy '{self.object}' deleted."),
  1242. )