wagtail 6.1.3__py3-none-any.whl → 6.2rc1__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 (257) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/actions/copy_for_translation.py +15 -1
  3. wagtail/admin/checks.py +20 -30
  4. wagtail/admin/forms/pages.py +10 -0
  5. wagtail/admin/icons.py +43 -0
  6. wagtail/admin/locale/en/LC_MESSAGES/django.po +405 -295
  7. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +21 -3
  8. wagtail/admin/locale/sl/LC_MESSAGES/django.mo +0 -0
  9. wagtail/admin/locale/sl/LC_MESSAGES/django.po +30 -0
  10. wagtail/admin/menu.py +2 -2
  11. wagtail/admin/migrations/0004_editingsession.py +57 -0
  12. wagtail/admin/migrations/0005_editingsession_is_editing.py +18 -0
  13. wagtail/admin/models.py +36 -3
  14. wagtail/admin/rich_text/editors/draftail/__init__.py +2 -20
  15. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  16. wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
  17. wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
  18. wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
  19. wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
  20. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  21. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  22. wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
  23. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  24. wagtail/admin/static/wagtailadmin/js/expanding-formset.js +1 -1
  25. wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
  26. wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
  27. wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
  28. wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
  29. wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
  30. wagtail/admin/static/wagtailadmin/js/preview-panel.js +2 -1
  31. wagtail/admin/static/wagtailadmin/js/preview-panel.js.LICENSE.txt +11 -0
  32. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  33. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  34. wagtail/admin/static/wagtailadmin/js/task-chooser-modal.js +1 -1
  35. wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
  36. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  37. wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
  38. wagtail/admin/static/wagtailadmin/js/userbar.js +2 -1
  39. wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +11 -0
  40. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  41. wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +0 -12
  42. wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
  43. wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
  44. wagtail/admin/templates/wagtailadmin/collection_privacy/ancestor_privacy.html +2 -6
  45. wagtail/admin/templates/wagtailadmin/generic/index_results.html +1 -17
  46. wagtail/admin/templates/wagtailadmin/generic/listing_results.html +20 -1
  47. wagtail/admin/templates/wagtailadmin/home/workflow_objects_to_moderate.html +2 -11
  48. wagtail/admin/templates/wagtailadmin/page_privacy/ancestor_privacy.html +2 -6
  49. wagtail/admin/templates/wagtailadmin/page_privacy/no_privacy.html +2 -0
  50. wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -1
  51. wagtail/admin/templates/wagtailadmin/pages/action_menu/menu.html +1 -1
  52. wagtail/admin/templates/wagtailadmin/reports/aging_pages_results.html +54 -0
  53. wagtail/admin/templates/wagtailadmin/reports/base_page_report.html +1 -17
  54. wagtail/admin/templates/wagtailadmin/reports/base_page_report_results.html +10 -0
  55. wagtail/admin/templates/wagtailadmin/reports/base_report.html +1 -40
  56. wagtail/admin/templates/wagtailadmin/reports/base_report_results.html +1 -0
  57. wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_report.html +21 -27
  58. wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_types_usage.html +48 -54
  59. wagtail/admin/templates/wagtailadmin/reports/{locked_pages.html → locked_pages_results.html} +3 -3
  60. wagtail/admin/templates/wagtailadmin/reports/page_types_usage_results.html +10 -0
  61. wagtail/admin/templates/wagtailadmin/reports/site_history_results.html +53 -0
  62. wagtail/admin/templates/wagtailadmin/reports/workflow_results.html +74 -0
  63. wagtail/admin/templates/wagtailadmin/reports/workflow_tasks_results.html +56 -0
  64. wagtail/admin/templates/wagtailadmin/shared/_workflow_init.html +8 -44
  65. wagtail/admin/templates/wagtailadmin/shared/avatar.html +11 -1
  66. wagtail/admin/templates/wagtailadmin/shared/dialog/dialog.html +5 -4
  67. wagtail/admin/templates/wagtailadmin/shared/dropdown/dropdown_button.html +2 -1
  68. wagtail/admin/templates/wagtailadmin/shared/editing_sessions/list.html +132 -0
  69. wagtail/admin/templates/wagtailadmin/shared/editing_sessions/module.html +44 -0
  70. wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +7 -1
  71. wagtail/admin/templates/wagtailadmin/shared/page_status_tag_new.html +1 -1
  72. wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html +32 -16
  73. wagtail/admin/templates/wagtailadmin/skeleton.html +1 -1
  74. wagtail/admin/templates/wagtailadmin/userbar/item_accessibility.html +9 -11
  75. wagtail/admin/templatetags/wagtailadmin_tags.py +13 -2
  76. wagtail/admin/tests/formats/en/__init__.py +0 -0
  77. wagtail/admin/tests/formats/en/formats.py +1 -0
  78. wagtail/admin/tests/pages/test_create_page.py +47 -0
  79. wagtail/admin/tests/pages/test_edit_page.py +10 -8
  80. wagtail/admin/tests/pages/test_parent_page_chooser_view.py +45 -1
  81. wagtail/admin/tests/test_checks.py +53 -3
  82. wagtail/admin/tests/test_collections_views.py +62 -1
  83. wagtail/admin/tests/test_edit_handlers.py +37 -0
  84. wagtail/admin/tests/test_editing_sessions.py +1336 -0
  85. wagtail/admin/tests/test_icon_sprite.py +12 -21
  86. wagtail/admin/tests/test_page_chooser.py +309 -7
  87. wagtail/admin/tests/test_privacy.py +82 -0
  88. wagtail/admin/tests/test_reports_views.py +464 -70
  89. wagtail/admin/tests/test_userbar.py +93 -6
  90. wagtail/admin/tests/test_workflows.py +223 -33
  91. wagtail/admin/tests/viewsets/test_model_viewset.py +151 -2
  92. wagtail/admin/ui/editing_sessions.py +57 -0
  93. wagtail/admin/urls/__init__.py +9 -15
  94. wagtail/admin/urls/editing_sessions.py +17 -0
  95. wagtail/admin/urls/reports.py +33 -1
  96. wagtail/admin/userbar.py +77 -20
  97. wagtail/admin/views/chooser.py +49 -22
  98. wagtail/admin/views/collections.py +0 -11
  99. wagtail/admin/views/editing_sessions.py +193 -0
  100. wagtail/admin/views/generic/__init__.py +1 -0
  101. wagtail/admin/views/generic/history.py +9 -3
  102. wagtail/admin/views/generic/mixins.py +44 -3
  103. wagtail/admin/views/generic/models.py +46 -72
  104. wagtail/admin/views/generic/permissions.py +20 -10
  105. wagtail/admin/views/home.py +2 -31
  106. wagtail/admin/views/page_privacy.py +20 -5
  107. wagtail/admin/views/pages/choose_parent.py +62 -0
  108. wagtail/admin/views/pages/edit.py +28 -0
  109. wagtail/admin/views/reports/aging_pages.py +6 -10
  110. wagtail/admin/views/reports/audit_logging.py +13 -42
  111. wagtail/admin/views/reports/base.py +31 -4
  112. wagtail/admin/views/reports/locked_pages.py +5 -8
  113. wagtail/admin/views/reports/page_types_usage.py +6 -10
  114. wagtail/admin/views/reports/workflows.py +36 -12
  115. wagtail/admin/viewsets/base.py +8 -3
  116. wagtail/admin/viewsets/chooser.py +1 -1
  117. wagtail/admin/viewsets/model.py +26 -1
  118. wagtail/admin/wagtail_hooks.py +2 -1
  119. wagtail/api/v2/filters.py +6 -0
  120. wagtail/api/v2/tests/test_documents.py +1 -1
  121. wagtail/api/v2/tests/test_images.py +1 -1
  122. wagtail/api/v2/tests/test_pages.py +11 -1
  123. wagtail/api/v2/utils.py +2 -2
  124. wagtail/blocks/base.py +35 -12
  125. wagtail/blocks/definition_lookup.py +85 -0
  126. wagtail/blocks/list_block.py +12 -0
  127. wagtail/blocks/migrations/migrate_operation.py +2 -0
  128. wagtail/blocks/stream_block.py +19 -0
  129. wagtail/blocks/struct_block.py +19 -0
  130. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +1 -1
  131. wagtail/contrib/frontend_cache/backends/__init__.py +5 -0
  132. wagtail/contrib/frontend_cache/backends/azure.py +179 -0
  133. wagtail/contrib/frontend_cache/backends/base.py +28 -0
  134. wagtail/contrib/frontend_cache/backends/cloudflare.py +114 -0
  135. wagtail/contrib/frontend_cache/backends/cloudfront.py +99 -0
  136. wagtail/contrib/frontend_cache/backends/http.py +64 -0
  137. wagtail/contrib/frontend_cache/tests.py +59 -17
  138. wagtail/contrib/frontend_cache/utils.py +26 -8
  139. wagtail/contrib/redirects/filters.py +15 -1
  140. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +37 -72
  141. wagtail/contrib/redirects/models.py +6 -5
  142. wagtail/contrib/redirects/templates/wagtailredirects/edit.html +1 -38
  143. wagtail/contrib/redirects/tests/test_redirects.py +141 -1
  144. wagtail/contrib/redirects/urls.py +1 -2
  145. wagtail/contrib/redirects/views.py +39 -80
  146. wagtail/contrib/routable_page/models.py +6 -4
  147. wagtail/contrib/routable_page/tests.py +11 -0
  148. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +1 -1
  149. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +4 -4
  150. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +5 -1
  151. wagtail/contrib/simple_translation/models.py +2 -1
  152. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
  153. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  154. wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
  155. wagtail/contrib/typed_table_block/blocks.py +19 -0
  156. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
  157. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  158. wagtail/contrib/typed_table_block/tests.py +38 -0
  159. wagtail/coreutils.py +1 -1
  160. wagtail/documents/__init__.py +1 -1
  161. wagtail/documents/locale/en/LC_MESSAGES/django.po +8 -8
  162. wagtail/documents/locale/sl/LC_MESSAGES/django.mo +0 -0
  163. wagtail/documents/locale/sl/LC_MESSAGES/django.po +20 -0
  164. wagtail/documents/models.py +5 -1
  165. wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
  166. wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
  167. wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
  168. wagtail/documents/tests/test_models.py +5 -1
  169. wagtail/embeds/apps.py +2 -0
  170. wagtail/embeds/embeds.py +12 -14
  171. wagtail/embeds/finders/__init__.py +2 -0
  172. wagtail/embeds/finders/facebook.py +17 -33
  173. wagtail/embeds/finders/instagram.py +19 -16
  174. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  175. wagtail/embeds/signal_handlers.py +13 -0
  176. wagtail/embeds/tests/test_embeds.py +7 -7
  177. wagtail/fields.py +58 -14
  178. wagtail/images/__init__.py +1 -1
  179. wagtail/images/locale/en/LC_MESSAGES/django.po +34 -34
  180. wagtail/images/locale/sl/LC_MESSAGES/django.mo +0 -0
  181. wagtail/images/locale/sl/LC_MESSAGES/django.po +20 -0
  182. wagtail/images/models.py +2 -0
  183. wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
  184. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  185. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  186. wagtail/images/templates/wagtailimages/images/edit.html +4 -4
  187. wagtail/images/tests/test_admin_views.py +26 -2
  188. wagtail/images/views/chooser.py +6 -1
  189. wagtail/locale/en/LC_MESSAGES/django.po +84 -80
  190. wagtail/locales/locale/en/LC_MESSAGES/django.po +2 -2
  191. wagtail/locales/tests.py +16 -0
  192. wagtail/locales/wagtail_hooks.py +0 -9
  193. wagtail/migrations/0094_alter_page_locale.py +19 -0
  194. wagtail/models/__init__.py +11 -5
  195. wagtail/models/i18n.py +6 -1
  196. wagtail/project_template/requirements.txt +1 -1
  197. wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
  198. wagtail/signals.py +4 -0
  199. wagtail/sites/locale/en/LC_MESSAGES/django.po +2 -2
  200. wagtail/sites/tests.py +15 -0
  201. wagtail/sites/wagtail_hooks.py +0 -9
  202. wagtail/snippets/locale/en/LC_MESSAGES/django.po +9 -9
  203. wagtail/snippets/permissions.py +5 -3
  204. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
  205. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
  206. wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/menu.html +1 -1
  207. wagtail/snippets/tests/test_snippets.py +78 -12
  208. wagtail/snippets/tests/test_viewset.py +22 -0
  209. wagtail/snippets/views/snippets.py +19 -14
  210. wagtail/snippets/wagtail_hooks.py +2 -10
  211. wagtail/templatetags/wagtailcore_tags.py +3 -0
  212. wagtail/test/dummy_external_storage.py +1 -1
  213. wagtail/test/i18n/migrations/0003_alter_clusterabletestmodel_locale_and_more.py +40 -0
  214. wagtail/test/routablepage/models.py +4 -0
  215. wagtail/test/snippets/migrations/0012_alter_translatablesnippet_locale.py +20 -0
  216. wagtail/test/testapp/migrations/0038_sociallink.py +52 -0
  217. wagtail/test/testapp/migrations/0039_alter_eventcategory_locale_and_more.py +45 -0
  218. wagtail/test/testapp/models.py +24 -0
  219. wagtail/test/testapp/views.py +1 -0
  220. wagtail/test/testapp/wagtail_hooks.py +9 -0
  221. wagtail/test/urls_multilang.py +6 -1
  222. wagtail/test/urls_multilang_non_root.py +11 -0
  223. wagtail/tests/streamfield_migrations/test_migrations.py +53 -12
  224. wagtail/tests/test_audit_log.py +72 -2
  225. wagtail/tests/test_blocks.py +103 -0
  226. wagtail/tests/test_signals.py +49 -2
  227. wagtail/tests/test_streamfield.py +153 -0
  228. wagtail/tests/test_utils.py +14 -0
  229. wagtail/tests/tests.py +5 -0
  230. wagtail/users/apps.py +1 -0
  231. wagtail/users/forms.py +7 -0
  232. wagtail/users/locale/en/LC_MESSAGES/django.po +55 -50
  233. wagtail/users/models.py +1 -0
  234. wagtail/users/templates/wagtailusers/groups/includes/formatted_permissions.html +3 -2
  235. wagtail/users/templatetags/wagtailusers_tags.py +9 -0
  236. wagtail/users/tests/__init__.py +7 -1
  237. wagtail/users/tests/test_admin_views.py +117 -32
  238. wagtail/users/views/groups.py +4 -0
  239. wagtail/users/views/users.py +58 -14
  240. wagtail/users/wagtail_hooks.py +7 -123
  241. wagtail/utils/utils.py +1 -0
  242. wagtail/utils/version.py +5 -2
  243. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/METADATA +3 -3
  244. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/RECORD +248 -222
  245. wagtail/admin/templates/wagtailadmin/reports/aging_pages.html +0 -58
  246. wagtail/admin/templates/wagtailadmin/reports/page_types_usage.html +0 -18
  247. wagtail/admin/templates/wagtailadmin/reports/site_history.html +0 -57
  248. wagtail/admin/templates/wagtailadmin/reports/workflow.html +0 -81
  249. wagtail/admin/templates/wagtailadmin/reports/workflow_tasks.html +0 -63
  250. wagtail/contrib/frontend_cache/backends.py +0 -400
  251. wagtail/contrib/redirects/templates/wagtailredirects/list.html +0 -43
  252. wagtail/contrib/redirects/templates/wagtailredirects/reports/redirects_report.html +0 -14
  253. wagtail/contrib/redirects/tests/test_reports_view.py +0 -82
  254. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/LICENSE +0 -0
  255. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/WHEEL +0 -0
  256. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/entry_points.txt +0 -0
  257. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/top_level.txt +0 -0
@@ -21,6 +21,7 @@ from wagtail.admin.ui.tables import BooleanColumn, UpdatedAtColumn
21
21
  from wagtail.admin.utils import set_query_params
22
22
  from wagtail.admin.views.account import BaseSettingsPanel
23
23
  from wagtail.admin.widgets import Button
24
+ from wagtail.permission_policies.base import ModelPermissionPolicy
24
25
  from wagtail.snippets.bulk_actions.snippet_bulk_action import SnippetBulkAction
25
26
  from wagtail.snippets.models import register_snippet
26
27
  from wagtail.snippets.views.chooser import SnippetChooserViewSet
@@ -264,6 +265,13 @@ class FullFeaturedSnippetFilterSet(WagtailFilterSet):
264
265
  fields = ["country_code", "some_date"]
265
266
 
266
267
 
268
+ class FullFeaturedPermissionPolicy(ModelPermissionPolicy):
269
+ def user_has_permission(self, user, action):
270
+ if not user.is_anonymous and "[FORBIDDEN]" in user.get_full_name():
271
+ return False
272
+ return super().user_has_permission(user, action)
273
+
274
+
267
275
  class FullFeaturedSnippetChooserViewSet(SnippetChooserViewSet):
268
276
  form_fields = ["text", "country_code", "some_number"]
269
277
 
@@ -303,6 +311,7 @@ class FullFeaturedSnippetViewSet(SnippetViewSet):
303
311
  # Ensure that the menu item is placed last
304
312
  menu_order = 999999
305
313
  inspect_view_enabled = True
314
+ permission_policy = FullFeaturedPermissionPolicy(FullFeaturedSnippet)
306
315
 
307
316
  class IndexView(SnippetViewSet.index_view_class):
308
317
  def get_add_url(self):
@@ -2,5 +2,10 @@ from django.conf.urls.i18n import i18n_patterns
2
2
  from django.urls import include, path
3
3
 
4
4
  from wagtail import urls as wagtail_urls
5
+ from wagtail.admin import urls as wagtailadmin_urls
5
6
 
6
- urlpatterns = i18n_patterns(path("", include(wagtail_urls)))
7
+ urlpatterns = [
8
+ path("admin/", include(wagtailadmin_urls)),
9
+ ]
10
+
11
+ urlpatterns += i18n_patterns(path("", include(wagtail_urls)))
@@ -0,0 +1,11 @@
1
+ from django.conf.urls.i18n import i18n_patterns
2
+ from django.urls import include, path
3
+
4
+ from wagtail import urls as wagtail_urls
5
+ from wagtail.admin import urls as wagtailadmin_urls
6
+
7
+ urlpatterns = [
8
+ path("admin/", include(wagtailadmin_urls)),
9
+ ]
10
+
11
+ urlpatterns += i18n_patterns(path("site/", include(wagtail_urls)))
@@ -1,7 +1,8 @@
1
1
  import datetime
2
2
  import json
3
3
 
4
- from django.db.models import F, JSONField
4
+ from django.db import connection
5
+ from django.db.models import F, JSONField, TextField
5
6
  from django.db.models.functions import Cast
6
7
  from django.test import TestCase
7
8
  from django.utils import timezone
@@ -24,8 +25,8 @@ class BaseMigrationTest(TestCase, MigrationTestMixin):
24
25
  ]
25
26
  app_name = None
26
27
 
27
- def setUp(self):
28
- instances = [
28
+ def _get_test_instances(self):
29
+ return [
29
30
  self.factory(
30
31
  content__0__char1="Test char 1",
31
32
  content__1__char1="Test char 2",
@@ -44,6 +45,9 @@ class BaseMigrationTest(TestCase, MigrationTestMixin):
44
45
  ),
45
46
  ]
46
47
 
48
+ def setUp(self):
49
+ instances = self._get_test_instances()
50
+
47
51
  self.original_raw_data = {}
48
52
  self.original_revisions = {}
49
53
 
@@ -102,9 +106,7 @@ class BaseMigrationTest(TestCase, MigrationTestMixin):
102
106
 
103
107
  self.apply_migration()
104
108
 
105
- instances = self.model.objects.all().annotate(
106
- raw_content=Cast(F("content"), JSONField())
107
- )
109
+ instances = self.model.objects.all()
108
110
 
109
111
  for instance in instances:
110
112
  old_revisions = self.original_revisions[instance.id]
@@ -128,9 +130,7 @@ class BaseMigrationTest(TestCase, MigrationTestMixin):
128
130
  revisions_from = timezone.now() + datetime.timedelta(days=2)
129
131
  self.apply_migration(revisions_from=revisions_from)
130
132
 
131
- instances = self.model.objects.all().annotate(
132
- raw_content=Cast(F("content"), JSONField())
133
- )
133
+ instances = self.model.objects.all()
134
134
 
135
135
  for instance in instances:
136
136
  old_revisions = self.original_revisions[instance.id]
@@ -159,9 +159,7 @@ class BaseMigrationTest(TestCase, MigrationTestMixin):
159
159
  revisions_from = timezone.now() - datetime.timedelta(days=2)
160
160
  self.apply_migration(revisions_from=revisions_from)
161
161
 
162
- instances = self.model.objects.all().annotate(
163
- raw_content=Cast(F("content"), JSONField())
164
- )
162
+ instances = self.model.objects.all()
165
163
 
166
164
  for instance in instances:
167
165
  old_revisions = self.original_revisions[instance.id]
@@ -209,3 +207,46 @@ class TestPage(BaseMigrationTest):
209
207
 
210
208
  def test_migrate_revisions_from_date(self):
211
209
  self._test_migrate_revisions_from_date()
210
+
211
+
212
+ class TestNullStreamField(BaseMigrationTest):
213
+ """
214
+ Migrations are processed if the underlying JSON is null.
215
+
216
+ This might occur if we're operating on a StreamField that was added to a model that
217
+ had existing records.
218
+ """
219
+
220
+ model = models.SamplePage
221
+ factory = factories.SamplePageFactory
222
+ has_revisions = True
223
+ app_name = "streamfield_migration_tests"
224
+
225
+ def _get_test_instances(self):
226
+ return self.factory.create_batch(1, content=None)
227
+
228
+ def setUp(self):
229
+ super().setUp()
230
+
231
+ # Bypass StreamField/StreamBlock processing that cast a None stream field value
232
+ # to the empty StreamValue, and set the underlying JSON to null.
233
+ with connection.cursor() as cursor:
234
+ cursor.execute(f"UPDATE {self.model._meta.db_table} SET content = 'null'")
235
+
236
+ def assert_null_content(self):
237
+ """
238
+ The raw JSON of all instances for this test is null.
239
+ """
240
+
241
+ instances = self.model.objects.all().annotate(
242
+ raw_content=Cast(F("content"), TextField())
243
+ )
244
+
245
+ for instance in instances:
246
+ with self.subTest(instance=instance):
247
+ self.assertEqual(instance.raw_content, "null")
248
+
249
+ def test_migrate_stream_data(self):
250
+ self.assert_null_content()
251
+ self.apply_migration()
252
+ self.assert_null_content()
@@ -3,6 +3,7 @@ import json
3
3
 
4
4
  from django.conf import settings
5
5
  from django.contrib.auth import get_user_model
6
+ from django.contrib.contenttypes.models import ContentType
6
7
  from django.core.exceptions import ValidationError
7
8
  from django.core.serializers.json import DjangoJSONEncoder
8
9
  from django.test import TestCase
@@ -10,6 +11,7 @@ from django.utils import timezone
10
11
  from freezegun import freeze_time
11
12
 
12
13
  from wagtail.log_actions import LogActionRegistry
14
+ from wagtail.log_actions import registry as log_registry
13
15
  from wagtail.models import (
14
16
  Page,
15
17
  PageLogEntry,
@@ -36,8 +38,13 @@ class TestAuditLogManager(WagtailTestUtils, TestCase):
36
38
  title="Simple page", slug="simple", content="Hello", owner=self.user
37
39
  )
38
40
  )
41
+ self.snippet_1 = FullFeaturedSnippet.objects.create(text="snippet 1")
42
+ self.snippet_2 = FullFeaturedSnippet.objects.create(text="snippet 2")
43
+ self.snippet_content_type = ContentType.objects.get_for_model(
44
+ FullFeaturedSnippet
45
+ )
39
46
 
40
- def test_log_action(self):
47
+ def test_log_action_for_page(self):
41
48
  now = timezone.now()
42
49
 
43
50
  with freeze_time(now):
@@ -49,7 +56,19 @@ class TestAuditLogManager(WagtailTestUtils, TestCase):
49
56
  self.assertEqual(entry.user, self.user)
50
57
  self.assertEqual(entry.timestamp, now)
51
58
 
52
- def test_get_for_model(self):
59
+ def test_log_action_for_snippet(self):
60
+ now = timezone.now()
61
+
62
+ with freeze_time(now):
63
+ entry = ModelLogEntry.objects.log_action(
64
+ self.snippet_1, "wagtail.edit", user=self.user
65
+ )
66
+
67
+ self.assertEqual(entry.content_type, self.snippet_content_type)
68
+ self.assertEqual(entry.user, self.user)
69
+ self.assertEqual(entry.timestamp, now)
70
+
71
+ def test_get_for_page_model(self):
53
72
  PageLogEntry.objects.log_action(self.page, "wagtail.edit")
54
73
  PageLogEntry.objects.log_action(self.simple_page, "wagtail.edit")
55
74
 
@@ -59,11 +78,62 @@ class TestAuditLogManager(WagtailTestUtils, TestCase):
59
78
  list(entries), list(PageLogEntry.objects.filter(page=self.simple_page))
60
79
  )
61
80
 
81
+ def test_get_for_snippet_model(self):
82
+ ModelLogEntry.objects.log_action(self.snippet_1, "wagtail.edit")
83
+ ModelLogEntry.objects.log_action(self.snippet_2, "wagtail.edit")
84
+
85
+ entries = ModelLogEntry.objects.get_for_model(FullFeaturedSnippet)
86
+ self.assertEqual(entries.count(), 2)
87
+ self.assertListEqual(
88
+ list(entries),
89
+ list(ModelLogEntry.objects.filter(content_type=self.snippet_content_type)),
90
+ )
91
+
62
92
  def test_get_for_user(self):
63
93
  self.assertEqual(
64
94
  PageLogEntry.objects.get_for_user(self.user).count(), 1
65
95
  ) # the create from setUp
66
96
 
97
+ def test_get_for_page_instance(self):
98
+ PageLogEntry.objects.log_action(self.page, "wagtail.edit")
99
+ PageLogEntry.objects.log_action(self.simple_page, "wagtail.edit")
100
+ other_simple_page = self.page.add_child(
101
+ instance=SimplePage(
102
+ title="Simple page 2", slug="simple2", content="Hello", owner=self.user
103
+ )
104
+ )
105
+ PageLogEntry.objects.log_action(other_simple_page, "wagtail.edit")
106
+
107
+ entries = PageLogEntry.objects.for_instance(self.simple_page)
108
+ expected_entries = list(PageLogEntry.objects.filter(page=self.simple_page))
109
+ self.assertEqual(entries.count(), 2)
110
+ self.assertListEqual(list(entries), expected_entries)
111
+
112
+ # should also be able to retrieve entries via the log registry, which
113
+ # eliminates the need to know that PageLogEntry is the log entry model
114
+ entries = log_registry.get_logs_for_instance(self.simple_page)
115
+ self.assertEqual(entries.count(), 2)
116
+ self.assertListEqual(list(entries), expected_entries)
117
+
118
+ def test_get_for_snippet_instance(self):
119
+ ModelLogEntry.objects.log_action(self.snippet_1, "wagtail.edit")
120
+ ModelLogEntry.objects.log_action(self.snippet_2, "wagtail.edit")
121
+
122
+ entries = ModelLogEntry.objects.for_instance(self.snippet_1)
123
+ expected_entries = list(
124
+ ModelLogEntry.objects.filter(
125
+ content_type=self.snippet_content_type, object_id=self.snippet_1.pk
126
+ )
127
+ )
128
+ self.assertEqual(entries.count(), 1)
129
+ self.assertListEqual(list(entries), expected_entries)
130
+
131
+ # should also be able to retrieve entries via the log registry, which
132
+ # eliminates the need to know that ModelLogEntry is the log entry model
133
+ entries = log_registry.get_logs_for_instance(self.snippet_1)
134
+ self.assertEqual(entries.count(), 1)
135
+ self.assertListEqual(list(entries), expected_entries)
136
+
67
137
 
68
138
  class TestAuditLog(TestCase):
69
139
  def setUp(self):
@@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _
17
17
 
18
18
  from wagtail import blocks
19
19
  from wagtail.blocks.base import get_error_json_data
20
+ from wagtail.blocks.definition_lookup import BlockDefinitionLookup
20
21
  from wagtail.blocks.field_block import FieldBlockAdapter
21
22
  from wagtail.blocks.list_block import ListBlockAdapter, ListBlockValidationError
22
23
  from wagtail.blocks.static_block import StaticBlockAdapter
@@ -5882,3 +5883,105 @@ class TestValidationErrorAsJsonData(TestCase):
5882
5883
  ],
5883
5884
  },
5884
5885
  )
5886
+
5887
+
5888
+ class TestBlockDefinitionLookup(TestCase):
5889
+ def test_simple_lookup(self):
5890
+ lookup = BlockDefinitionLookup(
5891
+ {
5892
+ 0: ("wagtail.blocks.CharBlock", [], {"required": True}),
5893
+ 1: ("wagtail.blocks.RichTextBlock", [], {}),
5894
+ }
5895
+ )
5896
+ char_block = lookup.get_block(0)
5897
+ char_block.set_name("title")
5898
+ self.assertIsInstance(char_block, blocks.CharBlock)
5899
+ self.assertTrue(char_block.required)
5900
+
5901
+ rich_text_block = lookup.get_block(1)
5902
+ self.assertIsInstance(rich_text_block, blocks.RichTextBlock)
5903
+
5904
+ # A subsequent call to get_block with the same index should return a new instance;
5905
+ # this ensures that state changes such as set_name are independent of other blocks
5906
+ char_block_2 = lookup.get_block(0)
5907
+ char_block_2.set_name("subtitle")
5908
+ self.assertIsInstance(char_block, blocks.CharBlock)
5909
+ self.assertTrue(char_block.required)
5910
+ self.assertIsNot(char_block, char_block_2)
5911
+ self.assertEqual(char_block.name, "title")
5912
+ self.assertEqual(char_block_2.name, "subtitle")
5913
+
5914
+ def test_structblock_lookup(self):
5915
+ lookup = BlockDefinitionLookup(
5916
+ {
5917
+ 0: ("wagtail.blocks.CharBlock", [], {"required": True}),
5918
+ 1: ("wagtail.blocks.RichTextBlock", [], {}),
5919
+ 2: (
5920
+ "wagtail.blocks.StructBlock",
5921
+ [
5922
+ [
5923
+ ("title", 0),
5924
+ ("description", 1),
5925
+ ],
5926
+ ],
5927
+ {},
5928
+ ),
5929
+ }
5930
+ )
5931
+ struct_block = lookup.get_block(2)
5932
+ self.assertIsInstance(struct_block, blocks.StructBlock)
5933
+ title_block = struct_block.child_blocks["title"]
5934
+ self.assertIsInstance(title_block, blocks.CharBlock)
5935
+ self.assertTrue(title_block.required)
5936
+ description_block = struct_block.child_blocks["description"]
5937
+ self.assertIsInstance(description_block, blocks.RichTextBlock)
5938
+
5939
+ def test_streamblock_lookup(self):
5940
+ lookup = BlockDefinitionLookup(
5941
+ {
5942
+ 0: ("wagtail.blocks.CharBlock", [], {"required": True}),
5943
+ 1: ("wagtail.blocks.RichTextBlock", [], {}),
5944
+ 2: (
5945
+ "wagtail.blocks.StreamBlock",
5946
+ [
5947
+ [
5948
+ ("heading", 0),
5949
+ ("paragraph", 1),
5950
+ ],
5951
+ ],
5952
+ {},
5953
+ ),
5954
+ }
5955
+ )
5956
+ stream_block = lookup.get_block(2)
5957
+ self.assertIsInstance(stream_block, blocks.StreamBlock)
5958
+ title_block = stream_block.child_blocks["heading"]
5959
+ self.assertIsInstance(title_block, blocks.CharBlock)
5960
+ self.assertTrue(title_block.required)
5961
+ description_block = stream_block.child_blocks["paragraph"]
5962
+ self.assertIsInstance(description_block, blocks.RichTextBlock)
5963
+
5964
+ def test_listblock_lookup(self):
5965
+ lookup = BlockDefinitionLookup(
5966
+ {
5967
+ 0: ("wagtail.blocks.CharBlock", [], {"required": True}),
5968
+ 1: ("wagtail.blocks.ListBlock", [0], {}),
5969
+ }
5970
+ )
5971
+ list_block = lookup.get_block(1)
5972
+ self.assertIsInstance(list_block, blocks.ListBlock)
5973
+ list_item_block = list_block.child_block
5974
+ self.assertIsInstance(list_item_block, blocks.CharBlock)
5975
+ self.assertTrue(list_item_block.required)
5976
+
5977
+ # Passing a class as the child block is still valid; this is not converted
5978
+ # to a reference
5979
+ lookup = BlockDefinitionLookup(
5980
+ {
5981
+ 0: ("wagtail.blocks.ListBlock", [blocks.CharBlock], {}),
5982
+ }
5983
+ )
5984
+ list_block = lookup.get_block(0)
5985
+ self.assertIsInstance(list_block, blocks.ListBlock)
5986
+ list_item_block = list_block.child_block
5987
+ self.assertIsInstance(list_item_block, blocks.CharBlock)
@@ -1,9 +1,10 @@
1
1
  from unittest import mock
2
2
 
3
+ from django.conf import settings
3
4
  from django.test import TestCase
4
5
 
5
- from wagtail.models import Site
6
- from wagtail.signals import page_slug_changed
6
+ from wagtail.models import Locale, Site
7
+ from wagtail.signals import copy_for_translation_done, page_slug_changed
7
8
  from wagtail.test.testapp.models import SimplePage
8
9
  from wagtail.test.utils import WagtailTestUtils
9
10
 
@@ -97,3 +98,49 @@ class TestPageSlugChangedSignal(WagtailTestUtils, TestCase):
97
98
 
98
99
  # Check the signal was NOT fired
99
100
  self.assertEqual(handler.call_count, 0)
101
+
102
+
103
+ class TestCopyForTranslationDoneSignal(WagtailTestUtils, TestCase):
104
+ """
105
+ Tests for the `wagtail.signals.copy_for_translation_done` signal
106
+ """
107
+
108
+ def setUp(self):
109
+ # Find root page
110
+ site = Site.objects.select_related("root_page").get(is_default_site=True)
111
+ root_page = site.root_page
112
+
113
+ # Create a subpage
114
+ self.subpage = SimplePage(
115
+ title="Subpage in english", slug="subpage-in-english", content="hello"
116
+ )
117
+ root_page.add_child(instance=self.subpage)
118
+
119
+ # Get the languages and create locales
120
+ language_codes = dict(settings.LANGUAGES).keys()
121
+
122
+ for language_code in language_codes:
123
+ Locale.objects.get_or_create(language_code=language_code)
124
+
125
+ # Get the locales needed
126
+ self.locale = Locale.objects.get(language_code="en")
127
+ self.another_locale = Locale.objects.get(language_code="fr")
128
+
129
+ root_page.copy_for_translation(self.another_locale)
130
+
131
+ def test_signal_emitted_on_copy_for_translation_done(self):
132
+ # Connect a mock signal handler to the signal
133
+ handler = mock.MagicMock()
134
+ copy_for_translation_done.connect(handler)
135
+
136
+ page_to_translate = SimplePage.objects.get(id=self.subpage.id)
137
+
138
+ try:
139
+ with self.captureOnCommitCallbacks(execute=True):
140
+ page_to_translate.copy_for_translation(self.another_locale)
141
+ finally:
142
+ # Disconnect mock handler to prevent cross-test pollution
143
+ copy_for_translation_done.disconnect(handler)
144
+
145
+ # Check the signal was fired
146
+ self.assertEqual(handler.call_count, 1)
@@ -721,3 +721,156 @@ class TestGetBlockByContentPath(TestCase):
721
721
  self.assertEqual(bound_block.value, "Barnaby Rudge")
722
722
  bound_block = field.get_block_by_content_path(self.page.body, ["456", "999"])
723
723
  self.assertIsNone(bound_block)
724
+
725
+
726
+ class TestConstructStreamFieldFromLookup(TestCase):
727
+ def test_construct_block_list_from_lookup(self):
728
+ field = StreamField(
729
+ [
730
+ ("heading", 0),
731
+ ("paragraph", 1),
732
+ ("button", 3),
733
+ ],
734
+ block_lookup={
735
+ 0: ("wagtail.blocks.CharBlock", [], {"required": True}),
736
+ 1: ("wagtail.blocks.RichTextBlock", [], {}),
737
+ 2: ("wagtail.blocks.PageChooserBlock", [], {}),
738
+ 3: (
739
+ "wagtail.blocks.StructBlock",
740
+ [
741
+ [
742
+ ("page", 2),
743
+ ("link_text", 0),
744
+ ]
745
+ ],
746
+ {},
747
+ ),
748
+ },
749
+ )
750
+ stream_block = field.stream_block
751
+ self.assertIsInstance(stream_block, blocks.StreamBlock)
752
+ self.assertEqual(len(stream_block.child_blocks), 3)
753
+
754
+ heading_block = stream_block.child_blocks["heading"]
755
+ self.assertIsInstance(heading_block, blocks.CharBlock)
756
+ self.assertTrue(heading_block.required)
757
+ self.assertEqual(heading_block.name, "heading")
758
+
759
+ paragraph_block = stream_block.child_blocks["paragraph"]
760
+ self.assertIsInstance(paragraph_block, blocks.RichTextBlock)
761
+ self.assertEqual(paragraph_block.name, "paragraph")
762
+
763
+ button_block = stream_block.child_blocks["button"]
764
+ self.assertIsInstance(button_block, blocks.StructBlock)
765
+ self.assertEqual(button_block.name, "button")
766
+ self.assertEqual(len(button_block.child_blocks), 2)
767
+ page_block = button_block.child_blocks["page"]
768
+ self.assertIsInstance(page_block, blocks.PageChooserBlock)
769
+ link_text_block = button_block.child_blocks["link_text"]
770
+ self.assertIsInstance(link_text_block, blocks.CharBlock)
771
+ self.assertEqual(link_text_block.name, "link_text")
772
+
773
+ def test_construct_top_level_block_from_lookup(self):
774
+ field = StreamField(
775
+ 4,
776
+ block_lookup={
777
+ 0: ("wagtail.blocks.CharBlock", [], {"required": True}),
778
+ 1: ("wagtail.blocks.RichTextBlock", [], {}),
779
+ 2: ("wagtail.blocks.PageChooserBlock", [], {}),
780
+ 3: (
781
+ "wagtail.blocks.StructBlock",
782
+ [
783
+ [
784
+ ("page", 2),
785
+ ("link_text", 0),
786
+ ]
787
+ ],
788
+ {},
789
+ ),
790
+ 4: (
791
+ "wagtail.blocks.StreamBlock",
792
+ [
793
+ [
794
+ ("heading", 0),
795
+ ("paragraph", 1),
796
+ ("button", 3),
797
+ ]
798
+ ],
799
+ {},
800
+ ),
801
+ },
802
+ )
803
+ stream_block = field.stream_block
804
+ self.assertIsInstance(stream_block, blocks.StreamBlock)
805
+ self.assertEqual(len(stream_block.child_blocks), 3)
806
+
807
+ heading_block = stream_block.child_blocks["heading"]
808
+ self.assertIsInstance(heading_block, blocks.CharBlock)
809
+ self.assertTrue(heading_block.required)
810
+ self.assertEqual(heading_block.name, "heading")
811
+
812
+ paragraph_block = stream_block.child_blocks["paragraph"]
813
+ self.assertIsInstance(paragraph_block, blocks.RichTextBlock)
814
+ self.assertEqual(paragraph_block.name, "paragraph")
815
+
816
+ button_block = stream_block.child_blocks["button"]
817
+ self.assertIsInstance(button_block, blocks.StructBlock)
818
+ self.assertEqual(button_block.name, "button")
819
+ self.assertEqual(len(button_block.child_blocks), 2)
820
+ page_block = button_block.child_blocks["page"]
821
+ self.assertIsInstance(page_block, blocks.PageChooserBlock)
822
+ link_text_block = button_block.child_blocks["link_text"]
823
+ self.assertIsInstance(link_text_block, blocks.CharBlock)
824
+ self.assertEqual(link_text_block.name, "link_text")
825
+
826
+
827
+ class TestDeconstructStreamFieldWithLookup(TestCase):
828
+ def test_deconstruct(self):
829
+ class ButtonBlock(blocks.StructBlock):
830
+ page = blocks.PageChooserBlock()
831
+ link_text = blocks.CharBlock(required=True)
832
+
833
+ field = StreamField(
834
+ [
835
+ ("heading", blocks.CharBlock(required=True)),
836
+ ("paragraph", blocks.RichTextBlock()),
837
+ ("button", ButtonBlock()),
838
+ ],
839
+ blank=True,
840
+ )
841
+ field.set_attributes_from_name("body")
842
+
843
+ name, path, args, kwargs = field.deconstruct()
844
+ self.assertEqual(name, "body")
845
+ self.assertEqual(path, "wagtail.fields.StreamField")
846
+ self.assertEqual(
847
+ args,
848
+ [
849
+ [
850
+ ("heading", 0),
851
+ ("paragraph", 1),
852
+ ("button", 3),
853
+ ]
854
+ ],
855
+ )
856
+ self.assertEqual(
857
+ kwargs,
858
+ {
859
+ "blank": True,
860
+ "block_lookup": {
861
+ 0: ("wagtail.blocks.CharBlock", (), {"required": True}),
862
+ 1: ("wagtail.blocks.RichTextBlock", (), {}),
863
+ 2: ("wagtail.blocks.PageChooserBlock", (), {}),
864
+ 3: (
865
+ "wagtail.blocks.StructBlock",
866
+ [
867
+ [
868
+ ("page", 2),
869
+ ("link_text", 0),
870
+ ]
871
+ ],
872
+ {},
873
+ ),
874
+ },
875
+ },
876
+ )
@@ -29,6 +29,7 @@ from wagtail.coreutils import (
29
29
  from wagtail.models import Page, Site
30
30
  from wagtail.utils.file import hash_filelike
31
31
  from wagtail.utils.utils import deep_update, flatten_choices
32
+ from wagtail.utils.version import get_main_version
32
33
 
33
34
 
34
35
  class TestCamelCaseToUnderscore(TestCase):
@@ -577,6 +578,19 @@ class HashFileLikeTestCase(SimpleTestCase):
577
578
  )
578
579
 
579
580
 
581
+ class TestVersion(SimpleTestCase):
582
+ def test_get_main_version(self):
583
+ cases = [
584
+ ((6, 2, 0, "final", 0), False, "6.2"),
585
+ ((6, 2, 1, "final", 0), False, "6.2"),
586
+ ((6, 2, 0, "final", 0), True, "6.2"),
587
+ ((6, 2, 1, "final", 0), True, "6.2.1"),
588
+ ]
589
+ for version, include_patch, expected in cases:
590
+ with self.subTest(version=version, include_patch=include_patch):
591
+ self.assertEqual(get_main_version(version, include_patch), expected)
592
+
593
+
580
594
  class TestFlattenChoices(SimpleTestCase):
581
595
  def test_tuple_choices(self):
582
596
  choices = [(1, "1st"), (2, "2nd")]
wagtail/tests/tests.py CHANGED
@@ -9,6 +9,7 @@ from django.test import TestCase
9
9
  from django.test.utils import override_settings
10
10
  from django.urls.exceptions import NoReverseMatch
11
11
  from django.utils.safestring import SafeString
12
+ from django.utils.translation import gettext_lazy
12
13
 
13
14
  from wagtail.coreutils import (
14
15
  get_dummy_request,
@@ -536,6 +537,10 @@ class TestRichtextTag(TestCase):
536
537
  self.assertEqual(result, "Hello world!")
537
538
  self.assertIsInstance(result, SafeString)
538
539
 
540
+ def test_call_with_lazy(self):
541
+ result = richtext(gettext_lazy("test"))
542
+ self.assertEqual(result, "test")
543
+
539
544
  def test_call_with_none(self):
540
545
  result = richtext(None)
541
546
  self.assertEqual(result, "")
wagtail/users/apps.py CHANGED
@@ -8,3 +8,4 @@ class WagtailUsersAppConfig(AppConfig):
8
8
  verbose_name = _("Wagtail users")
9
9
  default_auto_field = "django.db.models.AutoField"
10
10
  group_viewset = "wagtail.users.views.groups.GroupViewSet"
11
+ user_viewset = "wagtail.users.views.users.UserViewSet"