wagtail 6.3.1__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 (307) 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/locale/gl/LC_MESSAGES/djangojs.mo +0 -0
  18. wagtail/admin/locale/gl/LC_MESSAGES/djangojs.po +5 -5
  19. wagtail/admin/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  20. wagtail/admin/locale/pt_BR/LC_MESSAGES/django.po +29 -0
  21. wagtail/admin/menu.py +0 -13
  22. wagtail/admin/panels/base.py +2 -2
  23. wagtail/admin/panels/group.py +4 -1
  24. wagtail/admin/panels/inline_panel.py +5 -2
  25. wagtail/admin/panels/model_utils.py +36 -0
  26. wagtail/admin/panels/page_utils.py +2 -40
  27. wagtail/admin/panels/signal_handlers.py +0 -2
  28. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  29. wagtail/admin/static/wagtailadmin/css/panels/draftail.css +1 -1
  30. wagtail/admin/static/wagtailadmin/css/panels/streamfield.css +1 -1
  31. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  32. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  33. wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +1 -8
  34. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  35. wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
  36. wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
  37. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  38. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  39. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  40. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  41. wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +1 -1
  42. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  43. wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +7 -0
  44. wagtail/admin/templates/wagtailadmin/404.html +4 -0
  45. wagtail/admin/templates/wagtailadmin/chooser/browse.html +2 -1
  46. wagtail/admin/templates/wagtailadmin/chooser/tables/parent_page_cell.html +1 -1
  47. wagtail/admin/templates/wagtailadmin/collections/_privacy_switch.html +8 -1
  48. wagtail/admin/templates/wagtailadmin/generic/confirm_delete.html +15 -9
  49. wagtail/admin/templates/wagtailadmin/generic/confirm_unpublish.html +21 -25
  50. wagtail/admin/templates/wagtailadmin/generic/form.html +1 -1
  51. wagtail/admin/templates/wagtailadmin/generic/preview_error.html +3 -0
  52. wagtail/admin/templates/wagtailadmin/generic/revisions/compare.html +63 -76
  53. wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -2
  54. wagtail/admin/templates/wagtailadmin/pages/edit.html +1 -5
  55. wagtail/admin/templates/wagtailadmin/panels/inline_panel_child.html +1 -0
  56. wagtail/admin/templates/wagtailadmin/permissions/includes/collection_member_permissions_form.html +1 -1
  57. wagtail/admin/templates/wagtailadmin/permissions/includes/collection_member_permissions_formset.html +6 -22
  58. wagtail/admin/templates/wagtailadmin/shared/formatted_field.html +2 -2
  59. wagtail/admin/templates/wagtailadmin/shared/header.html +2 -2
  60. wagtail/admin/templates/wagtailadmin/shared/page_status_tag_new.html +32 -39
  61. wagtail/admin/templates/wagtailadmin/shared/revisions/confirm_unschedule.html +13 -17
  62. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/privacy.html +15 -3
  63. wagtail/admin/templates/wagtailadmin/shared/side_panels/preview.html +1 -1
  64. wagtail/admin/templates/wagtailadmin/skeleton.html +4 -2
  65. wagtail/admin/templates/wagtailadmin/workflows/create.html +1 -1
  66. wagtail/admin/templates/wagtailadmin/workflows/edit.html +1 -1
  67. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_pages_form.html +1 -1
  68. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_pages_formset.html +6 -23
  69. wagtail/admin/templatetags/wagtailadmin_tags.py +12 -0
  70. wagtail/admin/templatetags/wagtailuserbar.py +2 -3
  71. wagtail/admin/tests/pages/test_create_page.py +110 -1
  72. wagtail/admin/tests/pages/test_edit_page.py +3 -2
  73. wagtail/admin/tests/pages/test_explorer_view.py +18 -0
  74. wagtail/admin/tests/pages/test_page_usage.py +24 -20
  75. wagtail/admin/tests/pages/test_preview.py +69 -1
  76. wagtail/admin/tests/pages/test_revisions.py +40 -6
  77. wagtail/admin/tests/test_account_management.py +39 -1
  78. wagtail/admin/tests/test_audit_log.py +4 -2
  79. wagtail/admin/tests/test_block_preview.py +224 -0
  80. wagtail/admin/tests/test_edit_handlers.py +23 -6
  81. wagtail/admin/tests/test_page_chooser.py +50 -3
  82. wagtail/admin/tests/test_privacy.py +49 -26
  83. wagtail/admin/tests/test_site_summary.py +15 -10
  84. wagtail/admin/tests/test_templatetags.py +19 -0
  85. wagtail/admin/tests/test_userbar.py +82 -1
  86. wagtail/admin/tests/test_views_generic.py +27 -12
  87. wagtail/admin/tests/test_workflows.py +69 -0
  88. wagtail/admin/tests/tests.py +23 -4
  89. wagtail/admin/tests/ui/test_sidebar.py +1 -1
  90. wagtail/admin/tests/viewsets/test_model_viewset.py +15 -13
  91. wagtail/admin/ui/side_panels.py +7 -4
  92. wagtail/admin/urls/__init__.py +6 -0
  93. wagtail/admin/urls/pages.py +1 -1
  94. wagtail/admin/userbar.py +21 -1
  95. wagtail/admin/views/account.py +5 -0
  96. wagtail/admin/views/chooser.py +5 -1
  97. wagtail/admin/views/collections.py +0 -2
  98. wagtail/admin/views/generic/base.py +20 -10
  99. wagtail/admin/views/generic/history.py +0 -1
  100. wagtail/admin/views/generic/models.py +79 -21
  101. wagtail/admin/views/generic/preview.py +50 -1
  102. wagtail/admin/views/mixins.py +4 -2
  103. wagtail/admin/views/pages/bulk_actions/delete.py +11 -23
  104. wagtail/admin/views/pages/bulk_actions/page_bulk_action.py +17 -0
  105. wagtail/admin/views/pages/bulk_actions/publish.py +11 -31
  106. wagtail/admin/views/pages/bulk_actions/unpublish.py +11 -31
  107. wagtail/admin/views/pages/create.py +1 -0
  108. wagtail/admin/views/pages/edit.py +38 -30
  109. wagtail/admin/views/pages/revisions.py +43 -114
  110. wagtail/admin/views/pages/utils.py +0 -1
  111. wagtail/admin/views/tags.py +6 -2
  112. wagtail/admin/views/workflows.py +8 -6
  113. wagtail/admin/viewsets/model.py +0 -4
  114. wagtail/admin/viewsets/pages.py +0 -1
  115. wagtail/admin/widgets/tags.py +1 -0
  116. wagtail/api/v2/tests/test_documents.py +4 -2
  117. wagtail/api/v2/tests/test_images.py +4 -2
  118. wagtail/api/v2/tests/test_pages.py +8 -4
  119. wagtail/blocks/base.py +59 -1
  120. wagtail/blocks/field_block.py +6 -0
  121. wagtail/blocks/list_block.py +4 -0
  122. wagtail/blocks/static_block.py +3 -0
  123. wagtail/blocks/stream_block.py +5 -1
  124. wagtail/blocks/struct_block.py +6 -0
  125. wagtail/compat.py +16 -0
  126. wagtail/contrib/forms/forms.py +27 -7
  127. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +2 -2
  128. wagtail/contrib/forms/locale/gl/LC_MESSAGES/django.mo +0 -0
  129. wagtail/contrib/forms/locale/gl/LC_MESSAGES/django.po +4 -4
  130. wagtail/contrib/forms/tests/test_models.py +7 -5
  131. wagtail/contrib/forms/tests/test_views.py +75 -0
  132. wagtail/contrib/frontend_cache/backends/cloudfront.py +1 -1
  133. wagtail/contrib/frontend_cache/tasks.py +83 -0
  134. wagtail/contrib/frontend_cache/tests.py +48 -33
  135. wagtail/contrib/frontend_cache/utils.py +2 -70
  136. wagtail/contrib/redirects/base_formats.py +2 -2
  137. wagtail/contrib/redirects/locale/ar/LC_MESSAGES/django.mo +0 -0
  138. wagtail/contrib/redirects/locale/ar/LC_MESSAGES/django.po +3 -0
  139. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +24 -37
  140. wagtail/contrib/redirects/templates/wagtailredirects/add.html +1 -24
  141. wagtail/contrib/redirects/templates/wagtailredirects/confirm_delete.html +3 -13
  142. wagtail/contrib/redirects/tests/test_redirects.py +122 -110
  143. wagtail/contrib/redirects/tests/test_signal_handlers.py +75 -69
  144. wagtail/contrib/redirects/urls.py +2 -2
  145. wagtail/contrib/redirects/views.py +35 -73
  146. wagtail/contrib/search_promotions/admin_urls.py +10 -3
  147. wagtail/contrib/search_promotions/forms.py +55 -26
  148. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +44 -54
  149. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/add.html +21 -31
  150. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/confirm_delete.html +3 -12
  151. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/edit.html +11 -34
  152. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/includes/searchpromotion_form.html +1 -0
  153. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js +2 -1
  154. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index.html +0 -1
  155. wagtail/contrib/search_promotions/tests.py +814 -13
  156. wagtail/contrib/search_promotions/views/__init__.py +1 -0
  157. wagtail/contrib/search_promotions/views/reports.py +56 -0
  158. wagtail/contrib/search_promotions/views/settings.py +258 -0
  159. wagtail/contrib/search_promotions/wagtail_hooks.py +12 -1
  160. wagtail/contrib/settings/locale/ar/LC_MESSAGES/django.mo +0 -0
  161. wagtail/contrib/settings/locale/ar/LC_MESSAGES/django.po +6 -1
  162. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +3 -3
  163. wagtail/contrib/settings/templates/wagtailsettings/edit.html +1 -5
  164. wagtail/contrib/settings/tests/generic/test_admin.py +2 -5
  165. wagtail/contrib/settings/tests/generic/test_register.py +1 -1
  166. wagtail/contrib/settings/tests/site_specific/test_admin.py +2 -5
  167. wagtail/contrib/settings/tests/site_specific/test_register.py +1 -1
  168. wagtail/contrib/settings/views.py +9 -23
  169. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  170. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +1 -1
  171. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  172. wagtail/contrib/table_block/tests.py +4 -1
  173. wagtail/contrib/typed_table_block/blocks.py +3 -0
  174. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
  175. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  176. wagtail/contrib/typed_table_block/tests.py +33 -0
  177. wagtail/documents/locale/en/LC_MESSAGES/django.po +26 -26
  178. wagtail/documents/migrations/0011_add_choose_permissions.py +1 -0
  179. wagtail/documents/models.py +1 -0
  180. wagtail/documents/signal_handlers.py +6 -2
  181. wagtail/documents/static/wagtaildocs/js/add-multiple.js +1 -1
  182. wagtail/documents/templates/wagtaildocs/documents/edit.html +1 -3
  183. wagtail/documents/templates/wagtaildocs/multiple/add.html +7 -1
  184. wagtail/documents/tests/test_admin_views.py +74 -33
  185. wagtail/documents/tests/test_views.py +21 -12
  186. wagtail/documents/views/chooser.py +1 -0
  187. wagtail/documents/views/documents.py +1 -2
  188. wagtail/documents/views/multiple.py +0 -1
  189. wagtail/documents/views/serve.py +9 -2
  190. wagtail/documents/wagtail_hooks.py +6 -1
  191. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  192. wagtail/embeds/oembed_providers.py +0 -64
  193. wagtail/fields.py +3 -0
  194. wagtail/images/apps.py +2 -1
  195. wagtail/images/blocks.py +14 -2
  196. wagtail/images/forms.py +40 -3
  197. wagtail/images/locale/ar/LC_MESSAGES/django.mo +0 -0
  198. wagtail/images/locale/ar/LC_MESSAGES/django.po +4 -0
  199. wagtail/images/locale/en/LC_MESSAGES/django.po +49 -49
  200. wagtail/images/migrations/0023_add_choose_permissions.py +1 -0
  201. wagtail/images/rich_text/contentstate.py +1 -0
  202. wagtail/images/rich_text/editor_html.py +1 -0
  203. wagtail/images/signal_handlers.py +17 -10
  204. wagtail/images/static/wagtailimages/js/add-multiple.js +1 -1
  205. wagtail/images/static/wagtailimages/js/image-block.js +1 -1
  206. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  207. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  208. wagtail/images/static/wagtailimages/js/image-url-generator.js +1 -1
  209. wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-image.js +1 -1
  210. wagtail/images/tasks.py +18 -0
  211. wagtail/images/templates/wagtailimages/images/edit.html +1 -3
  212. wagtail/images/templates/wagtailimages/images/url_generator.html +1 -1
  213. wagtail/images/templates/wagtailimages/multiple/add.html +7 -2
  214. wagtail/images/templates/wagtailimages/widgets/image_chooser.html +1 -1
  215. wagtail/images/tests/test_admin_views.py +53 -29
  216. wagtail/images/tests/test_blocks.py +34 -2
  217. wagtail/images/tests/test_models.py +12 -10
  218. wagtail/images/tests/tests.py +10 -0
  219. wagtail/images/views/chooser.py +1 -0
  220. wagtail/images/views/images.py +1 -3
  221. wagtail/images/views/multiple.py +0 -1
  222. wagtail/images/views/serve.py +18 -2
  223. wagtail/images/widgets.py +3 -0
  224. wagtail/locale/en/LC_MESSAGES/django.po +228 -216
  225. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  226. wagtail/management/commands/publish_scheduled.py +1 -1
  227. wagtail/migrations/0087_alter_grouppagepermission_unique_together_and_more.py +16 -8
  228. wagtail/models/__init__.py +300 -119
  229. wagtail/models/i18n.py +2 -2
  230. wagtail/models/panels.py +37 -0
  231. wagtail/models/sites.py +7 -6
  232. wagtail/permission_policies/pages.py +2 -2
  233. wagtail/project_template/project_name/settings/base.py +4 -0
  234. wagtail/project_template/requirements.txt +1 -1
  235. wagtail/query.py +145 -0
  236. wagtail/search/backends/database/mysql/mysql.py +25 -17
  237. wagtail/search/backends/database/postgres/postgres.py +44 -83
  238. wagtail/search/backends/database/sqlite/sqlite.py +25 -17
  239. wagtail/search/backends/elasticsearch7.py +4 -0
  240. wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
  241. wagtail/search/query.py +8 -2
  242. wagtail/search/signal_handlers.py +6 -9
  243. wagtail/search/tasks.py +10 -0
  244. wagtail/search/tests/test_elasticsearch7_backend.py +21 -0
  245. wagtail/search/tests/test_index_functions.py +10 -6
  246. wagtail/search/tests/test_postgres_backend.py +0 -14
  247. wagtail/signal_handlers.py +5 -20
  248. wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
  249. wagtail/snippets/locale/en/LC_MESSAGES/django.po +3 -13
  250. wagtail/snippets/locale/sv/LC_MESSAGES/django.mo +0 -0
  251. wagtail/snippets/locale/sv/LC_MESSAGES/django.po +4 -3
  252. wagtail/snippets/tests/test_preview.py +5 -0
  253. wagtail/snippets/tests/test_snippets.py +100 -45
  254. wagtail/snippets/tests/test_usage.py +29 -24
  255. wagtail/snippets/tests/test_viewset.py +1 -1
  256. wagtail/snippets/views/snippets.py +0 -12
  257. wagtail/tasks.py +41 -0
  258. wagtail/templates/wagtailcore/shared/block_preview.html +29 -0
  259. wagtail/test/earlypage/__init__.py +0 -0
  260. wagtail/test/earlypage/migrations/0001_initial.py +37 -0
  261. wagtail/test/earlypage/migrations/__init__.py +0 -0
  262. wagtail/test/earlypage/models.py +14 -0
  263. wagtail/test/settings.py +3 -0
  264. wagtail/test/testapp/fixtures/test.json +7 -0
  265. wagtail/test/testapp/fixtures/test_specific.json +6 -3
  266. wagtail/test/testapp/models.py +58 -44
  267. wagtail/test/testapp/templates/tests/custom_block_preview.html +16 -0
  268. wagtail/test/testapp/templates/tests/static_block_preview.html +5 -0
  269. wagtail/test/testapp/wagtail_hooks.py +9 -0
  270. wagtail/tests/test_blocks.py +189 -2
  271. wagtail/tests/test_hooks.py +166 -1
  272. wagtail/tests/test_management_commands.py +54 -13
  273. wagtail/tests/test_page_allowed_http_methods.py +32 -0
  274. wagtail/tests/test_page_model.py +68 -0
  275. wagtail/tests/test_page_privacy.py +10 -0
  276. wagtail/tests/test_page_queryset.py +79 -0
  277. wagtail/tests/test_reference_index.py +84 -75
  278. wagtail/tests/test_streamfield.py +30 -0
  279. wagtail/tests/test_utils.py +61 -0
  280. wagtail/users/forms.py +2 -9
  281. wagtail/users/locale/en/LC_MESSAGES/django.po +17 -17
  282. wagtail/users/locale/nl/LC_MESSAGES/django.mo +0 -0
  283. wagtail/users/locale/nl/LC_MESSAGES/django.po +4 -3
  284. wagtail/users/templates/wagtailusers/groups/create.html +0 -5
  285. wagtail/users/templates/wagtailusers/groups/includes/page_permissions_form.html +1 -1
  286. wagtail/users/templates/wagtailusers/groups/includes/page_permissions_formset.html +6 -6
  287. wagtail/users/tests/test_admin_views.py +96 -4
  288. wagtail/users/tests/test_utils.py +76 -0
  289. wagtail/users/utils.py +43 -11
  290. wagtail/utils/setup.py +2 -2
  291. wagtail/utils/templates.py +26 -0
  292. wagtail/utils/widgets.py +1 -0
  293. wagtail/views.py +9 -1
  294. wagtail/wagtail_hooks.py +67 -29
  295. {wagtail-6.3.1.dist-info → wagtail-6.4rc1.dist-info}/METADATA +2 -2
  296. {wagtail-6.3.1.dist-info → wagtail-6.4rc1.dist-info}/RECORD +300 -287
  297. wagtail/admin/static/wagtailadmin/js/expanding-formset.js +0 -1
  298. wagtail/admin/static/wagtailadmin/js/vendor/rangy-core.js +0 -1
  299. wagtail/admin/static/wagtailadmin/js/vendor/uuidv4.min.js +0 -1
  300. wagtail/contrib/search_promotions/views.py +0 -323
  301. wagtail/images/static/wagtailimages/js/vendor/canvas-to-blob.min.js +0 -1
  302. wagtail/users/static/wagtailusers/js/group-form.js +0 -1
  303. wagtail/users/templates/wagtailusers/groups/includes/group_form_js.html +0 -3
  304. {wagtail-6.3.1.dist-info → wagtail-6.4rc1.dist-info}/LICENSE +0 -0
  305. {wagtail-6.3.1.dist-info → wagtail-6.4rc1.dist-info}/WHEEL +0 -0
  306. {wagtail-6.3.1.dist-info → wagtail-6.4rc1.dist-info}/entry_points.txt +0 -0
  307. {wagtail-6.3.1.dist-info → wagtail-6.4rc1.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,16 @@
1
1
  import json
2
2
  from datetime import date, datetime, timedelta
3
- from io import StringIO
3
+ from io import BytesIO, StringIO
4
4
 
5
5
  from django.contrib.auth.models import Permission
6
6
  from django.core import management
7
7
  from django.test import TestCase
8
8
  from django.urls import reverse
9
+ from django.utils import timezone
10
+ from openpyxl import load_workbook
9
11
 
10
12
  from wagtail.admin.admin_url_finder import AdminURLFinder
13
+ from wagtail.admin.tests.test_reports_views import BaseReportViewTestCase
11
14
  from wagtail.contrib.search_promotions.models import (
12
15
  Query,
13
16
  QueryDailyHits,
@@ -16,7 +19,9 @@ from wagtail.contrib.search_promotions.models import (
16
19
  from wagtail.contrib.search_promotions.templatetags.wagtailsearchpromotions_tags import (
17
20
  get_search_promotions,
18
21
  )
22
+ from wagtail.log_actions import registry as log_registry
19
23
  from wagtail.test.utils import WagtailTestUtils
24
+ from wagtail.test.utils.template_tests import AdminTemplateTestUtils
20
25
 
21
26
 
22
27
  class TestSearchPromotions(TestCase):
@@ -171,7 +176,7 @@ class TestGetSearchPromotionsTemplateTag(TestCase):
171
176
  self.assertEqual(search_picks, [])
172
177
 
173
178
 
174
- class TestSearchPromotionsIndexView(WagtailTestUtils, TestCase):
179
+ class TestSearchPromotionsIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
175
180
  def setUp(self):
176
181
  self.user = self.login()
177
182
 
@@ -179,6 +184,10 @@ class TestSearchPromotionsIndexView(WagtailTestUtils, TestCase):
179
184
  response = self.client.get(reverse("wagtailsearchpromotions:index"))
180
185
  self.assertEqual(response.status_code, 200)
181
186
  self.assertTemplateUsed(response, "wagtailsearchpromotions/index.html")
187
+ self.assertBreadcrumbsItemsRendered(
188
+ [{"url": "", "label": "Promoted search results"}],
189
+ response.content,
190
+ )
182
191
 
183
192
  def test_search(self):
184
193
  response = self.client.get(
@@ -459,14 +468,24 @@ class TestSearchPromotionsIndexView(WagtailTestUtils, TestCase):
459
468
  self.assertIsNone(soup.select_one(f'a[href="{add_url}"]'))
460
469
 
461
470
 
462
- class TestSearchPromotionsAddView(WagtailTestUtils, TestCase):
471
+ class TestSearchPromotionsAddView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
463
472
  def setUp(self):
464
- self.login()
473
+ self.user = self.login()
465
474
 
466
475
  def test_simple(self):
467
476
  response = self.client.get(reverse("wagtailsearchpromotions:add"))
468
477
  self.assertEqual(response.status_code, 200)
469
478
  self.assertTemplateUsed(response, "wagtailsearchpromotions/add.html")
479
+ self.assertBreadcrumbsItemsRendered(
480
+ [
481
+ {
482
+ "url": reverse("wagtailsearchpromotions:index"),
483
+ "label": "Promoted search results",
484
+ },
485
+ {"url": "", "label": "New: Promoted search result"},
486
+ ],
487
+ response.content,
488
+ )
470
489
 
471
490
  def test_post(self):
472
491
  # Submit
@@ -488,6 +507,160 @@ class TestSearchPromotionsAddView(WagtailTestUtils, TestCase):
488
507
  # Check that the search pick was created
489
508
  self.assertTrue(Query.get("test").editors_picks.filter(page_id=1).exists())
490
509
 
510
+ # Ensure that only one log entry was created for the search pick
511
+ search_picks = list(Query.get("test").editors_picks.all())
512
+ self.assertEqual(len(search_picks), 1)
513
+ self.assertTrue(search_picks[0].page_id, 1)
514
+ logs = log_registry.get_logs_for_instance(search_picks[0])
515
+ self.assertEqual(len(logs), 1)
516
+ self.assertEqual(logs[0].action, "wagtail.create")
517
+
518
+ def test_with_multiple_picks(self):
519
+ # Submit
520
+ post_data = {
521
+ "query_string": "test",
522
+ "editors_picks-TOTAL_FORMS": 2,
523
+ "editors_picks-INITIAL_FORMS": 0,
524
+ "editors_picks-MAX_NUM_FORMS": 1000,
525
+ "editors_picks-0-DELETE": "",
526
+ "editors_picks-0-ORDER": 0,
527
+ "editors_picks-0-page": 1,
528
+ "editors_picks-0-description": "Hello",
529
+ "editors_picks-1-DELETE": "",
530
+ "editors_picks-1-ORDER": 1,
531
+ "editors_picks-1-page": "",
532
+ "editors_picks-1-external_link_url": "https://wagtail.org",
533
+ "editors_picks-1-external_link_text": "Wagtail",
534
+ "editors_picks-1-description": "The landing page",
535
+ }
536
+ response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
537
+
538
+ # User should be redirected back to the index
539
+ self.assertRedirects(response, reverse("wagtailsearchpromotions:index"))
540
+
541
+ # Check that the search pick was created
542
+ search_picks = list(
543
+ Query.get("test").editors_picks.all().order_by("description")
544
+ )
545
+ self.assertEqual(len(search_picks), 2)
546
+ self.assertEqual(search_picks[0].page_id, 1)
547
+ self.assertEqual(search_picks[0].description, "Hello")
548
+ self.assertEqual(search_picks[1].external_link_url, "https://wagtail.org")
549
+ self.assertEqual(search_picks[1].description, "The landing page")
550
+
551
+ # Ensure that only one log entry was created for each search pick
552
+ for search_pick in search_picks:
553
+ logs = log_registry.get_logs_for_instance(search_pick)
554
+ self.assertEqual(len(logs), 1)
555
+ self.assertEqual(logs[0].action, "wagtail.create")
556
+ self.assertEqual(logs[0].user, self.user)
557
+
558
+ def test_post_with_existing_query_string(self):
559
+ # Create an existing query with search picks
560
+ query = Query.get("test")
561
+ search_pick_1 = query.editors_picks.create(
562
+ page_id=1, sort_order=0, description="Root page"
563
+ )
564
+ search_pick_2 = query.editors_picks.create(
565
+ page_id=2, sort_order=1, description="Homepage"
566
+ )
567
+
568
+ # Submit
569
+ post_data = {
570
+ "query_string": "test",
571
+ "editors_picks-TOTAL_FORMS": 1,
572
+ "editors_picks-INITIAL_FORMS": 0,
573
+ "editors_picks-MAX_NUM_FORMS": 1000,
574
+ "editors_picks-0-DELETE": "",
575
+ "editors_picks-0-ORDER": 1,
576
+ "editors_picks-0-external_link_url": "https://wagtail.org",
577
+ "editors_picks-0-external_link_text": "Wagtail",
578
+ "editors_picks-0-description": "A Django-based CMS",
579
+ }
580
+ response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
581
+
582
+ # User should be redirected back to the index
583
+ self.assertRedirects(response, reverse("wagtailsearchpromotions:index"))
584
+
585
+ # Check that the submitted search pick is created
586
+ # and the existing ones are still there
587
+ self.assertEqual(
588
+ set(
589
+ Query.get("test")
590
+ .editors_picks.all()
591
+ .values_list("page_id", "external_link_url")
592
+ ),
593
+ {
594
+ (search_pick_1.page_id, ""),
595
+ (search_pick_2.page_id, ""),
596
+ (None, "https://wagtail.org"),
597
+ },
598
+ )
599
+
600
+ def test_post_with_invalid_query_string(self):
601
+ # Submit
602
+ post_data = {
603
+ "query_string": "",
604
+ "editors_picks-TOTAL_FORMS": 1,
605
+ "editors_picks-INITIAL_FORMS": 0,
606
+ "editors_picks-MAX_NUM_FORMS": 1000,
607
+ "editors_picks-0-DELETE": "",
608
+ "editors_picks-0-ORDER": 0,
609
+ "editors_picks-0-page": 1,
610
+ "editors_picks-0-description": "Hello",
611
+ }
612
+ response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
613
+
614
+ # User should be given an error on the specific field in the form
615
+ self.assertEqual(response.status_code, 200)
616
+
617
+ self.assertFormError(
618
+ response.context["form"], "query_string", "This field is required."
619
+ )
620
+ # The formset should still contain the submitted data
621
+ self.assertEqual(len(response.context["searchpicks_formset"].forms), 1)
622
+ self.assertEqual(
623
+ response.context["searchpicks_formset"].forms[0].cleaned_data["page"].id,
624
+ 1,
625
+ )
626
+ self.assertEqual(
627
+ response.context["searchpicks_formset"]
628
+ .forms[0]
629
+ .cleaned_data["description"],
630
+ "Hello",
631
+ )
632
+ # Should not raise an error anywhere else
633
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, "page", [])
634
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, None, [])
635
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
636
+
637
+ def test_post_with_invalid_page(self):
638
+ # Submit
639
+ post_data = {
640
+ "query_string": "test",
641
+ "editors_picks-TOTAL_FORMS": 1,
642
+ "editors_picks-INITIAL_FORMS": 0,
643
+ "editors_picks-MAX_NUM_FORMS": 1000,
644
+ "editors_picks-0-DELETE": "",
645
+ "editors_picks-0-ORDER": 0,
646
+ "editors_picks-0-page": 9999999999,
647
+ "editors_picks-0-description": "Hello",
648
+ }
649
+ response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
650
+
651
+ # User should be given an error on the specific field in the form
652
+ self.assertEqual(response.status_code, 200)
653
+ self.assertFormSetError(
654
+ response.context["searchpicks_formset"],
655
+ 0,
656
+ "page",
657
+ "Select a valid choice. "
658
+ "That choice is not one of the available choices.",
659
+ )
660
+ # Should not raise an error anywhere else
661
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, None, [])
662
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
663
+
491
664
  def test_post_with_external_link(self):
492
665
  # Submit
493
666
  post_data = {
@@ -547,14 +720,16 @@ class TestSearchPromotionsAddView(WagtailTestUtils, TestCase):
547
720
  }
548
721
  response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
549
722
 
550
- # User should be given an error
723
+ # User should be given an error on a specific form in the formset
551
724
  self.assertEqual(response.status_code, 200)
552
725
  self.assertFormSetError(
553
726
  response.context["searchpicks_formset"],
554
- None,
727
+ 0,
555
728
  None,
556
729
  "Please only select a page OR enter an external link.",
557
730
  )
731
+ # Should not raise an error on the top-level formset
732
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
558
733
 
559
734
  def test_post_missing_recommendation(self):
560
735
  post_data = {
@@ -568,14 +743,42 @@ class TestSearchPromotionsAddView(WagtailTestUtils, TestCase):
568
743
  }
569
744
  response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
570
745
 
571
- # User should be given an error
746
+ # User should be given an error on a specific form in the formset
572
747
  self.assertEqual(response.status_code, 200)
573
748
  self.assertFormSetError(
574
749
  response.context["searchpicks_formset"],
575
- None,
750
+ 0,
576
751
  None,
577
752
  "You must recommend a page OR an external link.",
578
753
  )
754
+ # Should not raise an error on the top-level formset
755
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
756
+
757
+ def test_post_invalid_external_link(self):
758
+ post_data = {
759
+ "query_string": "test",
760
+ "editors_picks-TOTAL_FORMS": 1,
761
+ "editors_picks-INITIAL_FORMS": 0,
762
+ "editors_picks-MAX_NUM_FORMS": 1000,
763
+ "editors_picks-0-DELETE": "",
764
+ "editors_picks-0-ORDER": 0,
765
+ "editors_picks-0-external_link_url": "notalink",
766
+ "editors_picks-0-external_link_text": "Wagtail",
767
+ "editors_picks-0-description": "Hello",
768
+ }
769
+ response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
770
+
771
+ # User should be given an error on the specific field in the form
772
+ self.assertEqual(response.status_code, 200)
773
+ self.assertFormSetError(
774
+ response.context["searchpicks_formset"],
775
+ 0,
776
+ "external_link_url",
777
+ "Enter a valid URL.",
778
+ )
779
+ # Should not raise an error anywhere else
780
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, None, [])
781
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
579
782
 
580
783
  def test_post_missing_external_text(self):
581
784
  post_data = {
@@ -589,17 +792,55 @@ class TestSearchPromotionsAddView(WagtailTestUtils, TestCase):
589
792
  }
590
793
  response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
591
794
 
592
- # User should be given an error
795
+ # User should be given an error on the specific field in the form
593
796
  self.assertEqual(response.status_code, 200)
594
797
  self.assertFormSetError(
595
798
  response.context["searchpicks_formset"],
596
- None,
597
- None,
799
+ 0,
800
+ "external_link_text",
598
801
  "You must enter an external link text if you enter an external link URL.",
599
802
  )
600
803
 
804
+ # Should not raise an error anywhere else
805
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, None, [])
806
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
601
807
 
602
- class TestSearchPromotionsEditView(WagtailTestUtils, TestCase):
808
+ def test_get_with_no_permission(self):
809
+ self.user.is_superuser = False
810
+ self.user.save()
811
+ # Only basic access_admin permission is given
812
+ self.user.user_permissions.add(
813
+ Permission.objects.get(
814
+ content_type__app_label="wagtailadmin",
815
+ codename="access_admin",
816
+ )
817
+ )
818
+
819
+ response = self.client.get(reverse("wagtailsearchpromotions:add"))
820
+ self.assertEqual(response.status_code, 302)
821
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
822
+
823
+ def test_get_with_add_permission_only(self):
824
+ self.user.is_superuser = False
825
+ self.user.save()
826
+ # Only basic access_admin permission is given
827
+ self.user.user_permissions.add(
828
+ Permission.objects.get(
829
+ content_type__app_label="wagtailadmin",
830
+ codename="access_admin",
831
+ ),
832
+ Permission.objects.get(
833
+ content_type__app_label="wagtailsearchpromotions",
834
+ codename="add_searchpromotion",
835
+ ),
836
+ )
837
+
838
+ response = self.client.get(reverse("wagtailsearchpromotions:add"))
839
+ self.assertEqual(response.status_code, 200)
840
+ self.assertTemplateUsed(response, "wagtailsearchpromotions/add.html")
841
+
842
+
843
+ class TestSearchPromotionsEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
603
844
  def setUp(self):
604
845
  self.user = self.login()
605
846
 
@@ -623,6 +864,17 @@ class TestSearchPromotionsEditView(WagtailTestUtils, TestCase):
623
864
  expected_url = "/admin/searchpicks/%d/" % self.query.id
624
865
  self.assertEqual(url_finder.get_edit_url(self.search_pick), expected_url)
625
866
 
867
+ self.assertBreadcrumbsItemsRendered(
868
+ [
869
+ {
870
+ "url": reverse("wagtailsearchpromotions:index"),
871
+ "label": "Promoted search results",
872
+ },
873
+ {"url": "", "label": "hello"},
874
+ ],
875
+ response.content,
876
+ )
877
+
626
878
  def test_post(self):
627
879
  # Submit
628
880
  post_data = {
@@ -654,6 +906,136 @@ class TestSearchPromotionsEditView(WagtailTestUtils, TestCase):
654
906
  "Description has changed",
655
907
  )
656
908
 
909
+ search_picks = list(
910
+ Query.get("Hello").editors_picks.all().order_by("description")
911
+ )
912
+ self.assertEqual(len(search_picks), 2)
913
+ self.assertEqual(search_picks[0].page_id, 1)
914
+ self.assertEqual(search_picks[0].description, "Description has changed")
915
+ self.assertEqual(search_picks[1].page_id, 2)
916
+ self.assertEqual(search_picks[1].description, "Homepage")
917
+
918
+ # Ensure that only one log entry was created for each search pick
919
+ for search_pick in search_picks:
920
+ logs = log_registry.get_logs_for_instance(search_pick)
921
+ self.assertEqual(len(logs), 1)
922
+ self.assertEqual(logs[0].action, "wagtail.edit")
923
+ self.assertEqual(logs[0].user, self.user)
924
+
925
+ def test_post_with_invalid_query_string(self):
926
+ # Submit
927
+ post_data = {
928
+ "query_string": "",
929
+ "editors_picks-TOTAL_FORMS": 2,
930
+ "editors_picks-INITIAL_FORMS": 2,
931
+ "editors_picks-MAX_NUM_FORMS": 1000,
932
+ "editors_picks-0-id": self.search_pick.id,
933
+ "editors_picks-0-DELETE": "",
934
+ "editors_picks-0-ORDER": 0,
935
+ "editors_picks-0-page": 1,
936
+ "editors_picks-0-description": "Description has changed", # Change
937
+ "editors_picks-1-id": self.search_pick_2.id,
938
+ "editors_picks-1-DELETE": "",
939
+ "editors_picks-1-ORDER": 1,
940
+ "editors_picks-1-page": 2,
941
+ "editors_picks-1-description": "Homepage",
942
+ }
943
+ response = self.client.post(reverse("wagtailsearchpromotions:add"), post_data)
944
+
945
+ # User should be given an error on the specific field in the form
946
+ self.assertEqual(response.status_code, 200)
947
+ self.assertFormError(
948
+ response.context["form"], "query_string", "This field is required."
949
+ )
950
+ # The formset should still contain the submitted data
951
+ self.assertEqual(len(response.context["searchpicks_formset"].forms), 2)
952
+ self.assertEqual(
953
+ response.context["searchpicks_formset"].forms[0].cleaned_data["page"].id,
954
+ 1,
955
+ )
956
+ self.assertEqual(
957
+ response.context["searchpicks_formset"]
958
+ .forms[0]
959
+ .cleaned_data["description"],
960
+ "Description has changed",
961
+ )
962
+ # Should not raise an error anywhere else
963
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, "page", [])
964
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, None, [])
965
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
966
+
967
+ def test_post_with_invalid_page(self):
968
+ # Submit
969
+ post_data = {
970
+ "query_string": "Hello",
971
+ "editors_picks-TOTAL_FORMS": 2,
972
+ "editors_picks-INITIAL_FORMS": 2,
973
+ "editors_picks-MAX_NUM_FORMS": 1000,
974
+ "editors_picks-0-id": self.search_pick.id,
975
+ "editors_picks-0-DELETE": "",
976
+ "editors_picks-0-ORDER": 0,
977
+ "editors_picks-0-page": 1,
978
+ "editors_picks-0-description": "Description has changed", # Change
979
+ "editors_picks-1-id": self.search_pick_2.id,
980
+ "editors_picks-1-DELETE": "",
981
+ "editors_picks-1-ORDER": 1,
982
+ "editors_picks-1-page": 9214599,
983
+ "editors_picks-1-description": "Homepage",
984
+ }
985
+ response = self.client.post(
986
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)), post_data
987
+ )
988
+
989
+ # User should be given an error on the specific field in the form
990
+ self.assertEqual(response.status_code, 200)
991
+ self.assertFormSetError(
992
+ response.context["searchpicks_formset"],
993
+ 1,
994
+ "page",
995
+ "Select a valid choice. "
996
+ "That choice is not one of the available choices.",
997
+ )
998
+ # Should not raise an error anywhere else
999
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, None, [])
1000
+ self.assertFormSetError(response.context["searchpicks_formset"], 1, None, [])
1001
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
1002
+
1003
+ def test_post_change_query_string(self):
1004
+ current_picks = set(self.query.editors_picks.all())
1005
+ # Submit
1006
+ post_data = {
1007
+ "query_string": "Hello again",
1008
+ "editors_picks-TOTAL_FORMS": 2,
1009
+ "editors_picks-INITIAL_FORMS": 2,
1010
+ "editors_picks-MAX_NUM_FORMS": 1000,
1011
+ "editors_picks-0-id": self.search_pick.id,
1012
+ "editors_picks-0-DELETE": "",
1013
+ "editors_picks-0-ORDER": 0,
1014
+ "editors_picks-0-page": 1,
1015
+ "editors_picks-0-description": "Description has changed", # Change
1016
+ "editors_picks-1-id": self.search_pick_2.id,
1017
+ "editors_picks-1-DELETE": "",
1018
+ "editors_picks-1-ORDER": 1,
1019
+ "editors_picks-1-page": 2,
1020
+ "editors_picks-1-description": "Homepage",
1021
+ }
1022
+ response = self.client.post(
1023
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)), post_data
1024
+ )
1025
+
1026
+ # User should be redirected back to the index
1027
+ self.assertRedirects(response, reverse("wagtailsearchpromotions:index"))
1028
+
1029
+ # Ensure search picks from the old query are moved to the new one
1030
+ new_query = Query.get("Hello again")
1031
+ self.assertEqual(set(new_query.editors_picks.all()), current_picks)
1032
+ self.search_pick.refresh_from_db()
1033
+ self.assertEqual(self.search_pick.query, new_query)
1034
+ self.assertEqual(self.query.editors_picks.count(), 0)
1035
+
1036
+ # Check that the search pick description was edited
1037
+ self.assertEqual(self.search_pick.description, "Description has changed")
1038
+
657
1039
  def test_post_reorder(self):
658
1040
  # Check order before reordering
659
1041
  self.assertEqual(Query.get("Hello").editors_picks.all()[0], self.search_pick)
@@ -695,6 +1077,45 @@ class TestSearchPromotionsEditView(WagtailTestUtils, TestCase):
695
1077
  self.assertEqual(Query.get("Hello").editors_picks.all()[0], self.search_pick_2)
696
1078
  self.assertEqual(Query.get("Hello").editors_picks.all()[1], self.search_pick)
697
1079
 
1080
+ def test_post_with_external_link(self):
1081
+ # Submit
1082
+ post_data = {
1083
+ "query_string": "Hello",
1084
+ "editors_picks-TOTAL_FORMS": 2,
1085
+ "editors_picks-INITIAL_FORMS": 2,
1086
+ "editors_picks-MAX_NUM_FORMS": 1000,
1087
+ "editors_picks-0-id": self.search_pick.id,
1088
+ "editors_picks-0-DELETE": "",
1089
+ "editors_picks-0-ORDER": 1, # Change
1090
+ "editors_picks-0-external_link_url": "https://wagtail.org",
1091
+ "editors_picks-0-external_link_text": "Wagtail",
1092
+ "editors_picks-0-description": "Root page",
1093
+ "editors_picks-1-id": self.search_pick_2.id,
1094
+ "editors_picks-1-DELETE": "",
1095
+ "editors_picks-1-ORDER": 0, # Change
1096
+ "editors_picks-1-external_link_url": "https://djangoproject.com",
1097
+ "editors_picks-1-external_link_text": "Django",
1098
+ "editors_picks-1-description": "Homepage",
1099
+ }
1100
+ response = self.client.post(
1101
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)), post_data
1102
+ )
1103
+
1104
+ # User should be redirected back to the index
1105
+ self.assertRedirects(response, reverse("wagtailsearchpromotions:index"))
1106
+
1107
+ # Check that the search pick was created
1108
+ self.assertTrue(
1109
+ Query.get("Hello")
1110
+ .editors_picks.filter(external_link_url="https://wagtail.org")
1111
+ .exists()
1112
+ )
1113
+ self.assertTrue(
1114
+ Query.get("Hello")
1115
+ .editors_picks.filter(external_link_url="https://djangoproject.com")
1116
+ .exists()
1117
+ )
1118
+
698
1119
  def test_post_delete_recommendation(self):
699
1120
  # Submit
700
1121
  post_data = {
@@ -759,10 +1180,184 @@ class TestSearchPromotionsEditView(WagtailTestUtils, TestCase):
759
1180
  "Please specify at least one recommendation for this search term.",
760
1181
  )
761
1182
 
1183
+ def test_post_with_page_and_external_link(self):
1184
+ post_data = {
1185
+ "query_string": "Hello",
1186
+ "editors_picks-TOTAL_FORMS": 2,
1187
+ "editors_picks-INITIAL_FORMS": 2,
1188
+ "editors_picks-MAX_NUM_FORMS": 1000,
1189
+ "editors_picks-0-id": self.search_pick.id,
1190
+ "editors_picks-0-DELETE": "",
1191
+ "editors_picks-0-ORDER": 0,
1192
+ "editors_picks-0-page": 1,
1193
+ "editors_picks-0-description": "Description has changed", # Change
1194
+ "editors_picks-1-id": self.search_pick_2.id,
1195
+ "editors_picks-1-DELETE": "",
1196
+ "editors_picks-1-ORDER": 1,
1197
+ "editors_picks-1-page": 2,
1198
+ "editors_picks-1-external_link_url": "https://wagtail.org",
1199
+ "editors_picks-1-external_link_text": "Wagtail",
1200
+ "editors_picks-1-description": "Homepage",
1201
+ }
1202
+ response = self.client.post(
1203
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)), post_data
1204
+ )
1205
+
1206
+ # User should be given an error on a specific form in the formset
1207
+ self.assertEqual(response.status_code, 200)
1208
+ self.assertFormSetError(
1209
+ response.context["searchpicks_formset"],
1210
+ 1,
1211
+ None,
1212
+ "Please only select a page OR enter an external link.",
1213
+ )
1214
+ # Should not raise an error anywhere else
1215
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
1216
+ self.assertFormSetError(response.context["searchpicks_formset"], 0, None, [])
1217
+
1218
+ def test_post_missing_recommendation(self):
1219
+ post_data = {
1220
+ "query_string": "Hello",
1221
+ "editors_picks-TOTAL_FORMS": 2,
1222
+ "editors_picks-INITIAL_FORMS": 2,
1223
+ "editors_picks-MAX_NUM_FORMS": 1000,
1224
+ "editors_picks-0-id": self.search_pick.id,
1225
+ "editors_picks-0-DELETE": "",
1226
+ "editors_picks-0-ORDER": 0,
1227
+ "editors_picks-0-description": "Description has changed", # Change
1228
+ "editors_picks-1-id": self.search_pick_2.id,
1229
+ "editors_picks-1-DELETE": "",
1230
+ "editors_picks-1-ORDER": 1,
1231
+ "editors_picks-1-external_link_url": "https://wagtail.org",
1232
+ "editors_picks-1-external_link_text": "Wagtail",
1233
+ "editors_picks-1-description": "Homepage",
1234
+ }
1235
+ response = self.client.post(
1236
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)), post_data
1237
+ )
1238
+
1239
+ # User should be given an error on a specific form in the formset
1240
+ self.assertEqual(response.status_code, 200)
1241
+ self.assertFormSetError(
1242
+ response.context["searchpicks_formset"],
1243
+ 0,
1244
+ None,
1245
+ "You must recommend a page OR an external link.",
1246
+ )
1247
+ # Should not raise an error anywhere else
1248
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
1249
+ self.assertFormSetError(response.context["searchpicks_formset"], 1, None, [])
1250
+
1251
+ def test_post_invalid_external_link(self):
1252
+ post_data = {
1253
+ "query_string": "Hello",
1254
+ "editors_picks-TOTAL_FORMS": 2,
1255
+ "editors_picks-INITIAL_FORMS": 2,
1256
+ "editors_picks-MAX_NUM_FORMS": 1000,
1257
+ "editors_picks-0-id": self.search_pick.id,
1258
+ "editors_picks-0-DELETE": "",
1259
+ "editors_picks-0-ORDER": 0,
1260
+ "editors_picks-0-page": 1,
1261
+ "editors_picks-0-description": "Description has changed", # Change
1262
+ "editors_picks-1-id": self.search_pick_2.id,
1263
+ "editors_picks-1-DELETE": "",
1264
+ "editors_picks-1-ORDER": 1,
1265
+ "editors_picks-1-external_link_url": "notalink",
1266
+ "editors_picks-1-external_link_text": "Wagtail",
1267
+ "editors_picks-1-description": "Homepage",
1268
+ }
1269
+ response = self.client.post(
1270
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)), post_data
1271
+ )
1272
+
1273
+ # User should be given an error on the specific field in the form
1274
+ self.assertEqual(response.status_code, 200)
1275
+ self.assertFormSetError(
1276
+ response.context["searchpicks_formset"],
1277
+ 1,
1278
+ "external_link_url",
1279
+ "Enter a valid URL.",
1280
+ )
1281
+ # Should not raise an error anywhere else
1282
+ self.assertFormSetError(response.context["searchpicks_formset"], 1, None, [])
1283
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
1284
+
1285
+ def test_post_missing_external_text(self):
1286
+ post_data = {
1287
+ "query_string": "Hello",
1288
+ "editors_picks-TOTAL_FORMS": 2,
1289
+ "editors_picks-INITIAL_FORMS": 2,
1290
+ "editors_picks-MAX_NUM_FORMS": 1000,
1291
+ "editors_picks-0-id": self.search_pick.id,
1292
+ "editors_picks-0-DELETE": "",
1293
+ "editors_picks-0-ORDER": 0,
1294
+ "editors_picks-0-page": 1,
1295
+ "editors_picks-0-description": "Description has changed", # Change
1296
+ "editors_picks-1-id": self.search_pick_2.id,
1297
+ "editors_picks-1-DELETE": "",
1298
+ "editors_picks-1-ORDER": 1,
1299
+ "editors_picks-1-external_link_url": "https://wagtail.org",
1300
+ "editors_picks-1-description": "Homepage",
1301
+ }
1302
+ response = self.client.post(
1303
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)), post_data
1304
+ )
1305
+
1306
+ # User should be given an error on the specific field in the form
1307
+ self.assertEqual(response.status_code, 200)
1308
+ self.assertFormSetError(
1309
+ response.context["searchpicks_formset"],
1310
+ 1,
1311
+ "external_link_text",
1312
+ "You must enter an external link text if you enter an external link URL.",
1313
+ )
1314
+
1315
+ # Should not raise an error anywhere else
1316
+ self.assertFormSetError(response.context["searchpicks_formset"], 1, None, [])
1317
+ self.assertFormSetError(response.context["searchpicks_formset"], None, None, [])
1318
+
1319
+ def test_get_with_no_permission(self):
1320
+ self.user.is_superuser = False
1321
+ self.user.save()
1322
+ # Only basic access_admin permission is given
1323
+ self.user.user_permissions.add(
1324
+ Permission.objects.get(
1325
+ content_type__app_label="wagtailadmin",
1326
+ codename="access_admin",
1327
+ )
1328
+ )
1329
+
1330
+ response = self.client.get(
1331
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)),
1332
+ )
1333
+ self.assertEqual(response.status_code, 302)
1334
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
1335
+
1336
+ def test_get_with_edit_permission_only(self):
1337
+ self.user.is_superuser = False
1338
+ self.user.save()
1339
+ # Only basic access_admin permission is given
1340
+ self.user.user_permissions.add(
1341
+ Permission.objects.get(
1342
+ content_type__app_label="wagtailadmin",
1343
+ codename="access_admin",
1344
+ ),
1345
+ Permission.objects.get(
1346
+ content_type__app_label="wagtailsearchpromotions",
1347
+ codename="change_searchpromotion",
1348
+ ),
1349
+ )
1350
+
1351
+ response = self.client.get(
1352
+ reverse("wagtailsearchpromotions:edit", args=(self.query.id,)),
1353
+ )
1354
+ self.assertEqual(response.status_code, 200)
1355
+ self.assertTemplateUsed(response, "wagtailsearchpromotions/edit.html")
1356
+
762
1357
 
763
1358
  class TestSearchPromotionsDeleteView(WagtailTestUtils, TestCase):
764
1359
  def setUp(self):
765
- self.login()
1360
+ self.user = self.login()
766
1361
 
767
1362
  # Create a search pick to delete
768
1363
  self.query = Query.get("Hello")
@@ -799,6 +1394,44 @@ class TestSearchPromotionsDeleteView(WagtailTestUtils, TestCase):
799
1394
  SearchPromotion.objects.filter(id=self.search_pick.id).exists()
800
1395
  )
801
1396
 
1397
+ def test_get_with_no_permission(self):
1398
+ self.user.is_superuser = False
1399
+ self.user.save()
1400
+ # Only basic access_admin permission is given
1401
+ self.user.user_permissions.add(
1402
+ Permission.objects.get(
1403
+ content_type__app_label="wagtailadmin",
1404
+ codename="access_admin",
1405
+ )
1406
+ )
1407
+
1408
+ response = self.client.get(
1409
+ reverse("wagtailsearchpromotions:delete", args=(self.query.id,)),
1410
+ )
1411
+ self.assertEqual(response.status_code, 302)
1412
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
1413
+
1414
+ def test_get_with_edit_permission_only(self):
1415
+ self.user.is_superuser = False
1416
+ self.user.save()
1417
+ # Only basic access_admin permission is given
1418
+ self.user.user_permissions.add(
1419
+ Permission.objects.get(
1420
+ content_type__app_label="wagtailadmin",
1421
+ codename="access_admin",
1422
+ ),
1423
+ Permission.objects.get(
1424
+ content_type__app_label="wagtailsearchpromotions",
1425
+ codename="delete_searchpromotion",
1426
+ ),
1427
+ )
1428
+
1429
+ response = self.client.get(
1430
+ reverse("wagtailsearchpromotions:delete", args=(self.query.id,)),
1431
+ )
1432
+ self.assertEqual(response.status_code, 200)
1433
+ self.assertTemplateUsed(response, "wagtailsearchpromotions/confirm_delete.html")
1434
+
802
1435
 
803
1436
  class TestGarbageCollectManagementCommand(TestCase):
804
1437
  def test_garbage_collect_command(self):
@@ -972,3 +1605,171 @@ class TestQueryPopularity(TestCase):
972
1605
  self.assertEqual(popular_queries[0], Query.get("unpopular query"))
973
1606
  self.assertEqual(popular_queries[1], Query.get("popular query"))
974
1607
  self.assertEqual(popular_queries[2], Query.get("little popular query"))
1608
+
1609
+
1610
+ class TestQueryHitsReportView(BaseReportViewTestCase):
1611
+ url_name = "wagtailsearchpromotions:search_terms"
1612
+
1613
+ @classmethod
1614
+ def setUpTestData(self):
1615
+ self.query = Query.get("A query with three hits")
1616
+ self.query.add_hit()
1617
+ self.query.add_hit()
1618
+ self.query.add_hit()
1619
+ Query.get("a query with no hits")
1620
+ Query.get("A query with one hit").add_hit()
1621
+ query = Query.get("A query with two hits")
1622
+ query.add_hit()
1623
+ query.add_hit()
1624
+
1625
+ def test_simple(self):
1626
+ response = self.get()
1627
+ self.assertEqual(response.status_code, 200)
1628
+ self.assertTemplateUsed(response, "wagtailadmin/reports/base_report.html")
1629
+ self.assertTemplateUsed(
1630
+ response,
1631
+ "wagtailadmin/reports/base_report_results.html",
1632
+ )
1633
+ self.assertBreadcrumbs(
1634
+ [{"url": "", "label": "Search terms"}],
1635
+ response.content,
1636
+ )
1637
+
1638
+ soup = self.get_soup(response.content)
1639
+ trs = soup.select("main tr")
1640
+
1641
+ # Default ordering should be by hits descending
1642
+ self.assertEqual(
1643
+ [[cell.text.strip() for cell in tr.select("th,td")] for tr in trs],
1644
+ [
1645
+ ["Search term(s)", "Views"],
1646
+ ["a query with three hits", "3"],
1647
+ ["a query with two hits", "2"],
1648
+ ["a query with one hit", "1"],
1649
+ ],
1650
+ )
1651
+
1652
+ self.assertNotContains(response, "There are no results.")
1653
+ self.assertActiveFilterNotRendered(soup)
1654
+ self.assertPageTitle(soup, "Search terms - Wagtail")
1655
+
1656
+ def test_get_with_no_permissions(self):
1657
+ self.user.is_superuser = False
1658
+ self.user.save()
1659
+ self.user.user_permissions.add(
1660
+ Permission.objects.get(
1661
+ content_type__app_label="wagtailadmin", codename="access_admin"
1662
+ )
1663
+ )
1664
+
1665
+ response = self.get()
1666
+
1667
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
1668
+
1669
+ def test_csv_export(self):
1670
+ response = self.get(params={"export": "csv"})
1671
+ self.assertEqual(response.status_code, 200)
1672
+
1673
+ data_lines = response.getvalue().decode().splitlines()
1674
+ self.assertEqual(
1675
+ data_lines,
1676
+ [
1677
+ "Search term(s),Views",
1678
+ "a query with three hits,3",
1679
+ "a query with two hits,2",
1680
+ "a query with one hit,1",
1681
+ ],
1682
+ )
1683
+
1684
+ def test_xlsx_export(self):
1685
+ response = self.get(params={"export": "xlsx"})
1686
+ self.assertEqual(response.status_code, 200)
1687
+ workbook_data = response.getvalue()
1688
+ worksheet = load_workbook(filename=BytesIO(workbook_data))["Sheet1"]
1689
+ cell_array = [[cell.value for cell in row] for row in worksheet.rows]
1690
+ self.assertEqual(
1691
+ cell_array,
1692
+ [
1693
+ ["Search term(s)", "Views"],
1694
+ ["a query with three hits", 3],
1695
+ ["a query with two hits", 2],
1696
+ ["a query with one hit", 1],
1697
+ ],
1698
+ )
1699
+
1700
+ def test_ordering(self):
1701
+ cases = {
1702
+ "query_string": [
1703
+ ["a query with one hit", "1"],
1704
+ ["a query with three hits", "3"],
1705
+ ["a query with two hits", "2"],
1706
+ ],
1707
+ "-query_string": [
1708
+ ["a query with two hits", "2"],
1709
+ ["a query with three hits", "3"],
1710
+ ["a query with one hit", "1"],
1711
+ ],
1712
+ "_hits": [
1713
+ ["a query with one hit", "1"],
1714
+ ["a query with two hits", "2"],
1715
+ ["a query with three hits", "3"],
1716
+ ],
1717
+ "-_hits": [
1718
+ ["a query with three hits", "3"],
1719
+ ["a query with two hits", "2"],
1720
+ ["a query with one hit", "1"],
1721
+ ],
1722
+ }
1723
+ for ordering, results in cases.items():
1724
+ with self.subTest(ordering=ordering):
1725
+ response = self.get(params={"ordering": ordering})
1726
+ self.assertEqual(response.status_code, 200)
1727
+ soup = self.get_soup(response.content)
1728
+ trs = soup.select("main tbody tr")
1729
+ self.assertEqual(
1730
+ [[cell.text.strip() for cell in tr.select("td")] for tr in trs],
1731
+ results,
1732
+ )
1733
+
1734
+
1735
+ class TestFilteredQueryHitsView(BaseReportViewTestCase):
1736
+ url_name = "wagtailsearchpromotions:search_terms"
1737
+
1738
+ def setUp(self):
1739
+ self.user = self.login()
1740
+ self.query_hit = Query.get("This will be found")
1741
+ self.date = timezone.now().date()
1742
+ self.query_hit.add_hit(date=self.date)
1743
+
1744
+ def test_search_by_query_string(self):
1745
+ response = self.get(params={"q": "Found"})
1746
+ self.assertEqual(response.status_code, 200)
1747
+ self.assertContains(response, "this will be found")
1748
+ self.assertNotContains(response, "There are no results.")
1749
+ self.assertActiveFilterNotRendered(self.get_soup(response.content))
1750
+
1751
+ response = self.get(params={"q": "Not found"})
1752
+ self.assertEqual(response.status_code, 200)
1753
+ self.assertContains(response, "There are no results.")
1754
+ self.assertNotContains(response, "this will be found")
1755
+ self.assertActiveFilterNotRendered(self.get_soup(response.content))
1756
+
1757
+ def test_filter_by_date(self):
1758
+ params = {
1759
+ "hit_date_from": self.date.replace(day=1, month=1),
1760
+ }
1761
+ response = self.get(params=params)
1762
+ self.assertEqual(response.status_code, 200)
1763
+ self.assertContains(response, "this will be found")
1764
+ self.assertNotContains(response, "There are no results.")
1765
+ self.assertActiveFilter(
1766
+ self.get_soup(response.content), "hit_date_from", params["hit_date_from"]
1767
+ )
1768
+
1769
+ params["hit_date_from"] = self.date.replace(year=self.date.year + 1)
1770
+ params["hit_date_to"] = self.date.replace(year=self.date.year + 2)
1771
+
1772
+ response = self.get(params=params)
1773
+ self.assertEqual(response.status_code, 200)
1774
+ self.assertContains(response, "There are no results.")
1775
+ self.assertNotContains(response, "this will be found")