wagtail 7.2.1__py3-none-any.whl → 7.3rc1__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 (312) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/actions/copy_for_translation.py +4 -2
  3. wagtail/admin/action_menu.py +4 -1
  4. wagtail/admin/api/actions/convert_alias.py +2 -2
  5. wagtail/admin/api/actions/copy.py +2 -2
  6. wagtail/admin/api/actions/copy_for_translation.py +2 -2
  7. wagtail/admin/api/actions/create_alias.py +2 -2
  8. wagtail/admin/api/actions/delete.py +1 -1
  9. wagtail/admin/api/actions/move.py +1 -1
  10. wagtail/admin/api/actions/publish.py +2 -2
  11. wagtail/admin/api/actions/revert_to_page_revision.py +2 -2
  12. wagtail/admin/api/actions/unpublish.py +1 -1
  13. wagtail/admin/api/filters.py +2 -2
  14. wagtail/admin/compare.py +22 -0
  15. wagtail/admin/forms/account.py +52 -1
  16. wagtail/admin/forms/comments.py +53 -0
  17. wagtail/admin/forms/models.py +36 -0
  18. wagtail/admin/forms/pages.py +7 -0
  19. wagtail/admin/forms/workflows.py +5 -2
  20. wagtail/admin/icons.py +4 -3
  21. wagtail/admin/locale/ar/LC_MESSAGES/django.mo +0 -0
  22. wagtail/admin/locale/ar/LC_MESSAGES/django.po +35 -0
  23. wagtail/admin/locale/en/LC_MESSAGES/django.po +262 -234
  24. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +72 -43
  25. wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
  26. wagtail/admin/locale/it/LC_MESSAGES/django.po +566 -6
  27. wagtail/admin/locale/it/LC_MESSAGES/djangojs.mo +0 -0
  28. wagtail/admin/locale/it/LC_MESSAGES/djangojs.po +40 -2
  29. wagtail/admin/locale/nl/LC_MESSAGES/django.mo +0 -0
  30. wagtail/admin/locale/nl/LC_MESSAGES/django.po +2 -2
  31. wagtail/admin/locale/sv/LC_MESSAGES/django.mo +0 -0
  32. wagtail/admin/locale/sv/LC_MESSAGES/django.po +330 -15
  33. wagtail/admin/locale/sv/LC_MESSAGES/djangojs.mo +0 -0
  34. wagtail/admin/locale/sv/LC_MESSAGES/djangojs.po +3 -2
  35. wagtail/admin/panels/comment_panel.py +1 -51
  36. wagtail/admin/panels/title_field_panel.py +3 -1
  37. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  38. wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
  39. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  40. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  41. wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +1 -1
  42. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  43. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  44. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  45. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  46. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  47. wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +1 -1
  48. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  49. wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
  50. wagtail/admin/templates/wagtailadmin/base.html +1 -1
  51. wagtail/admin/templates/wagtailadmin/generic/edit_partials.html +100 -0
  52. wagtail/admin/templates/wagtailadmin/generic/form.html +26 -5
  53. wagtail/admin/templates/wagtailadmin/generic/includes/_loaded_revision_inputs.html +3 -0
  54. wagtail/admin/templates/wagtailadmin/generic/listing_results.html +1 -17
  55. wagtail/admin/templates/wagtailadmin/pages/create.html +14 -4
  56. wagtail/admin/templates/wagtailadmin/pages/edit.html +16 -3
  57. wagtail/admin/templates/wagtailadmin/pages/edit_partials.html +31 -0
  58. wagtail/admin/templates/wagtailadmin/pages/index_results.html +1 -9
  59. wagtail/admin/templates/wagtailadmin/shared/autosave/indicator.html +36 -0
  60. wagtail/admin/templates/wagtailadmin/shared/breadcrumbs.html +1 -1
  61. wagtail/admin/templates/wagtailadmin/shared/editing_sessions/list.html +2 -2
  62. wagtail/admin/templates/wagtailadmin/shared/editing_sessions/module.html +19 -3
  63. wagtail/admin/templates/wagtailadmin/shared/headers/_actions.html +5 -0
  64. wagtail/admin/templates/wagtailadmin/shared/headers/_history_icon_link.html +2 -2
  65. wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +8 -9
  66. wagtail/admin/templates/wagtailadmin/shared/listing/filter_partials.html +19 -0
  67. wagtail/admin/templates/wagtailadmin/shared/side_panel_toggle.html +3 -3
  68. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/side_panel.html +3 -0
  69. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/privacy.html +18 -6
  70. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/usage.html +11 -4
  71. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/workflow.html +22 -1
  72. wagtail/admin/templates/wagtailadmin/shared/side_panels/preview.html +1 -0
  73. wagtail/admin/templates/wagtailadmin/shared/side_panels.html +1 -2
  74. wagtail/admin/templates/wagtailadmin/shared/unsaved_changes_warning.html +20 -20
  75. wagtail/admin/templates/wagtailadmin/userbar/base.html +2 -0
  76. wagtail/admin/templates/wagtailadmin/userbar/item_accessibility.html +1 -1
  77. wagtail/admin/templatetags/wagtailadmin_tags.py +6 -14
  78. wagtail/admin/tests/pages/test_create_page.py +298 -1
  79. wagtail/admin/tests/pages/test_custom_listing.py +48 -2
  80. wagtail/admin/tests/pages/test_edit_page.py +721 -7
  81. wagtail/admin/tests/pages/test_explorer_view.py +9 -5
  82. wagtail/admin/tests/pages/test_page_search.py +15 -0
  83. wagtail/admin/tests/pages/test_revisions.py +4 -0
  84. wagtail/admin/tests/test_account_management.py +88 -2
  85. wagtail/admin/tests/test_collections_views.py +15 -15
  86. wagtail/admin/tests/test_compare.py +34 -0
  87. wagtail/admin/tests/test_editing_sessions.py +230 -8
  88. wagtail/admin/tests/test_forms.py +192 -1
  89. wagtail/admin/tests/test_icon_sprite.py +22 -2
  90. wagtail/admin/tests/test_page_chooser.py +13 -13
  91. wagtail/admin/tests/test_reports_views.py +4 -1
  92. wagtail/admin/tests/test_userbar.py +69 -5
  93. wagtail/admin/tests/test_views_generic.py +5 -5
  94. wagtail/admin/tests/test_workflows.py +14 -12
  95. wagtail/admin/tests/viewsets/test_model_viewset.py +13 -0
  96. wagtail/admin/ui/autosave.py +5 -0
  97. wagtail/admin/ui/editing_sessions.py +3 -0
  98. wagtail/admin/ui/side_panels.py +19 -20
  99. wagtail/admin/userbar.py +48 -27
  100. wagtail/admin/views/bulk_action/dispatcher.py +2 -2
  101. wagtail/admin/views/chooser.py +6 -6
  102. wagtail/admin/views/editing_sessions.py +20 -7
  103. wagtail/admin/views/generic/__init__.py +1 -0
  104. wagtail/admin/views/generic/chooser.py +5 -5
  105. wagtail/admin/views/generic/mixins.py +143 -26
  106. wagtail/admin/views/generic/models.py +157 -27
  107. wagtail/admin/views/generic/ordering.py +1 -1
  108. wagtail/admin/views/generic/preview.py +4 -4
  109. wagtail/admin/views/home.py +3 -1
  110. wagtail/admin/views/pages/create.py +77 -29
  111. wagtail/admin/views/pages/edit.py +160 -34
  112. wagtail/admin/views/pages/preview.py +4 -4
  113. wagtail/admin/views/pages/revisions.py +3 -0
  114. wagtail/admin/views/pages/search.py +4 -4
  115. wagtail/admin/views/pages/usage.py +2 -2
  116. wagtail/admin/views/tags.py +2 -2
  117. wagtail/admin/views/workflows.py +73 -54
  118. wagtail/admin/viewsets/model.py +34 -8
  119. wagtail/admin/widgets/button.py +11 -4
  120. wagtail/admin/widgets/chooser.py +6 -4
  121. wagtail/admin/widgets/slug.py +1 -1
  122. wagtail/api/v2/filters.py +27 -21
  123. wagtail/api/v2/pagination.py +4 -4
  124. wagtail/api/v2/views.py +7 -7
  125. wagtail/blocks/list_block.py +0 -8
  126. wagtail/blocks/migrations/migrate_operation.py +8 -1
  127. wagtail/blocks/stream_block.py +2 -10
  128. wagtail/blocks/struct_block.py +192 -12
  129. wagtail/compat.py +2 -2
  130. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +1 -1
  131. wagtail/contrib/forms/locale/it/LC_MESSAGES/django.mo +0 -0
  132. wagtail/contrib/forms/locale/it/LC_MESSAGES/django.po +40 -2
  133. wagtail/contrib/frontend_cache/backends/azure.py +6 -6
  134. wagtail/contrib/frontend_cache/backends/cloudfront.py +2 -2
  135. wagtail/contrib/frontend_cache/utils.py +1 -1
  136. wagtail/contrib/redirects/forms.py +1 -1
  137. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +14 -14
  138. wagtail/contrib/redirects/locale/it/LC_MESSAGES/django.mo +0 -0
  139. wagtail/contrib/redirects/locale/it/LC_MESSAGES/django.po +15 -2
  140. wagtail/contrib/redirects/locale/sv/LC_MESSAGES/django.mo +0 -0
  141. wagtail/contrib/redirects/locale/sv/LC_MESSAGES/django.po +16 -3
  142. wagtail/contrib/redirects/middleware.py +11 -7
  143. wagtail/contrib/redirects/models.py +17 -1
  144. wagtail/contrib/redirects/tests/test_import_admin_views.py +3 -3
  145. wagtail/contrib/redirects/tests/test_redirects.py +56 -8
  146. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +6 -6
  147. wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.mo +0 -0
  148. wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.po +43 -2
  149. wagtail/contrib/search_promotions/tests.py +1 -1
  150. wagtail/contrib/search_promotions/views/settings.py +24 -25
  151. wagtail/contrib/settings/jinja2tags.py +2 -2
  152. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +2 -2
  153. wagtail/contrib/settings/locale/it/LC_MESSAGES/django.mo +0 -0
  154. wagtail/contrib/settings/locale/it/LC_MESSAGES/django.po +6 -2
  155. wagtail/contrib/settings/locale/sv/LC_MESSAGES/django.mo +0 -0
  156. wagtail/contrib/settings/locale/sv/LC_MESSAGES/django.po +3 -2
  157. wagtail/contrib/settings/tests/generic/test_admin.py +118 -2
  158. wagtail/contrib/settings/tests/site_specific/test_admin.py +78 -15
  159. wagtail/contrib/settings/tests/site_specific/test_model.py +2 -2
  160. wagtail/contrib/settings/views.py +7 -0
  161. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  162. wagtail/contrib/simple_translation/locale/it/LC_MESSAGES/django.mo +0 -0
  163. wagtail/contrib/simple_translation/locale/it/LC_MESSAGES/django.po +5 -2
  164. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
  165. wagtail/contrib/styleguide/tests.py +2 -0
  166. wagtail/contrib/styleguide/views.py +4 -5
  167. wagtail/contrib/table_block/locale/ar/LC_MESSAGES/django.mo +0 -0
  168. wagtail/contrib/table_block/locale/ar/LC_MESSAGES/django.po +5 -1
  169. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  170. wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.mo +0 -0
  171. wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.po +5 -2
  172. wagtail/contrib/typed_table_block/blocks.py +37 -0
  173. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
  174. wagtail/contrib/typed_table_block/locale/sv/LC_MESSAGES/django.mo +0 -0
  175. wagtail/contrib/typed_table_block/locale/sv/LC_MESSAGES/django.po +4 -3
  176. wagtail/contrib/typed_table_block/tests.py +108 -0
  177. wagtail/coreutils.py +4 -4
  178. wagtail/documents/__init__.py +4 -4
  179. wagtail/documents/locale/en/LC_MESSAGES/django.po +15 -15
  180. wagtail/documents/locale/it/LC_MESSAGES/django.mo +0 -0
  181. wagtail/documents/locale/it/LC_MESSAGES/django.po +32 -2
  182. wagtail/documents/locale/sv/LC_MESSAGES/django.mo +0 -0
  183. wagtail/documents/locale/sv/LC_MESSAGES/django.po +32 -4
  184. wagtail/documents/models.py +1 -1
  185. wagtail/documents/tests/test_admin_views.py +19 -4
  186. wagtail/documents/tests/test_search.py +0 -2
  187. wagtail/documents/tests/test_views.py +6 -4
  188. wagtail/documents/views/bulk_actions/add_tags.py +1 -1
  189. wagtail/embeds/finders/facebook.py +3 -3
  190. wagtail/embeds/finders/instagram.py +3 -3
  191. wagtail/embeds/finders/oembed.py +7 -2
  192. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  193. wagtail/embeds/oembed_providers.py +8 -0
  194. wagtail/embeds/tests/test_embeds.py +35 -0
  195. wagtail/images/__init__.py +4 -4
  196. wagtail/images/admin_urls.py +3 -3
  197. wagtail/images/blocks.py +1 -1
  198. wagtail/images/formats.py +2 -2
  199. wagtail/images/image_operations.py +2 -2
  200. wagtail/images/locale/en/LC_MESSAGES/django.po +44 -40
  201. wagtail/images/locale/it/LC_MESSAGES/django.mo +0 -0
  202. wagtail/images/locale/it/LC_MESSAGES/django.po +50 -4
  203. wagtail/images/locale/nl/LC_MESSAGES/django.mo +0 -0
  204. wagtail/images/locale/nl/LC_MESSAGES/django.po +3 -3
  205. wagtail/images/locale/sv/LC_MESSAGES/django.mo +0 -0
  206. wagtail/images/locale/sv/LC_MESSAGES/django.po +49 -6
  207. wagtail/images/models.py +11 -10
  208. wagtail/images/templates/wagtailimages/images/index_results.html +1 -1
  209. wagtail/images/templates/wagtailimages/images/url_generator.html +17 -38
  210. wagtail/images/templates/wagtailimages/images/url_generator_output.html +28 -0
  211. wagtail/images/templatetags/wagtailimages_tags.py +4 -4
  212. wagtail/images/tests/test_admin_views.py +432 -59
  213. wagtail/images/tests/test_image_operations.py +2 -2
  214. wagtail/images/tests/test_models.py +0 -2
  215. wagtail/images/views/bulk_actions/add_tags.py +1 -1
  216. wagtail/images/views/images.py +72 -39
  217. wagtail/locale/en/LC_MESSAGES/django.po +103 -105
  218. wagtail/locale/it/LC_MESSAGES/django.mo +0 -0
  219. wagtail/locale/it/LC_MESSAGES/django.po +105 -2
  220. wagtail/locale/sv/LC_MESSAGES/django.mo +0 -0
  221. wagtail/locale/sv/LC_MESSAGES/django.po +95 -12
  222. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  223. wagtail/locales/locale/it/LC_MESSAGES/django.mo +0 -0
  224. wagtail/locales/locale/it/LC_MESSAGES/django.po +13 -2
  225. wagtail/locales/locale/sv/LC_MESSAGES/django.mo +0 -0
  226. wagtail/locales/locale/sv/LC_MESSAGES/django.po +4 -3
  227. wagtail/locales/tests.py +5 -5
  228. wagtail/models/i18n.py +4 -6
  229. wagtail/models/media.py +18 -0
  230. wagtail/models/pages.py +65 -11
  231. wagtail/models/reference_index.py +15 -0
  232. wagtail/models/revisions.py +40 -12
  233. wagtail/models/workflows.py +0 -3
  234. wagtail/permission_policies/base.py +2 -2
  235. wagtail/permission_policies/collections.py +2 -2
  236. wagtail/project_template/requirements.txt +2 -2
  237. wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
  238. wagtail/signal_handlers.py +1 -0
  239. wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
  240. wagtail/sites/locale/sv/LC_MESSAGES/django.mo +0 -0
  241. wagtail/sites/locale/sv/LC_MESSAGES/django.po +3 -2
  242. wagtail/sites/tests.py +7 -7
  243. wagtail/snippets/action_menu.py +2 -1
  244. wagtail/snippets/locale/en/LC_MESSAGES/django.po +23 -13
  245. wagtail/snippets/locale/sv/LC_MESSAGES/django.mo +0 -0
  246. wagtail/snippets/locale/sv/LC_MESSAGES/django.po +12 -1
  247. wagtail/snippets/tests/test_chooser_block.py +197 -0
  248. wagtail/snippets/tests/test_chooser_panel.py +149 -0
  249. wagtail/snippets/tests/test_chooser_views.py +375 -0
  250. wagtail/snippets/tests/test_chooser_widget.py +22 -0
  251. wagtail/snippets/tests/test_compare_revisions_view.py +167 -0
  252. wagtail/snippets/tests/test_copy_view.py +38 -0
  253. wagtail/snippets/tests/test_create_view.py +1159 -0
  254. wagtail/snippets/tests/test_delete_view.py +225 -0
  255. wagtail/snippets/tests/test_edit_view.py +2882 -0
  256. wagtail/snippets/tests/test_history_view.py +182 -0
  257. wagtail/snippets/tests/test_index_view.py +105 -0
  258. wagtail/snippets/tests/test_list_view.py +786 -0
  259. wagtail/snippets/tests/test_locking.py +28 -0
  260. wagtail/snippets/tests/test_permissions.py +167 -0
  261. wagtail/snippets/tests/test_preview.py +3 -3
  262. wagtail/snippets/tests/test_revert_view.py +343 -0
  263. wagtail/snippets/tests/test_snippet_models.py +112 -0
  264. wagtail/snippets/tests/test_unpublish_view.py +228 -0
  265. wagtail/snippets/tests/test_unschedule_view.py +151 -0
  266. wagtail/snippets/tests/test_viewset.py +38 -5
  267. wagtail/snippets/views/snippets.py +78 -30
  268. wagtail/snippets/widgets.py +2 -2
  269. wagtail/templatetags/wagtailcore_tags.py +2 -2
  270. wagtail/test/dummy_external_storage.py +1 -1
  271. wagtail/test/testapp/fixtures/test.json +22 -0
  272. wagtail/test/testapp/fixtures/test_empty.json +2 -0
  273. wagtail/test/testapp/fixtures/test_explorable_pages.json +10 -0
  274. wagtail/test/testapp/fixtures/test_specific.json +9 -0
  275. wagtail/test/testapp/migrations/0059_nopromotepage.py +25 -0
  276. wagtail/test/testapp/models.py +7 -0
  277. wagtail/test/testapp/views.py +5 -0
  278. wagtail/test/utils/page_tests.py +7 -7
  279. wagtail/test/utils/wagtail_factories/builder.py +2 -2
  280. wagtail/tests/streamfield_migrations/test_migrations.py +35 -0
  281. wagtail/tests/test_blocks.py +321 -61
  282. wagtail/tests/test_collection_model.py +12 -0
  283. wagtail/tests/test_page_model.py +190 -1
  284. wagtail/tests/test_page_privacy.py +5 -0
  285. wagtail/tests/test_reference_index.py +42 -0
  286. wagtail/tests/test_revision_model.py +118 -1
  287. wagtail/tests/test_utils.py +2 -2
  288. wagtail/users/locale/en/LC_MESSAGES/django.po +14 -14
  289. wagtail/users/locale/id_ID/LC_MESSAGES/django.mo +0 -0
  290. wagtail/users/locale/id_ID/LC_MESSAGES/django.po +6 -2
  291. wagtail/users/locale/it/LC_MESSAGES/django.mo +0 -0
  292. wagtail/users/locale/it/LC_MESSAGES/django.po +14 -2
  293. wagtail/users/locale/sv/LC_MESSAGES/django.mo +0 -0
  294. wagtail/users/locale/sv/LC_MESSAGES/django.po +48 -17
  295. wagtail/users/tests/test_admin_views.py +39 -25
  296. wagtail/users/utils.py +4 -1
  297. wagtail/users/views/groups.py +19 -5
  298. wagtail/users/wagtail_hooks.py +1 -1
  299. wagtail/utils/loading.py +2 -2
  300. wagtail-7.3rc1.data/data/AGENTS.md +21 -0
  301. wagtail-7.3rc1.data/data/CHANGELOG.txt +4941 -0
  302. wagtail-7.3rc1.data/data/CODE_OF_CONDUCT.md +71 -0
  303. wagtail-7.3rc1.data/data/CONTRIBUTORS.md +999 -0
  304. wagtail-7.3rc1.data/data/SPONSORS.md +55 -0
  305. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/METADATA +6 -6
  306. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/RECORD +310 -280
  307. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/WHEEL +1 -1
  308. wagtail/images/static/wagtailimages/js/image-url-generator.js +0 -1
  309. wagtail/snippets/tests/test_snippets.py +0 -6377
  310. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/entry_points.txt +0 -0
  311. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/licenses/LICENSE +0 -0
  312. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,786 @@
1
+ from django.contrib.admin.utils import quote
2
+ from django.contrib.auth import get_user_model
3
+ from django.contrib.auth.models import Permission
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.test import TestCase, TransactionTestCase
6
+ from django.test.utils import override_settings
7
+ from django.urls import reverse
8
+ from django.utils.timezone import now
9
+
10
+ from wagtail import hooks
11
+ from wagtail.admin.forms.search import SearchForm
12
+ from wagtail.admin.staticfiles import versioned_static
13
+ from wagtail.admin.widgets.button import Button, ButtonWithDropdown, ListingButton
14
+ from wagtail.models import Locale, ModelLogEntry
15
+ from wagtail.snippets.widgets import (
16
+ SnippetListingButton,
17
+ )
18
+ from wagtail.test.snippets.models import (
19
+ NonAutocompleteSearchableSnippet,
20
+ SearchableSnippet,
21
+ StandardSnippetWithCustomPrimaryKey,
22
+ TranslatableSnippet,
23
+ )
24
+ from wagtail.test.testapp.models import (
25
+ Advert,
26
+ DraftStateModel,
27
+ FullFeaturedSnippet,
28
+ )
29
+ from wagtail.test.utils import WagtailTestUtils
30
+ from wagtail.utils.deprecation import RemovedInWagtail80Warning
31
+
32
+
33
+ class TestSnippetListView(WagtailTestUtils, TestCase):
34
+ def setUp(self):
35
+ self.login()
36
+ user_model = get_user_model()
37
+ self.user = user_model.objects.get()
38
+
39
+ def get(self, params=None):
40
+ return self.client.get(reverse("wagtailsnippets_tests_advert:list"), params)
41
+
42
+ def test_simple(self):
43
+ response = self.get()
44
+ self.assertEqual(response.status_code, 200)
45
+ self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html")
46
+ self.assertEqual(response.context["header_icon"], "snippet")
47
+
48
+ def get_with_limited_permissions(self):
49
+ self.user.is_superuser = False
50
+ self.user.user_permissions.add(
51
+ Permission.objects.get(
52
+ content_type__app_label="wagtailadmin", codename="access_admin"
53
+ )
54
+ )
55
+ self.user.save()
56
+
57
+ response = self.get()
58
+ self.assertEqual(response.status_code, 302)
59
+
60
+ def get_with_edit_permission_only(self):
61
+ self.user.is_superuser = False
62
+ self.user.user_permissions.add(
63
+ Permission.objects.get(
64
+ content_type__app_label="wagtailadmin", codename="access_admin"
65
+ ),
66
+ Permission.objects.get(
67
+ content_type__app_label="tests", codename="change_advert"
68
+ ),
69
+ )
70
+ self.user.save()
71
+
72
+ response = self.get()
73
+ self.assertEqual(response.status_code, 200)
74
+ self.assertContains(
75
+ response,
76
+ "<p>There are no adverts to display.</p>",
77
+ html=True,
78
+ )
79
+ self.assertNotContains(response, reverse("wagtailsnippets_tests_advert:add"))
80
+
81
+ def test_ordering(self):
82
+ """
83
+ Listing should be ordered descending by PK if no ordering has been set on the model
84
+ """
85
+ for i in range(1, 11):
86
+ Advert.objects.create(pk=i, text="advert %d" % i)
87
+ response = self.get()
88
+ self.assertEqual(response.status_code, 200)
89
+ self.assertEqual(response.context["page_obj"][0].text, "advert 10")
90
+
91
+ def test_simple_pagination(self):
92
+ pages = ["0", "1", "-1", "9999", "Not a page"]
93
+ for page in pages:
94
+ response = self.get({"p": page})
95
+ self.assertEqual(response.status_code, 200)
96
+ self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html")
97
+
98
+ def test_displays_add_button(self):
99
+ self.assertContains(self.get(), "Add advert")
100
+
101
+ def test_not_searchable(self):
102
+ self.assertFalse(self.get().context.get("search_form"))
103
+
104
+ def test_register_snippet_listing_buttons_hook_deprecated_class(self):
105
+ advert = Advert.objects.create(text="My Lovely advert")
106
+
107
+ def snippet_listing_buttons(snippet, user, next_url=None):
108
+ self.assertEqual(snippet, advert)
109
+ self.assertEqual(user, self.user)
110
+ self.assertEqual(next_url, reverse("wagtailsnippets_tests_advert:list"))
111
+
112
+ yield SnippetListingButton(
113
+ "Another useless snippet listing button", "/custom-url", priority=10
114
+ )
115
+
116
+ with hooks.register_temporarily(
117
+ "register_snippet_listing_buttons", snippet_listing_buttons
118
+ ):
119
+ with self.assertWarnsMessage(
120
+ RemovedInWagtail80Warning,
121
+ "`SnippetListingButton` is deprecated. "
122
+ "Use `wagtail.admin.widgets.button.Button` "
123
+ "or `wagtail.admin.widgets.button.ListingButton` instead.",
124
+ ):
125
+ response = self.get()
126
+
127
+ self.assertEqual(response.status_code, 200)
128
+ self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html")
129
+
130
+ soup = self.get_soup(response.content)
131
+ actions = soup.select_one("tbody tr td ul.actions")
132
+ top_level_custom_button = actions.select_one("li > a[href='/custom-url']")
133
+ self.assertIsNone(top_level_custom_button)
134
+ custom_button = actions.select_one(
135
+ "li [data-controller='w-dropdown'] a[href='/custom-url']"
136
+ )
137
+ self.assertIsNotNone(custom_button)
138
+ self.assertEqual(
139
+ custom_button.text.strip(),
140
+ "Another useless snippet listing button",
141
+ )
142
+
143
+ def test_register_snippet_listing_buttons_hook(self):
144
+ advert = Advert.objects.create(text="My Lovely advert")
145
+
146
+ def snippet_listing_buttons(snippet, user, next_url=None):
147
+ self.assertEqual(snippet, advert)
148
+ self.assertEqual(user, self.user)
149
+ self.assertEqual(next_url, reverse("wagtailsnippets_tests_advert:list"))
150
+
151
+ yield ListingButton(
152
+ "A useless top-level snippet listing button",
153
+ "/custom-url",
154
+ priority=10,
155
+ )
156
+
157
+ yield Button(
158
+ "A useless snippet listing button inside the 'More' dropdown",
159
+ "/custom-url",
160
+ priority=10,
161
+ )
162
+
163
+ with hooks.register_temporarily(
164
+ "register_snippet_listing_buttons", snippet_listing_buttons
165
+ ):
166
+ response = self.get()
167
+
168
+ self.assertEqual(response.status_code, 200)
169
+ self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html")
170
+
171
+ soup = self.get_soup(response.content)
172
+ actions = soup.select_one("tbody tr td ul.actions")
173
+ custom_buttons = actions.select("a[href='/custom-url']")
174
+ top_level_custom_button = actions.select_one("li > a[href='/custom-url']")
175
+ self.assertIs(top_level_custom_button, custom_buttons[0])
176
+ self.assertEqual(
177
+ top_level_custom_button.text.strip(),
178
+ "A useless top-level snippet listing button",
179
+ )
180
+ in_dropdown_custom_button = actions.select_one(
181
+ "li [data-controller='w-dropdown'] a[href='/custom-url']"
182
+ )
183
+ self.assertIs(in_dropdown_custom_button, custom_buttons[1])
184
+ self.assertEqual(
185
+ in_dropdown_custom_button.text.strip(),
186
+ "A useless snippet listing button inside the 'More' dropdown",
187
+ )
188
+
189
+ def test_register_snippet_listing_buttons_hook_with_dropdown(self):
190
+ advert = Advert.objects.create(text="My Lovely advert")
191
+
192
+ def snippet_listing_buttons(snippet, user, next_url=None):
193
+ self.assertEqual(snippet, advert)
194
+ self.assertEqual(user, self.user)
195
+ self.assertEqual(next_url, reverse("wagtailsnippets_tests_advert:list"))
196
+ yield ButtonWithDropdown(
197
+ label="Moar pls!",
198
+ buttons=[ListingButton("Alrighty", "/cheers", priority=10)],
199
+ attrs={"data-foo": "bar"},
200
+ )
201
+
202
+ with hooks.register_temporarily(
203
+ "register_snippet_listing_buttons", snippet_listing_buttons
204
+ ):
205
+ response = self.get()
206
+
207
+ self.assertEqual(response.status_code, 200)
208
+ self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html")
209
+
210
+ soup = self.get_soup(response.content)
211
+ actions = soup.select_one("tbody tr td ul.actions")
212
+ nested_dropdown = actions.select_one(
213
+ "li [data-controller='w-dropdown'] [data-controller='w-dropdown']"
214
+ )
215
+ self.assertIsNone(nested_dropdown)
216
+ dropdown_buttons = actions.select("li > [data-controller='w-dropdown']")
217
+ # Default "More" button and the custom "Moar pls!" button
218
+ self.assertEqual(len(dropdown_buttons), 2)
219
+ custom_dropdown = None
220
+ for button in dropdown_buttons:
221
+ if "Moar pls!" in button.text.strip():
222
+ custom_dropdown = button
223
+ self.assertIsNotNone(custom_dropdown)
224
+ self.assertEqual(custom_dropdown.select_one("button").text.strip(), "Moar pls!")
225
+ self.assertEqual(custom_dropdown.get("data-foo"), "bar")
226
+ # Should contain the custom button inside the custom dropdown
227
+ custom_button = custom_dropdown.find("a", attrs={"href": "/cheers"})
228
+ self.assertIsNotNone(custom_button)
229
+ self.assertEqual(custom_button.text.strip(), "Alrighty")
230
+
231
+ def test_construct_snippet_listing_buttons_hook(self):
232
+ Advert.objects.create(text="My Lovely advert")
233
+
234
+ # testapp implements a construct_snippet_listing_buttons hook
235
+ # that adds a dummy button with the label 'Dummy Button' which points
236
+ # to '/dummy-button' and is placed inside the default "More" dropdown button
237
+ response = self.get()
238
+ self.assertEqual(response.status_code, 200)
239
+ self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html")
240
+
241
+ soup = self.get_soup(response.content)
242
+ dropdowns = soup.select(
243
+ "tbody tr td ul.actions > li > [data-controller='w-dropdown']"
244
+ )
245
+ self.assertEqual(len(dropdowns), 1)
246
+ more_dropdown = dropdowns[0]
247
+ dummy_button = more_dropdown.find("a", attrs={"href": "/dummy-button"})
248
+ self.assertIsNotNone(dummy_button)
249
+ self.assertEqual(dummy_button.text.strip(), "Dummy Button")
250
+
251
+ def test_construct_snippet_listing_buttons_hook_contains_default_buttons(self):
252
+ advert = Advert.objects.create(text="My Lovely advert")
253
+ delete_url = reverse(
254
+ "wagtailsnippets_tests_advert:delete", args=[quote(advert.pk)]
255
+ )
256
+
257
+ def hide_delete_button_for_lovely_advert(buttons, snippet, user):
258
+ # Edit, delete, dummy button, copy button
259
+ self.assertEqual(len(buttons), 4)
260
+ buttons[:] = [button for button in buttons if button.url != delete_url]
261
+ self.assertEqual(len(buttons), 3)
262
+
263
+ with hooks.register_temporarily(
264
+ "construct_snippet_listing_buttons",
265
+ hide_delete_button_for_lovely_advert,
266
+ ):
267
+ response = self.get()
268
+
269
+ self.assertEqual(response.status_code, 200)
270
+ self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html")
271
+ self.assertNotContains(response, delete_url)
272
+
273
+ def test_dropdown_not_rendered_when_no_child_buttons_exist(self):
274
+ Advert.objects.create(text="My Lovely advert")
275
+
276
+ def remove_all_buttons(buttons, snippet, user):
277
+ buttons[:] = []
278
+ self.assertEqual(len(buttons), 0)
279
+
280
+ with hooks.register_temporarily(
281
+ "construct_snippet_listing_buttons",
282
+ remove_all_buttons,
283
+ ):
284
+ response = self.get()
285
+
286
+ soup = self.get_soup(response.content)
287
+ actions = soup.select_one("tbody tr td ul.actions")
288
+ self.assertIsNone(actions)
289
+
290
+ def test_use_latest_draft_as_title(self):
291
+ snippet = DraftStateModel.objects.create(text="Draft-enabled Foo, Published")
292
+ snippet.save_revision().publish()
293
+ snippet.text = "Draft-enabled Bar, In Draft"
294
+ snippet.save_revision()
295
+
296
+ response = self.client.get(
297
+ reverse("wagtailsnippets_tests_draftstatemodel:list"),
298
+ )
299
+
300
+ edit_url = reverse(
301
+ "wagtailsnippets_tests_draftstatemodel:edit",
302
+ args=[quote(snippet.pk)],
303
+ )
304
+
305
+ # Should use the latest draft title in the listing
306
+ self.assertContains(
307
+ response,
308
+ f"""
309
+ <a href="{edit_url}">
310
+ <span id="snippet_{quote(snippet.pk)}_title">
311
+ Draft-enabled Bar, In Draft
312
+ </span>
313
+ </a>
314
+ """,
315
+ html=True,
316
+ )
317
+
318
+ def test_use_fallback_for_blank_string_representation(self):
319
+ snippet = DraftStateModel.objects.create(text="", live=False)
320
+
321
+ response = self.client.get(
322
+ reverse("wagtailsnippets_tests_draftstatemodel:list"),
323
+ )
324
+
325
+ edit_url = reverse(
326
+ "wagtailsnippets_tests_draftstatemodel:edit",
327
+ args=[quote(snippet.pk)],
328
+ )
329
+ title = f"DraftStateModel object ({snippet.pk})"
330
+
331
+ self.assertContains(
332
+ response,
333
+ f"""
334
+ <a href="{edit_url}">
335
+ <span id="snippet_{quote(snippet.pk)}_title">
336
+ {title}
337
+ </span>
338
+ </a>
339
+ """,
340
+ html=True,
341
+ )
342
+
343
+ def test_use_fallback_for_blank_title_field(self):
344
+ # FullFeaturedSnippet's listing view uses the "text" field as the title column,
345
+ # rather than the str() representation. If this is blank, we show "(blank)" so that
346
+ # there is something to click on
347
+ snippet = FullFeaturedSnippet.objects.create(text="", live=False)
348
+ response = self.client.get(
349
+ reverse("some_namespace:list"),
350
+ )
351
+ edit_url = reverse(
352
+ "some_namespace:edit",
353
+ args=[quote(snippet.pk)],
354
+ )
355
+ self.assertContains(
356
+ response,
357
+ f"""
358
+ <a href="{edit_url}">
359
+ <span id="snippet_{quote(snippet.pk)}_title">
360
+ (blank)
361
+ </span>
362
+ </a>
363
+ """,
364
+ html=True,
365
+ )
366
+
367
+ def test_bulk_action_rendered(self):
368
+ response = self.get()
369
+ self.assertEqual(response.status_code, 200)
370
+ # Should render bulk actions markup
371
+ bulk_actions_js = versioned_static("wagtailadmin/js/bulk-actions.js")
372
+ soup = self.get_soup(response.content)
373
+ script = soup.select_one(f"script[src='{bulk_actions_js}']")
374
+ self.assertIsNotNone(script)
375
+ bulk_actions = soup.select("[data-bulk-action-button]")
376
+ self.assertTrue(bulk_actions)
377
+ # 'next' parameter is constructed client-side later based on filters state
378
+ for action in bulk_actions:
379
+ self.assertNotIn("next=", action["href"])
380
+
381
+
382
+ @override_settings(WAGTAIL_I18N_ENABLED=True)
383
+ class TestLocaleFeaturesOnList(WagtailTestUtils, TestCase):
384
+ @classmethod
385
+ def setUpTestData(cls):
386
+ cls.fr_locale = Locale.objects.create(language_code="fr")
387
+ cls.list_url = reverse("wagtailsnippets_snippetstests_translatablesnippet:list")
388
+ cls.add_url = reverse("wagtailsnippets_snippetstests_translatablesnippet:add")
389
+
390
+ def setUp(self):
391
+ self.user = self.login()
392
+
393
+ def _add_snippets(self):
394
+ TranslatableSnippet.objects.create(text="English snippet")
395
+ TranslatableSnippet.objects.create(text="French snippet", locale=self.fr_locale)
396
+
397
+ @override_settings(
398
+ WAGTAIL_CONTENT_LANGUAGES=[
399
+ ("ar", "Arabic"),
400
+ ("en", "English"),
401
+ ("fr", "French"),
402
+ ]
403
+ )
404
+ def test_locale_selector(self):
405
+ response = self.client.get(self.list_url)
406
+ soup = self.get_soup(response.content)
407
+
408
+ # Should only show languages that also have the corresponding Locale
409
+ # (the Arabic locale is not created in the setup, so it should not be shown)
410
+ arabic_input = soup.select_one('input[name="locale"][value="ar"]')
411
+ self.assertIsNone(arabic_input)
412
+
413
+ french_input = soup.select_one('input[name="locale"][value="fr"]')
414
+ self.assertIsNotNone(french_input)
415
+
416
+ # Check that the add URLs include the locale
417
+ add_url = f"{self.add_url}?locale=en"
418
+ add_buttons = soup.select(f'a[href="{add_url}"]')
419
+ self.assertEqual(len(add_buttons), 2)
420
+ self.assertContains(
421
+ response,
422
+ f"""<p>There are no translatable snippets to display.
423
+ Why not <a href="{add_url}">add one</a>?</p>""",
424
+ html=True,
425
+ )
426
+
427
+ def test_no_locale_filter_when_only_one_locale(self):
428
+ self.fr_locale.delete()
429
+ response = self.client.get(self.list_url)
430
+ soup = self.get_soup(response.content)
431
+
432
+ locale_input = soup.select_one('input[name="locale"]')
433
+ self.assertIsNone(locale_input)
434
+
435
+ # The viewset has no other filters configured,
436
+ # so the filters drilldown should not be present
437
+ filters_drilldown = soup.select_one("#filters-drilldown")
438
+ self.assertIsNone(filters_drilldown)
439
+
440
+ @override_settings(WAGTAIL_I18N_ENABLED=False)
441
+ def test_locale_selector_not_present_when_i18n_disabled(self):
442
+ response = self.client.get(self.list_url)
443
+ soup = self.get_soup(response.content)
444
+
445
+ input_element = soup.select_one('input[name="locale"]')
446
+ self.assertIsNone(input_element)
447
+
448
+ # Check that the add URLs don't include the locale
449
+ add_url = self.add_url
450
+ soup = self.get_soup(response.content)
451
+ add_buttons = soup.select(f'a[href="{add_url}"]')
452
+ self.assertEqual(len(add_buttons), 2)
453
+ self.assertContains(
454
+ response,
455
+ f"""<p>There are no translatable snippets to display.
456
+ Why not <a href="{add_url}">add one</a>?</p>""",
457
+ html=True,
458
+ )
459
+
460
+ def test_locale_selector_not_present_on_non_translatable_snippet(self):
461
+ response = self.client.get(reverse("wagtailsnippets_tests_advert:list"))
462
+ soup = self.get_soup(response.content)
463
+
464
+ input_element = soup.select_one('input[name="locale"]')
465
+ self.assertIsNone(input_element)
466
+
467
+ # Check that the add URLs don't include the locale
468
+ add_url = reverse("wagtailsnippets_tests_advert:add")
469
+ soup = self.get_soup(response.content)
470
+ add_buttons = soup.select(f'a[href="{add_url}"]')
471
+ self.assertEqual(len(add_buttons), 2)
472
+ self.assertContains(
473
+ response,
474
+ f"""<p>There are no adverts to display.
475
+ Why not <a href="{add_url}">add one</a>?</p>""",
476
+ html=True,
477
+ )
478
+
479
+ def test_locale_column(self):
480
+ self._add_snippets()
481
+ response = self.client.get(self.list_url)
482
+ soup = self.get_soup(response.content)
483
+ labels = soup.select("main table td .w-status--label")
484
+ self.assertEqual(len(labels), 2)
485
+ self.assertEqual(
486
+ sorted(label.text.strip() for label in labels),
487
+ ["English", "French"],
488
+ )
489
+
490
+ @override_settings(WAGTAIL_I18N_ENABLED=False)
491
+ def test_locale_column_not_present_with_i18n_disabled(self):
492
+ self._add_snippets()
493
+ response = self.client.get(self.list_url)
494
+ soup = self.get_soup(response.content)
495
+ labels = soup.select("main table td .w-status--label")
496
+ self.assertEqual(len(labels), 0)
497
+
498
+ def test_locale_column_not_present_for_non_translatable_snippet(self):
499
+ response = self.client.get(reverse("wagtailsnippets_tests_advert:list"))
500
+ Advert.objects.create(text="English text")
501
+ soup = self.get_soup(response.content)
502
+ labels = soup.select("main table td .w-status--label")
503
+ self.assertEqual(len(labels), 0)
504
+
505
+
506
+ class TestListViewOrdering(WagtailTestUtils, TestCase):
507
+ @classmethod
508
+ def setUpTestData(cls):
509
+ for i in range(1, 10):
510
+ advert = Advert.objects.create(text=f"{i * 'a'}dvert {i}")
511
+ draft = DraftStateModel.objects.create(
512
+ text=f"{i * 'd'}raft {i}", live=False
513
+ )
514
+ if i % 2 == 0:
515
+ ModelLogEntry.objects.create(
516
+ content_type=ContentType.objects.get_for_model(Advert),
517
+ label="Test Advert",
518
+ action="wagtail.create",
519
+ timestamp=now(),
520
+ object_id=advert.pk,
521
+ )
522
+ draft.save_revision().publish()
523
+
524
+ def setUp(self):
525
+ self.login()
526
+
527
+ def test_listing_orderable_columns_with_no_mixin(self):
528
+ list_url = reverse("wagtailsnippets_tests_advert:list")
529
+ response = self.client.get(list_url)
530
+ sort_updated_url = list_url + "?ordering=_updated_at"
531
+ sort_live_url = list_url + "?ordering=live"
532
+
533
+ self.assertEqual(response.status_code, 200)
534
+ # Should use the tables framework
535
+ self.assertTemplateUsed(response, "wagtailadmin/tables/table.html")
536
+ # The Updated column header should be a link with the correct query param
537
+ self.assertContains(
538
+ response,
539
+ f'<th><a href="{sort_updated_url}" title="Sort by &#x27;Updated&#x27; in ascending order." class="icon icon-arrow-down-after label">Updated</a></th>',
540
+ html=True,
541
+ )
542
+ # Should not contain the Status column header
543
+ self.assertNotContains(
544
+ response,
545
+ f'<th><a href="{sort_live_url}" title="Sort by &#x27;Status&#x27; in ascending order." class="icon icon-arrow-down-after label">Status</a></th>',
546
+ html=True,
547
+ )
548
+
549
+ def test_listing_orderable_columns_with_draft_state_mixin(self):
550
+ list_url = reverse("wagtailsnippets_tests_draftstatemodel:list")
551
+ response = self.client.get(list_url)
552
+ sort_updated_url = list_url + "?ordering=_updated_at"
553
+ sort_live_url = list_url + "?ordering=live"
554
+
555
+ self.assertEqual(response.status_code, 200)
556
+ # Should use the tables framework
557
+ self.assertTemplateUsed(response, "wagtailadmin/tables/table.html")
558
+ # The Updated column header should be a link with the correct query param
559
+ self.assertContains(
560
+ response,
561
+ f'<th><a href="{sort_updated_url}" title="Sort by &#x27;Updated&#x27; in ascending order." class="icon icon-arrow-down-after label">Updated</a></th>',
562
+ html=True,
563
+ )
564
+ # The Status column header should be a link with the correct query param
565
+ self.assertContains(
566
+ response,
567
+ f'<th><a href="{sort_live_url}" title="Sort by &#x27;Status&#x27; in ascending order." class="icon icon-arrow-down-after label">Status</a></th>',
568
+ html=True,
569
+ )
570
+
571
+ def test_order_by_updated_at_with_no_mixin(self):
572
+ list_url = reverse("wagtailsnippets_tests_advert:list")
573
+ response = self.client.get(list_url + "?ordering=_updated_at")
574
+
575
+ self.assertEqual(response.status_code, 200)
576
+
577
+ # With ascending order, empty updated_at information should be shown first
578
+ self.assertIsNone(response.context["page_obj"][0]._updated_at)
579
+ # The most recently updated should be at the bottom
580
+ self.assertEqual(response.context["page_obj"][-1].text, "aaaaaaaadvert 8")
581
+ self.assertIsNotNone(response.context["page_obj"][-1]._updated_at)
582
+
583
+ # Should contain a link to reverse the order
584
+ self.assertContains(response, list_url + "?ordering=-_updated_at")
585
+
586
+ response = self.client.get(list_url + "?ordering=-_updated_at")
587
+
588
+ self.assertEqual(response.status_code, 200)
589
+
590
+ # With descending order, the first object should be the one that was last updated
591
+ self.assertEqual(response.context["page_obj"][0].text, "aaaaaaaadvert 8")
592
+ self.assertIsNotNone(response.context["page_obj"][0]._updated_at)
593
+
594
+ # Should contain a link to reverse the order
595
+ self.assertContains(response, list_url + "?ordering=_updated_at")
596
+
597
+ def test_order_by_updated_at_with_draft_state_mixin(self):
598
+ list_url = reverse("wagtailsnippets_tests_draftstatemodel:list")
599
+ response = self.client.get(list_url + "?ordering=_updated_at")
600
+
601
+ self.assertEqual(response.status_code, 200)
602
+
603
+ # With ascending order, empty updated_at information should be shown first
604
+ self.assertIsNone(response.context["page_obj"][0]._updated_at)
605
+ # The most recently updated should be at the bottom
606
+ self.assertEqual(response.context["page_obj"][-1].text, "ddddddddraft 8")
607
+ self.assertIsNotNone(response.context["page_obj"][-1]._updated_at)
608
+
609
+ # Should contain a link to reverse the order
610
+ self.assertContains(response, list_url + "?ordering=-_updated_at")
611
+
612
+ response = self.client.get(list_url + "?ordering=-_updated_at")
613
+
614
+ self.assertEqual(response.status_code, 200)
615
+
616
+ # With descending order, the first object should be the one that was last updated
617
+ self.assertEqual(response.context["page_obj"][0].text, "ddddddddraft 8")
618
+ self.assertIsNotNone(response.context["page_obj"][0]._updated_at)
619
+
620
+ # Should contain a link to reverse the order
621
+ self.assertContains(response, list_url + "?ordering=_updated_at")
622
+
623
+ def test_order_by_live(self):
624
+ list_url = reverse("wagtailsnippets_tests_draftstatemodel:list")
625
+ response = self.client.get(list_url + "?ordering=live")
626
+
627
+ self.assertEqual(response.status_code, 200)
628
+
629
+ # With ascending order, live=False should be shown first
630
+ self.assertFalse(response.context["page_obj"][0].live)
631
+ # The last one should be live=True
632
+ self.assertTrue(response.context["page_obj"][-1].live)
633
+
634
+ # Should contain a link to reverse the order
635
+ self.assertContains(response, list_url + "?ordering=-live")
636
+
637
+ response = self.client.get(list_url + "?ordering=-live")
638
+
639
+ self.assertEqual(response.status_code, 200)
640
+
641
+ # With descending order, live=True should be shown first
642
+ self.assertTrue(response.context["page_obj"][0].live)
643
+ # The last one should be live=False
644
+ self.assertFalse(response.context["page_obj"][-1].live)
645
+
646
+ # Should contain a link to reverse the order
647
+ self.assertContains(response, list_url + "?ordering=live")
648
+
649
+
650
+ class TestSnippetListViewWithSearchableSnippet(WagtailTestUtils, TransactionTestCase):
651
+ def setUp(self):
652
+ self.login()
653
+
654
+ # Create some instances of the searchable snippet for testing
655
+ self.snippet_a = SearchableSnippet.objects.create(text="Hello")
656
+ self.snippet_b = SearchableSnippet.objects.create(text="World")
657
+ self.snippet_c = SearchableSnippet.objects.create(text="Hello World")
658
+
659
+ def get(self, params=None):
660
+ return self.client.get(
661
+ reverse("wagtailsnippets_snippetstests_searchablesnippet:list"),
662
+ params,
663
+ )
664
+
665
+ def test_simple(self):
666
+ response = self.get()
667
+ self.assertEqual(response.status_code, 200)
668
+ self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html")
669
+
670
+ # All snippets should be in items
671
+ items = list(response.context["page_obj"].object_list)
672
+ self.assertIn(self.snippet_a, items)
673
+ self.assertIn(self.snippet_b, items)
674
+ self.assertIn(self.snippet_c, items)
675
+
676
+ # The search box should not raise an error
677
+ self.assertNotContains(response, "This field is required.")
678
+
679
+ def test_empty_q(self):
680
+ response = self.get({"q": ""})
681
+ self.assertEqual(response.status_code, 200)
682
+ self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html")
683
+
684
+ # All snippets should be in items
685
+ items = list(response.context["page_obj"].object_list)
686
+ self.assertIn(self.snippet_a, items)
687
+ self.assertIn(self.snippet_b, items)
688
+ self.assertIn(self.snippet_c, items)
689
+
690
+ # The search box should not raise an error
691
+ self.assertNotContains(response, "This field is required.")
692
+
693
+ def test_is_searchable(self):
694
+ self.assertIsInstance(self.get().context["search_form"], SearchForm)
695
+
696
+ def test_search_hello(self):
697
+ response = self.get({"q": "Hello"})
698
+
699
+ # Just snippets with "Hello" should be in items
700
+ items = list(response.context["page_obj"].object_list)
701
+ self.assertIn(self.snippet_a, items)
702
+ self.assertNotIn(self.snippet_b, items)
703
+ self.assertIn(self.snippet_c, items)
704
+
705
+ def test_search_world_autocomplete(self):
706
+ response = self.get({"q": "wor"})
707
+
708
+ # Just snippets with "World" should be in items
709
+ items = list(response.context["page_obj"].object_list)
710
+ self.assertNotIn(self.snippet_a, items)
711
+ self.assertIn(self.snippet_b, items)
712
+ self.assertIn(self.snippet_c, items)
713
+
714
+
715
+ class TestSnippetListViewWithNonAutocompleteSearchableSnippet(
716
+ WagtailTestUtils, TransactionTestCase
717
+ ):
718
+ """
719
+ Test that searchable snippets with no AutocompleteFields defined can still be searched using
720
+ full words
721
+ """
722
+
723
+ def setUp(self):
724
+ self.login()
725
+
726
+ # Create some instances of the searchable snippet for testing
727
+ self.snippet_a = NonAutocompleteSearchableSnippet.objects.create(text="Hello")
728
+ self.snippet_b = NonAutocompleteSearchableSnippet.objects.create(text="World")
729
+ self.snippet_c = NonAutocompleteSearchableSnippet.objects.create(
730
+ text="Hello World"
731
+ )
732
+
733
+ def get(self, params=None):
734
+ return self.client.get(
735
+ reverse(
736
+ "wagtailsnippets_snippetstests_nonautocompletesearchablesnippet:list"
737
+ ),
738
+ params,
739
+ )
740
+
741
+ def test_search_hello(self):
742
+ with self.assertWarnsRegex(
743
+ RuntimeWarning, "does not specify any AutocompleteFields"
744
+ ):
745
+ response = self.get({"q": "Hello"})
746
+
747
+ # Just snippets with "Hello" should be in items
748
+ items = list(response.context["page_obj"].object_list)
749
+ self.assertIn(self.snippet_a, items)
750
+ self.assertNotIn(self.snippet_b, items)
751
+ self.assertIn(self.snippet_c, items)
752
+
753
+
754
+ class TestSnippetListViewWithCustomPrimaryKey(WagtailTestUtils, TestCase):
755
+ def setUp(self):
756
+ self.login()
757
+
758
+ # Create some instances of the searchable snippet for testing
759
+ self.snippet_a = StandardSnippetWithCustomPrimaryKey.objects.create(
760
+ snippet_id="snippet/01", text="Hello"
761
+ )
762
+ self.snippet_b = StandardSnippetWithCustomPrimaryKey.objects.create(
763
+ snippet_id="snippet/02", text="Hello"
764
+ )
765
+ self.snippet_c = StandardSnippetWithCustomPrimaryKey.objects.create(
766
+ snippet_id="snippet/03", text="Hello"
767
+ )
768
+
769
+ def get(self, params=None):
770
+ return self.client.get(
771
+ reverse(
772
+ "wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:list"
773
+ ),
774
+ params,
775
+ )
776
+
777
+ def test_simple(self):
778
+ response = self.get()
779
+ self.assertEqual(response.status_code, 200)
780
+ self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html")
781
+
782
+ # All snippets should be in items
783
+ items = list(response.context["page_obj"].object_list)
784
+ self.assertIn(self.snippet_a, items)
785
+ self.assertIn(self.snippet_b, items)
786
+ self.assertIn(self.snippet_c, items)