wagtail 6.3.2__py3-none-any.whl → 6.4rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/actions/publish_revision.py +4 -5
  3. wagtail/admin/auth.py +0 -2
  4. wagtail/admin/checks.py +1 -1
  5. wagtail/admin/filters.py +3 -1
  6. wagtail/admin/forms/account.py +21 -11
  7. wagtail/admin/forms/collections.py +2 -9
  8. wagtail/admin/forms/formsets.py +32 -0
  9. wagtail/admin/forms/pages.py +5 -1
  10. wagtail/admin/forms/workflows.py +2 -13
  11. wagtail/admin/locale/ar/LC_MESSAGES/django.mo +0 -0
  12. wagtail/admin/locale/ar/LC_MESSAGES/django.po +68 -1
  13. wagtail/admin/locale/ar/LC_MESSAGES/djangojs.mo +0 -0
  14. wagtail/admin/locale/ar/LC_MESSAGES/djangojs.po +5 -1
  15. wagtail/admin/locale/en/LC_MESSAGES/django.po +312 -356
  16. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +21 -16
  17. wagtail/admin/menu.py +0 -13
  18. wagtail/admin/panels/base.py +2 -2
  19. wagtail/admin/panels/group.py +4 -1
  20. wagtail/admin/panels/inline_panel.py +5 -2
  21. wagtail/admin/panels/model_utils.py +36 -0
  22. wagtail/admin/panels/page_utils.py +2 -40
  23. wagtail/admin/panels/signal_handlers.py +0 -2
  24. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  25. wagtail/admin/static/wagtailadmin/css/panels/draftail.css +1 -1
  26. wagtail/admin/static/wagtailadmin/css/panels/streamfield.css +1 -1
  27. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  28. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  29. wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +1 -8
  30. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  31. wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
  32. wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
  33. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  34. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  35. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  36. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  37. wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +1 -1
  38. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  39. wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +7 -0
  40. wagtail/admin/templates/wagtailadmin/404.html +4 -0
  41. wagtail/admin/templates/wagtailadmin/chooser/browse.html +2 -1
  42. wagtail/admin/templates/wagtailadmin/chooser/tables/parent_page_cell.html +1 -1
  43. wagtail/admin/templates/wagtailadmin/collections/_privacy_switch.html +8 -1
  44. wagtail/admin/templates/wagtailadmin/generic/confirm_delete.html +15 -9
  45. wagtail/admin/templates/wagtailadmin/generic/confirm_unpublish.html +21 -25
  46. wagtail/admin/templates/wagtailadmin/generic/form.html +1 -1
  47. wagtail/admin/templates/wagtailadmin/generic/preview_error.html +3 -0
  48. wagtail/admin/templates/wagtailadmin/generic/revisions/compare.html +63 -76
  49. wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -2
  50. wagtail/admin/templates/wagtailadmin/pages/edit.html +1 -5
  51. wagtail/admin/templates/wagtailadmin/panels/inline_panel_child.html +1 -0
  52. wagtail/admin/templates/wagtailadmin/permissions/includes/collection_member_permissions_form.html +1 -1
  53. wagtail/admin/templates/wagtailadmin/permissions/includes/collection_member_permissions_formset.html +6 -22
  54. wagtail/admin/templates/wagtailadmin/shared/formatted_field.html +2 -2
  55. wagtail/admin/templates/wagtailadmin/shared/header.html +2 -2
  56. wagtail/admin/templates/wagtailadmin/shared/page_status_tag_new.html +32 -39
  57. wagtail/admin/templates/wagtailadmin/shared/revisions/confirm_unschedule.html +13 -17
  58. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/privacy.html +15 -3
  59. wagtail/admin/templates/wagtailadmin/shared/side_panels/preview.html +1 -1
  60. wagtail/admin/templates/wagtailadmin/skeleton.html +4 -2
  61. wagtail/admin/templates/wagtailadmin/workflows/create.html +1 -1
  62. wagtail/admin/templates/wagtailadmin/workflows/edit.html +1 -1
  63. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_pages_form.html +1 -1
  64. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_pages_formset.html +6 -23
  65. wagtail/admin/templatetags/wagtailadmin_tags.py +12 -0
  66. wagtail/admin/templatetags/wagtailuserbar.py +2 -3
  67. wagtail/admin/tests/pages/test_create_page.py +110 -1
  68. wagtail/admin/tests/pages/test_edit_page.py +3 -2
  69. wagtail/admin/tests/pages/test_explorer_view.py +18 -0
  70. wagtail/admin/tests/pages/test_page_usage.py +24 -20
  71. wagtail/admin/tests/pages/test_preview.py +69 -1
  72. wagtail/admin/tests/pages/test_revisions.py +40 -6
  73. wagtail/admin/tests/test_account_management.py +39 -1
  74. wagtail/admin/tests/test_audit_log.py +4 -2
  75. wagtail/admin/tests/test_block_preview.py +224 -0
  76. wagtail/admin/tests/test_edit_handlers.py +23 -6
  77. wagtail/admin/tests/test_page_chooser.py +50 -3
  78. wagtail/admin/tests/test_privacy.py +49 -26
  79. wagtail/admin/tests/test_site_summary.py +15 -10
  80. wagtail/admin/tests/test_templatetags.py +19 -0
  81. wagtail/admin/tests/test_userbar.py +82 -1
  82. wagtail/admin/tests/test_views_generic.py +27 -12
  83. wagtail/admin/tests/test_workflows.py +69 -0
  84. wagtail/admin/tests/tests.py +23 -4
  85. wagtail/admin/tests/ui/test_sidebar.py +1 -1
  86. wagtail/admin/tests/viewsets/test_model_viewset.py +15 -13
  87. wagtail/admin/ui/side_panels.py +7 -4
  88. wagtail/admin/urls/__init__.py +6 -0
  89. wagtail/admin/urls/pages.py +1 -1
  90. wagtail/admin/userbar.py +21 -1
  91. wagtail/admin/views/account.py +5 -0
  92. wagtail/admin/views/chooser.py +5 -1
  93. wagtail/admin/views/collections.py +0 -2
  94. wagtail/admin/views/generic/base.py +20 -10
  95. wagtail/admin/views/generic/history.py +0 -1
  96. wagtail/admin/views/generic/models.py +79 -21
  97. wagtail/admin/views/generic/preview.py +50 -1
  98. wagtail/admin/views/mixins.py +4 -2
  99. wagtail/admin/views/pages/bulk_actions/delete.py +11 -23
  100. wagtail/admin/views/pages/bulk_actions/page_bulk_action.py +17 -0
  101. wagtail/admin/views/pages/bulk_actions/publish.py +11 -31
  102. wagtail/admin/views/pages/bulk_actions/unpublish.py +11 -31
  103. wagtail/admin/views/pages/create.py +1 -0
  104. wagtail/admin/views/pages/edit.py +38 -30
  105. wagtail/admin/views/pages/revisions.py +43 -114
  106. wagtail/admin/views/pages/utils.py +0 -1
  107. wagtail/admin/views/tags.py +6 -2
  108. wagtail/admin/views/workflows.py +8 -6
  109. wagtail/admin/viewsets/model.py +0 -4
  110. wagtail/admin/viewsets/pages.py +0 -1
  111. wagtail/admin/widgets/tags.py +1 -0
  112. wagtail/api/v2/tests/test_documents.py +4 -2
  113. wagtail/api/v2/tests/test_images.py +4 -2
  114. wagtail/api/v2/tests/test_pages.py +8 -4
  115. wagtail/blocks/base.py +59 -1
  116. wagtail/blocks/field_block.py +6 -0
  117. wagtail/blocks/list_block.py +4 -0
  118. wagtail/blocks/static_block.py +3 -0
  119. wagtail/blocks/stream_block.py +5 -1
  120. wagtail/blocks/struct_block.py +6 -0
  121. wagtail/compat.py +16 -0
  122. wagtail/contrib/forms/forms.py +27 -7
  123. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +2 -2
  124. wagtail/contrib/forms/tests/test_models.py +7 -5
  125. wagtail/contrib/forms/tests/test_views.py +75 -0
  126. wagtail/contrib/frontend_cache/tasks.py +83 -0
  127. wagtail/contrib/frontend_cache/tests.py +47 -32
  128. wagtail/contrib/frontend_cache/utils.py +2 -70
  129. wagtail/contrib/redirects/base_formats.py +2 -2
  130. wagtail/contrib/redirects/locale/ar/LC_MESSAGES/django.mo +0 -0
  131. wagtail/contrib/redirects/locale/ar/LC_MESSAGES/django.po +3 -0
  132. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +24 -37
  133. wagtail/contrib/redirects/templates/wagtailredirects/add.html +1 -24
  134. wagtail/contrib/redirects/templates/wagtailredirects/confirm_delete.html +3 -13
  135. wagtail/contrib/redirects/tests/test_redirects.py +122 -110
  136. wagtail/contrib/redirects/tests/test_signal_handlers.py +75 -69
  137. wagtail/contrib/redirects/urls.py +2 -2
  138. wagtail/contrib/redirects/views.py +35 -73
  139. wagtail/contrib/search_promotions/admin_urls.py +10 -3
  140. wagtail/contrib/search_promotions/forms.py +55 -26
  141. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +44 -54
  142. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/add.html +21 -31
  143. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/confirm_delete.html +3 -12
  144. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/edit.html +11 -34
  145. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/includes/searchpromotion_form.html +1 -0
  146. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js +2 -1
  147. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index.html +0 -1
  148. wagtail/contrib/search_promotions/tests.py +814 -13
  149. wagtail/contrib/search_promotions/views/__init__.py +1 -0
  150. wagtail/contrib/search_promotions/views/reports.py +56 -0
  151. wagtail/contrib/search_promotions/views/settings.py +258 -0
  152. wagtail/contrib/search_promotions/wagtail_hooks.py +12 -1
  153. wagtail/contrib/settings/locale/ar/LC_MESSAGES/django.mo +0 -0
  154. wagtail/contrib/settings/locale/ar/LC_MESSAGES/django.po +6 -1
  155. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +3 -3
  156. wagtail/contrib/settings/templates/wagtailsettings/edit.html +1 -5
  157. wagtail/contrib/settings/tests/generic/test_admin.py +2 -5
  158. wagtail/contrib/settings/tests/generic/test_register.py +1 -1
  159. wagtail/contrib/settings/tests/site_specific/test_admin.py +2 -5
  160. wagtail/contrib/settings/tests/site_specific/test_register.py +1 -1
  161. wagtail/contrib/settings/views.py +9 -23
  162. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  163. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +1 -1
  164. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  165. wagtail/contrib/table_block/tests.py +4 -1
  166. wagtail/contrib/typed_table_block/blocks.py +3 -0
  167. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
  168. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  169. wagtail/contrib/typed_table_block/tests.py +33 -0
  170. wagtail/documents/locale/en/LC_MESSAGES/django.po +26 -26
  171. wagtail/documents/migrations/0011_add_choose_permissions.py +1 -0
  172. wagtail/documents/models.py +1 -0
  173. wagtail/documents/signal_handlers.py +6 -2
  174. wagtail/documents/static/wagtaildocs/js/add-multiple.js +1 -1
  175. wagtail/documents/templates/wagtaildocs/documents/edit.html +1 -3
  176. wagtail/documents/templates/wagtaildocs/multiple/add.html +7 -1
  177. wagtail/documents/tests/test_admin_views.py +74 -33
  178. wagtail/documents/tests/test_views.py +21 -12
  179. wagtail/documents/views/chooser.py +1 -0
  180. wagtail/documents/views/documents.py +1 -2
  181. wagtail/documents/views/multiple.py +0 -1
  182. wagtail/documents/views/serve.py +9 -2
  183. wagtail/documents/wagtail_hooks.py +6 -1
  184. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  185. wagtail/embeds/oembed_providers.py +0 -64
  186. wagtail/fields.py +3 -0
  187. wagtail/images/apps.py +2 -1
  188. wagtail/images/blocks.py +6 -2
  189. wagtail/images/forms.py +40 -3
  190. wagtail/images/locale/ar/LC_MESSAGES/django.mo +0 -0
  191. wagtail/images/locale/ar/LC_MESSAGES/django.po +4 -0
  192. wagtail/images/locale/en/LC_MESSAGES/django.po +49 -49
  193. wagtail/images/migrations/0023_add_choose_permissions.py +1 -0
  194. wagtail/images/rich_text/contentstate.py +1 -0
  195. wagtail/images/rich_text/editor_html.py +1 -0
  196. wagtail/images/signal_handlers.py +17 -10
  197. wagtail/images/static/wagtailimages/js/add-multiple.js +1 -1
  198. wagtail/images/static/wagtailimages/js/image-block.js +1 -1
  199. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  200. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  201. wagtail/images/static/wagtailimages/js/image-url-generator.js +1 -1
  202. wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-image.js +1 -1
  203. wagtail/images/tasks.py +18 -0
  204. wagtail/images/templates/wagtailimages/images/edit.html +1 -3
  205. wagtail/images/templates/wagtailimages/images/url_generator.html +1 -1
  206. wagtail/images/templates/wagtailimages/multiple/add.html +7 -2
  207. wagtail/images/templates/wagtailimages/widgets/image_chooser.html +1 -1
  208. wagtail/images/tests/test_admin_views.py +53 -29
  209. wagtail/images/tests/test_blocks.py +3 -2
  210. wagtail/images/tests/test_models.py +12 -10
  211. wagtail/images/tests/tests.py +10 -0
  212. wagtail/images/views/chooser.py +1 -0
  213. wagtail/images/views/images.py +1 -3
  214. wagtail/images/views/multiple.py +0 -1
  215. wagtail/images/views/serve.py +18 -2
  216. wagtail/images/widgets.py +3 -0
  217. wagtail/locale/en/LC_MESSAGES/django.po +228 -216
  218. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  219. wagtail/management/commands/publish_scheduled.py +1 -1
  220. wagtail/migrations/0087_alter_grouppagepermission_unique_together_and_more.py +16 -8
  221. wagtail/models/__init__.py +300 -119
  222. wagtail/models/i18n.py +2 -2
  223. wagtail/models/panels.py +37 -0
  224. wagtail/models/sites.py +7 -6
  225. wagtail/permission_policies/pages.py +2 -2
  226. wagtail/project_template/project_name/settings/base.py +4 -0
  227. wagtail/project_template/requirements.txt +1 -1
  228. wagtail/query.py +145 -0
  229. wagtail/search/backends/database/mysql/mysql.py +25 -17
  230. wagtail/search/backends/database/postgres/postgres.py +44 -83
  231. wagtail/search/backends/database/sqlite/sqlite.py +25 -17
  232. wagtail/search/backends/elasticsearch7.py +4 -0
  233. wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
  234. wagtail/search/query.py +8 -2
  235. wagtail/search/signal_handlers.py +6 -9
  236. wagtail/search/tasks.py +10 -0
  237. wagtail/search/tests/test_elasticsearch7_backend.py +21 -0
  238. wagtail/search/tests/test_index_functions.py +10 -6
  239. wagtail/search/tests/test_postgres_backend.py +0 -14
  240. wagtail/signal_handlers.py +5 -20
  241. wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
  242. wagtail/snippets/locale/en/LC_MESSAGES/django.po +3 -13
  243. wagtail/snippets/tests/test_preview.py +5 -0
  244. wagtail/snippets/tests/test_snippets.py +100 -45
  245. wagtail/snippets/tests/test_usage.py +29 -24
  246. wagtail/snippets/tests/test_viewset.py +1 -1
  247. wagtail/snippets/views/snippets.py +0 -12
  248. wagtail/tasks.py +41 -0
  249. wagtail/templates/wagtailcore/shared/block_preview.html +29 -0
  250. wagtail/test/earlypage/__init__.py +0 -0
  251. wagtail/test/earlypage/migrations/0001_initial.py +37 -0
  252. wagtail/test/earlypage/migrations/__init__.py +0 -0
  253. wagtail/test/earlypage/models.py +14 -0
  254. wagtail/test/settings.py +3 -0
  255. wagtail/test/testapp/fixtures/test.json +7 -0
  256. wagtail/test/testapp/fixtures/test_specific.json +6 -3
  257. wagtail/test/testapp/models.py +58 -44
  258. wagtail/test/testapp/templates/tests/custom_block_preview.html +16 -0
  259. wagtail/test/testapp/templates/tests/static_block_preview.html +5 -0
  260. wagtail/test/testapp/wagtail_hooks.py +9 -0
  261. wagtail/tests/test_blocks.py +189 -2
  262. wagtail/tests/test_hooks.py +166 -1
  263. wagtail/tests/test_management_commands.py +54 -13
  264. wagtail/tests/test_page_allowed_http_methods.py +32 -0
  265. wagtail/tests/test_page_model.py +68 -0
  266. wagtail/tests/test_page_privacy.py +10 -0
  267. wagtail/tests/test_page_queryset.py +79 -0
  268. wagtail/tests/test_reference_index.py +84 -75
  269. wagtail/tests/test_streamfield.py +30 -0
  270. wagtail/tests/test_utils.py +61 -0
  271. wagtail/users/forms.py +2 -9
  272. wagtail/users/locale/en/LC_MESSAGES/django.po +17 -17
  273. wagtail/users/templates/wagtailusers/groups/create.html +0 -5
  274. wagtail/users/templates/wagtailusers/groups/includes/page_permissions_form.html +1 -1
  275. wagtail/users/templates/wagtailusers/groups/includes/page_permissions_formset.html +6 -6
  276. wagtail/users/tests/test_admin_views.py +96 -4
  277. wagtail/users/tests/test_utils.py +76 -0
  278. wagtail/users/utils.py +43 -11
  279. wagtail/utils/setup.py +2 -2
  280. wagtail/utils/templates.py +26 -0
  281. wagtail/utils/widgets.py +1 -0
  282. wagtail/views.py +9 -1
  283. wagtail/wagtail_hooks.py +67 -29
  284. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/METADATA +2 -2
  285. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/RECORD +289 -276
  286. wagtail/admin/static/wagtailadmin/js/expanding-formset.js +0 -1
  287. wagtail/admin/static/wagtailadmin/js/vendor/rangy-core.js +0 -1
  288. wagtail/admin/static/wagtailadmin/js/vendor/uuidv4.min.js +0 -1
  289. wagtail/contrib/search_promotions/views.py +0 -323
  290. wagtail/images/static/wagtailimages/js/vendor/canvas-to-blob.min.js +0 -1
  291. wagtail/users/static/wagtailusers/js/group-form.js +0 -1
  292. wagtail/users/templates/wagtailusers/groups/includes/group_form_js.html +0 -3
  293. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/LICENSE +0 -0
  294. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/WHEEL +0 -0
  295. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/entry_points.txt +0 -0
  296. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,224 @@
1
+ from django.contrib.auth.models import Permission
2
+ from django.http import HttpRequest
3
+ from django.test import TestCase
4
+ from django.urls import reverse
5
+ from django.utils.http import urlencode
6
+
7
+ from wagtail import blocks
8
+ from wagtail.test.utils import WagtailTestUtils
9
+
10
+
11
+ class TestStreamFieldBlockPreviewView(WagtailTestUtils, TestCase):
12
+ def get(self, block):
13
+ return self.client.get(
14
+ reverse("wagtailadmin_block_preview"),
15
+ {"id": block.definition_prefix},
16
+ )
17
+
18
+ def setUp(self):
19
+ self.user = self.login()
20
+
21
+ def test_simple(self):
22
+ block = blocks.CharBlock(
23
+ label="Single-line text",
24
+ description="A single line of text",
25
+ preview_value="Hello, world!",
26
+ )
27
+ response = self.get(block)
28
+ self.assertEqual(response.status_code, 200)
29
+ soup = self.get_soup(response.content)
30
+
31
+ html = soup.select_one("html")
32
+ self.assertIsNotNone(html)
33
+ self.assertEqual(html["lang"], "en")
34
+ self.assertEqual(html["dir"], "ltr")
35
+
36
+ robots = soup.select_one("meta[name=robots]")
37
+ self.assertIsNotNone(robots)
38
+ self.assertEqual(robots["content"], "noindex")
39
+
40
+ title = soup.select_one("title")
41
+ self.assertIsNotNone(title)
42
+ self.assertEqual(title.text.strip(), "Preview for Single-line text (CharBlock)")
43
+
44
+ main = soup.select_one("main")
45
+ self.assertIsNotNone(main)
46
+ self.assertEqual(main.text.strip(), "Hello, world!")
47
+
48
+ def test_nonexisting_block(self):
49
+ response = self.client.get(reverse("wagtailadmin_block_preview"))
50
+ self.assertEqual(response.status_code, 404)
51
+
52
+ response = self.client.get(
53
+ reverse("wagtailadmin_block_preview"),
54
+ {"id": "nonexisting"},
55
+ )
56
+ self.assertEqual(response.status_code, 404)
57
+
58
+ def test_no_admin_permission(self):
59
+ self.user.is_superuser = False
60
+ self.user.save()
61
+
62
+ block = blocks.CharBlock()
63
+ response = self.get(block)
64
+ self.assertRedirects(
65
+ response,
66
+ reverse("wagtailadmin_login")
67
+ + "?"
68
+ + urlencode({"next": response.wsgi_request.get_full_path()}),
69
+ )
70
+
71
+ def test_minimal_permission(self):
72
+ self.user.is_superuser = False
73
+ self.user.user_permissions.add(
74
+ Permission.objects.get(
75
+ content_type__app_label="wagtailadmin",
76
+ codename="access_admin",
77
+ )
78
+ )
79
+ self.user.save()
80
+
81
+ block = blocks.CharBlock(preview_value="Hello, world!")
82
+ response = self.get(block)
83
+ self.assertEqual(response.status_code, 200)
84
+
85
+ def test_no_preview_value_no_default(self):
86
+ block = blocks.Block()
87
+ response = self.get(block)
88
+ self.assertEqual(response.status_code, 200)
89
+ soup = self.get_soup(response.content)
90
+ main = soup.select_one("main")
91
+ self.assertIsNotNone(main)
92
+ self.assertEqual(main.text.strip(), "None")
93
+
94
+ def test_preview_value_falls_back_to_default(self):
95
+ block = blocks.IntegerBlock(default=42)
96
+ response = self.get(block)
97
+ self.assertEqual(response.status_code, 200)
98
+ soup = self.get_soup(response.content)
99
+ main = soup.select_one("main")
100
+ self.assertIsNotNone(main)
101
+ self.assertEqual(main.text.strip(), "42")
102
+
103
+ def test_preview_template(self):
104
+ class PreviewTemplateViaMeta(blocks.Block):
105
+ class Meta:
106
+ preview_template = "tests/custom_block_preview.html"
107
+
108
+ class PreviewTemplateViaMethod(blocks.Block):
109
+ def get_preview_template(self, value=None, context=None):
110
+ return "tests/custom_block_preview.html"
111
+
112
+ cases = [
113
+ ("meta", PreviewTemplateViaMeta()),
114
+ ("method", PreviewTemplateViaMethod()),
115
+ ("kwarg", blocks.Block(preview_template="tests/custom_block_preview.html")),
116
+ ]
117
+
118
+ for via, block in cases:
119
+ with self.subTest(via=via):
120
+ response = self.get(block)
121
+ self.assertEqual(response.status_code, 200)
122
+ self.assertTemplateUsed(response, "tests/custom_block_preview.html")
123
+
124
+ response = self.get(block)
125
+ self.assertEqual(response.status_code, 200)
126
+ self.assertTemplateUsed(response, "tests/custom_block_preview.html")
127
+
128
+ soup = self.get_soup(response.content)
129
+ custom_wrapper = soup.select_one("main .my-preview-wrapper")
130
+ self.assertIsNotNone(custom_wrapper)
131
+
132
+ custom_css = soup.select_one("link[rel=stylesheet]")
133
+ self.assertIsNotNone(custom_css)
134
+ self.assertEqual(custom_css["href"], "/static/css/custom.css")
135
+
136
+ custom_js = soup.select_one("script[src]")
137
+ self.assertIsNotNone(custom_js)
138
+ self.assertEqual(custom_js["src"], "/static/js/custom.js")
139
+
140
+ def test_preview_value(self):
141
+ class PreviewValueViaMeta(blocks.Block):
142
+ class Meta:
143
+ preview_value = "Hello, world!"
144
+
145
+ class PreviewValueViaMethod(blocks.Block):
146
+ def get_preview_value(self):
147
+ return "Hello, world!"
148
+
149
+ cases = [
150
+ ("meta", PreviewValueViaMeta()),
151
+ ("method", PreviewValueViaMethod()),
152
+ ("kwarg", blocks.Block(preview_value="Hello, world!")),
153
+ ]
154
+
155
+ for via, block in cases:
156
+ with self.subTest(via=via):
157
+ response = self.get(block)
158
+ self.assertEqual(response.status_code, 200)
159
+ soup = self.get_soup(response.content)
160
+ main = soup.select_one("main")
161
+ self.assertIsNotNone(main)
162
+ self.assertEqual(main.text.strip(), "Hello, world!")
163
+
164
+ def test_custom_preview_context(self):
165
+ preview_value = "With a custom context"
166
+ label = "Fancy block"
167
+
168
+ class CustomPreviewContextBlock(blocks.Block):
169
+ def get_preview_context(block, value, parent_context=None):
170
+ self.assertEqual(value, preview_value)
171
+ self.assertIsNotNone(parent_context)
172
+ self.assertIsInstance(parent_context.get("request"), HttpRequest)
173
+ self.assertIs(parent_context.get("block_def"), block)
174
+ self.assertIs(
175
+ parent_context.get("block_class"),
176
+ CustomPreviewContextBlock,
177
+ )
178
+ self.assertIsInstance(
179
+ parent_context.get("bound_block"),
180
+ blocks.BoundBlock,
181
+ )
182
+ self.assertEqual(
183
+ parent_context.get("page_title"),
184
+ "Preview for Fancy block (CustomPreviewContextBlock)",
185
+ )
186
+ return {
187
+ **parent_context,
188
+ "extra": "Added by get_preview_context",
189
+ "page_title": "Custom title",
190
+ }
191
+
192
+ block = CustomPreviewContextBlock(label=label, preview_value=preview_value)
193
+ response = self.get(block)
194
+ self.assertEqual(response.status_code, 200)
195
+ soup = self.get_soup(response.content)
196
+ main = soup.select_one("main")
197
+ self.assertIsNotNone(main)
198
+ self.assertEqual(main.text.strip(), preview_value)
199
+ # Ensure custom context can add and override values
200
+ self.assertEqual(response.context["extra"], "Added by get_preview_context")
201
+ self.assertEqual(response.context["page_title"], "Custom title")
202
+ # Ensure that the default context is still present
203
+ self.assertIs(response.context["block_def"], block)
204
+
205
+ def test_static_image_preview(self):
206
+ class StaticImagePreviewBlock(blocks.Block):
207
+ def get_preview_context(self, value, parent_context=None):
208
+ return {
209
+ "image_path": "block_previews/preview.jpg",
210
+ "image_description": "A preview of the block",
211
+ }
212
+
213
+ class Meta:
214
+ preview_template = "tests/static_block_preview.html"
215
+
216
+ block = StaticImagePreviewBlock()
217
+ response = self.get(block)
218
+ self.assertEqual(response.status_code, 200)
219
+ self.assertTemplateUsed(response, "tests/static_block_preview.html")
220
+ soup = self.get_soup(response.content)
221
+ img = soup.select_one("html body main img")
222
+ self.assertIsNotNone(img)
223
+ self.assertEqual(img["src"], "/static/block_previews/preview.jpg")
224
+ self.assertEqual(img["alt"], "A preview of the block")
@@ -30,6 +30,7 @@ from wagtail.admin.panels import (
30
30
  PublishingPanel,
31
31
  TabbedInterface,
32
32
  TitleFieldPanel,
33
+ expand_panel_list,
33
34
  extract_panel_definitions_from_model_class,
34
35
  get_form_for_model,
35
36
  )
@@ -1523,6 +1524,13 @@ class TestInlinePanel(WagtailTestUtils, TestCase):
1523
1524
  ),
1524
1525
  )
1525
1526
 
1527
+ def test_get_heading_and_label_from_field(self):
1528
+ panel = InlinePanel("social_links").bind_to_model(PersonPage)
1529
+ # Heading is the plural term, derived from the relation's related_name
1530
+ self.assertEqual(panel.heading, "Social links")
1531
+ # Label is the singular term, derived from the related model's verbose_name
1532
+ self.assertEqual(panel.label, "Social link")
1533
+
1526
1534
 
1527
1535
  class TestNonOrderableInlinePanel(WagtailTestUtils, TestCase):
1528
1536
  fixtures = ["test.json"]
@@ -1569,7 +1577,7 @@ class TestInlinePanelGetComparison(TestCase):
1569
1577
  self.request.user = user
1570
1578
 
1571
1579
  def test_get_comparison(self):
1572
- # Test whether the InlinePanel passes it's label in get_comparison
1580
+ # Test whether the InlinePanel passes its heading as the label in get_comparison
1573
1581
 
1574
1582
  page = Page.objects.get(id=4).specific
1575
1583
  comparison = (
@@ -1580,7 +1588,7 @@ class TestInlinePanelGetComparison(TestCase):
1580
1588
 
1581
1589
  comparison = [comp(page, page) for comp in comparison]
1582
1590
  field_labels = [comp.field_label() for comp in comparison]
1583
- self.assertIn("Speakers", field_labels)
1591
+ self.assertIn("Speaker lineup", field_labels)
1584
1592
 
1585
1593
 
1586
1594
  class TestInlinePanelRelatedModelPanelConfigChecks(TestCase):
@@ -1719,7 +1727,10 @@ class TestCommentPanel(WagtailTestUtils, TestCase):
1719
1727
  Test that the comment panel is missing if WAGTAILADMIN_COMMENTS_ENABLED=False
1720
1728
  """
1721
1729
  self.assertFalse(
1722
- any(isinstance(panel, CommentPanel) for panel in Page.settings_panels)
1730
+ any(
1731
+ isinstance(panel, CommentPanel)
1732
+ for panel in expand_panel_list(Page, Page.settings_panels)
1733
+ )
1723
1734
  )
1724
1735
  form_class = Page.get_edit_handler().get_form_class()
1725
1736
  form = form_class()
@@ -1730,7 +1741,10 @@ class TestCommentPanel(WagtailTestUtils, TestCase):
1730
1741
  Test that the comment panel is present by default
1731
1742
  """
1732
1743
  self.assertTrue(
1733
- any(isinstance(panel, CommentPanel) for panel in Page.settings_panels)
1744
+ any(
1745
+ isinstance(panel, CommentPanel)
1746
+ for panel in expand_panel_list(Page, Page.settings_panels)
1747
+ )
1734
1748
  )
1735
1749
  form_class = Page.get_edit_handler().get_form_class()
1736
1750
  form = form_class()
@@ -2017,7 +2031,10 @@ class TestPublishingPanel(WagtailTestUtils, TestCase):
2017
2031
  Test that the publishing panel is present by default
2018
2032
  """
2019
2033
  self.assertTrue(
2020
- any(isinstance(panel, PublishingPanel) for panel in Page.settings_panels)
2034
+ any(
2035
+ isinstance(panel, PublishingPanel)
2036
+ for panel in expand_panel_list(Page, Page.settings_panels)
2037
+ )
2021
2038
  )
2022
2039
  form_class = Page.get_edit_handler().get_form_class()
2023
2040
  form = form_class()
@@ -2078,7 +2095,7 @@ class TestMultipleChooserPanelGetComparison(TestCase):
2078
2095
  parent_page.add_child(instance=self.page)
2079
2096
 
2080
2097
  def test_get_comparison(self):
2081
- # Test whether the InlinePanel passes it's label in get_comparison
2098
+ # Test whether the MultipleChooserPanel passes its heading in get_comparison
2082
2099
 
2083
2100
  comparison = (
2084
2101
  self.page.get_edit_handler()
@@ -65,12 +65,20 @@ class TestChooserBrowse(WagtailTestUtils, TestCase):
65
65
 
66
66
  checkbox_value = str(self.child_page.id)
67
67
  decoded_content = response.content.decode()
68
+ response_json = json.loads(decoded_content)
69
+ self.assertEqual(response_json["step"], "browse")
70
+ response_html = response_json["html"]
68
71
 
69
- self.assertIn(f'value=\\"{checkbox_value}\\"', decoded_content)
72
+ self.assertIn(f'value="{checkbox_value}"', response_html)
70
73
 
71
74
  self.assertEqual(response.status_code, 200)
72
75
  self.assertTemplateUsed(response, "wagtailadmin/chooser/browse.html")
73
76
 
77
+ soup = self.get_soup(response_html)
78
+ search_url = soup.find("form", role="search")["action"]
79
+ search_query_params = parse_qs(urlsplit(search_url).query)
80
+ self.assertEqual(search_query_params["multiple"], ["1"])
81
+
74
82
  @override_settings(USE_THOUSAND_SEPARATOR=False)
75
83
  def test_multiple_chooser_view_without_thousand_separator(self):
76
84
  self.page = Page.objects.get(id=1)
@@ -241,6 +249,35 @@ class TestChooserBrowseChild(WagtailTestUtils, TestCase):
241
249
  self.assertIn(event_page.id, pages)
242
250
  self.assertTrue(pages[self.child_page.id].can_choose)
243
251
 
252
+ def test_with_multiple_specific_page_types_display_warning(self):
253
+ # Add a page that is not a SimplePage
254
+ event_page = EventPage(
255
+ title="event",
256
+ location="the moon",
257
+ audience="public",
258
+ cost="free",
259
+ date_from="2001-01-01",
260
+ )
261
+ self.root_page.add_child(instance=event_page)
262
+
263
+ # Send request
264
+ response = self.get({"page_type": "tests.simplepage,tests.eventpage"})
265
+
266
+ self.assertEqual(response.status_code, 200)
267
+ self.assertEqual(
268
+ response.context["page_type_names"], ["Simple page", "Event page"]
269
+ )
270
+
271
+ html = response.json().get("html")
272
+ expected = """
273
+ <p class="help-block help-warning">
274
+ <svg class="icon icon-warning icon" aria-hidden="true"><use href="#icon-warning"></use></svg>
275
+ Only the following page types may be chosen for this field: Simple page, Event page. Search results will exclude pages of other types.
276
+ </p>
277
+ """
278
+
279
+ self.assertTagInHTML(expected, html)
280
+
244
281
  def test_with_unknown_page_type(self):
245
282
  response = self.get({"page_type": "foo.bar"})
246
283
  self.assertEqual(response.status_code, 404)
@@ -363,12 +400,22 @@ class TestChooserSearch(WagtailTestUtils, TransactionTestCase):
363
400
  return self.client.get(reverse("wagtailadmin_choose_page_search"), params or {})
364
401
 
365
402
  def test_simple(self):
366
- response = self.get({"q": "foobarbaz"})
403
+ response = self.get({"q": "foobarbaz", "allow_external_link": "true"})
367
404
  self.assertEqual(response.status_code, 200)
368
405
  self.assertTemplateUsed(response, "wagtailadmin/chooser/_search_results.html")
369
406
  self.assertContains(response, "There is 1 match")
370
407
  self.assertContains(response, "foobarbaz")
371
408
 
409
+ # parent page link should preserve the allow_external_link parameter
410
+ expected_url = (
411
+ reverse("wagtailadmin_choose_page_child", args=[self.root_page.id])
412
+ + "?allow_external_link=true"
413
+ )
414
+ self.assertContains(
415
+ response,
416
+ f'<a href="{expected_url}" class="navigate-parent">{self.root_page.title}</a>',
417
+ )
418
+
372
419
  def test_partial_match(self):
373
420
  response = self.get({"q": "fooba"})
374
421
  self.assertEqual(response.status_code, 200)
@@ -912,7 +959,7 @@ class TestChooserExternalLinkWithNonRootServePath(TestChooserExternalLink):
912
959
  "external-link-chooser-link_text": "about",
913
960
  }
914
961
  )
915
- with self.assertNumQueries(11):
962
+ with self.assertNumQueries(10):
916
963
  response = self.post(
917
964
  {
918
965
  "external-link-chooser-url": f"http://localhost/{self.prefix}about/",
@@ -398,11 +398,16 @@ class TestPrivacyIndicators(WagtailTestUtils, TestCase):
398
398
  # Check the response
399
399
  self.assertEqual(response.status_code, 200)
400
400
 
401
+ soup = self.get_soup(response.content)
402
+
403
+ public_link = soup.select_one('[data-w-zone-switch-key-value="isPublic"]')
404
+ private_link = soup.select_one('[data-w-zone-switch-key-value="!isPublic"]')
405
+
401
406
  # Check the privacy indicator is public
402
- self.assertContains(
403
- response, '<div class="w-hidden" data-privacy-sidebar-private>'
404
- )
405
- self.assertContains(response, '<div class="" data-privacy-sidebar-public>')
407
+ self.assertEqual(private_link["class"], ["page-status-tag", "w-hidden"])
408
+
409
+ # Check the privacy indicator is private
410
+ self.assertEqual(public_link["class"], ["page-status-tag"])
406
411
 
407
412
  def test_explorer_private(self):
408
413
  """
@@ -417,14 +422,14 @@ class TestPrivacyIndicators(WagtailTestUtils, TestCase):
417
422
 
418
423
  soup = self.get_soup(response.content)
419
424
 
425
+ public_link = soup.select_one('[data-w-zone-switch-key-value="isPublic"]')
426
+ private_link = soup.select_one('[data-w-zone-switch-key-value="!isPublic"]')
427
+
420
428
  # Check the private privacy indicator is visible
421
- private_indicator = soup.select_one("[data-privacy-sidebar-private]")
422
- # There should not be any classes applied
423
- self.assertEqual(private_indicator["class"], [])
429
+ self.assertEqual(private_link["class"], ["page-status-tag"])
424
430
 
425
- # Privacy indicator should be hidden
426
- public_indicator = soup.select_one("[data-privacy-sidebar-public].w-hidden")
427
- self.assertIsNotNone(public_indicator)
431
+ # Check the public privacy indicator is hidden
432
+ self.assertEqual(public_link["class"], ["page-status-tag", "w-hidden"])
428
433
 
429
434
  def test_explorer_private_child(self):
430
435
  """
@@ -437,11 +442,16 @@ class TestPrivacyIndicators(WagtailTestUtils, TestCase):
437
442
  # Check the response
438
443
  self.assertEqual(response.status_code, 200)
439
444
 
445
+ soup = self.get_soup(response.content)
446
+
447
+ public_link = soup.select_one('[data-w-zone-switch-key-value="isPublic"]')
448
+ private_link = soup.select_one('[data-w-zone-switch-key-value="!isPublic"]')
449
+
440
450
  # Check the privacy indicator is private
441
- self.assertContains(response, '<div class="" data-privacy-sidebar-private>')
442
- self.assertContains(
443
- response, '<div class="w-hidden" data-privacy-sidebar-public>'
444
- )
451
+ self.assertEqual(private_link["class"], ["page-status-tag"])
452
+
453
+ # Check the public privacy indicator is hidden
454
+ self.assertEqual(public_link["class"], ["page-status-tag", "w-hidden"])
445
455
 
446
456
  def test_explorer_list_homepage(self):
447
457
  """
@@ -491,11 +501,15 @@ class TestPrivacyIndicators(WagtailTestUtils, TestCase):
491
501
  # Check the response
492
502
  self.assertEqual(response.status_code, 200)
493
503
 
504
+ soup = self.get_soup(response.content)
505
+
506
+ public_link = soup.select_one('[data-w-zone-switch-key-value="isPublic"]')
507
+ private_link = soup.select_one('[data-w-zone-switch-key-value="!isPublic"]')
508
+
494
509
  # Check the privacy indicator is public
495
- self.assertContains(
496
- response, '<div class="w-hidden" data-privacy-sidebar-private>'
497
- )
498
- self.assertContains(response, '<div class="" data-privacy-sidebar-public>')
510
+ self.assertEqual(public_link["class"], ["page-status-tag"])
511
+
512
+ self.assertEqual(private_link["class"], ["page-status-tag", "w-hidden"])
499
513
 
500
514
  def test_edit_private(self):
501
515
  """
@@ -508,11 +522,15 @@ class TestPrivacyIndicators(WagtailTestUtils, TestCase):
508
522
  # Check the response
509
523
  self.assertEqual(response.status_code, 200)
510
524
 
525
+ soup = self.get_soup(response.content)
526
+
527
+ public_link = soup.select_one('[data-w-zone-switch-key-value="isPublic"]')
528
+ private_link = soup.select_one('[data-w-zone-switch-key-value="!isPublic"]')
529
+
511
530
  # Check the privacy indicator is private
512
- self.assertContains(response, '<div class="" data-privacy-sidebar-private>')
513
- self.assertContains(
514
- response, '<div class="w-hidden" data-privacy-sidebar-public>'
515
- )
531
+ self.assertEqual(private_link["class"], ["page-status-tag"])
532
+
533
+ self.assertEqual(public_link["class"], ["page-status-tag", "w-hidden"])
516
534
 
517
535
  def test_edit_private_child(self):
518
536
  """
@@ -526,10 +544,15 @@ class TestPrivacyIndicators(WagtailTestUtils, TestCase):
526
544
  self.assertEqual(response.status_code, 200)
527
545
 
528
546
  # Check the privacy indicator is private
529
- self.assertContains(response, '<div class="" data-privacy-sidebar-private>')
530
- self.assertContains(
531
- response, '<div class="w-hidden" data-privacy-sidebar-public>'
532
- )
547
+ soup = self.get_soup(response.content)
548
+
549
+ public_link = soup.select_one('[data-w-zone-switch-key-value="isPublic"]')
550
+ private_link = soup.select_one('[data-w-zone-switch-key-value="!isPublic"]')
551
+
552
+ # Check the privacy indicator is private
553
+ self.assertEqual(private_link["class"], ["page-status-tag"])
554
+
555
+ self.assertEqual(public_link["class"], ["page-status-tag", "w-hidden"])
533
556
 
534
557
  def test_private_page_options_only_password_groups(self):
535
558
  # change the private_page_options to password and login
@@ -18,9 +18,6 @@ class TestPagesSummary(WagtailTestUtils, TestCase):
18
18
  cls.wagtail_root.add_child(instance=cls.test_page)
19
19
 
20
20
  cls.test_page_group = Group.objects.create(name="Test page")
21
- GroupPagePermission.objects.create(
22
- group=cls.test_page_group, page=cls.test_page, permission_type="change"
23
- )
24
21
 
25
22
  @classmethod
26
23
  def tearDownClass(cls):
@@ -68,14 +65,22 @@ class TestPagesSummary(WagtailTestUtils, TestCase):
68
65
  self.user.save()
69
66
  self.assertFalse(PagesSummaryItem(self.request).is_shown())
70
67
 
71
- def test_user_with_limited_page_permissions_summary_links_to_their_root(self):
68
+ def test_user_with_limited_page_permissions(self):
69
+ permissions = {"add", "change", "publish", "bulk_delete", "lock", "unlock"}
72
70
  self.user.is_superuser = False
73
71
  self.user.save()
74
72
  self.user.groups.add(self.test_page_group)
75
- self.assertSummaryContainsLinkToPage(self.test_page.pk)
76
73
 
77
- def test_user_with_limited_page_permissions_sees_proper_page_count(self):
78
- self.user.is_superuser = False
79
- self.user.save()
80
- self.user.groups.add(self.test_page_group)
81
- self.assertSummaryContains("1 Page")
74
+ for permission in permissions:
75
+ with self.subTest(permission=permission):
76
+ permission_obj = GroupPagePermission.objects.create(
77
+ group=self.test_page_group,
78
+ page=self.test_page,
79
+ permission_type=permission,
80
+ )
81
+ # Should link to the permission's page rather than Wagtail's root
82
+ self.assertSummaryContainsLinkToPage(self.test_page.pk)
83
+ # Should show the correct page count
84
+ self.assertSummaryContains("1 Page")
85
+ permission_obj.delete()
86
+ self.test_page_group.page_permissions.all().delete()
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import os
2
3
  import unittest
3
4
  from datetime import datetime, timedelta
4
5
  from datetime import timezone as dt_timezone
@@ -31,6 +32,24 @@ from wagtail.users.models import UserProfile
31
32
  from wagtail.utils.deprecation import RemovedInWagtail70Warning
32
33
 
33
34
 
35
+ class TestAvatarUrlInterceptTemplateTag(WagtailTestUtils, TestCase):
36
+ def setUp(self):
37
+ self.test_user = self.create_user(
38
+ username="testuser",
39
+ email="testuser@email.com",
40
+ password="password",
41
+ )
42
+
43
+ def test_get_avatar_url_undefined(self):
44
+ url = avatar_url(self.test_user)
45
+ self.assertIn("www.gravatar.com", url)
46
+
47
+ @mock.patch.dict(os.environ, {"AVATAR_INTERCEPT": "True"}, clear=True)
48
+ def test_get_avatar_url_registered(self):
49
+ url = avatar_url(self.test_user)
50
+ self.assertEqual(url, "/some/avatar/fred.png")
51
+
52
+
34
53
  class TestAvatarTemplateTag(WagtailTestUtils, TestCase):
35
54
  def setUp(self):
36
55
  # Create a user