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
wagtail/api/v2/filters.py CHANGED
@@ -46,6 +46,12 @@ class FieldsFilter(BaseFilterBackend):
46
46
  % (value, field_name, str(e))
47
47
  )
48
48
 
49
+ if "\x00" in str(value):
50
+ raise BadRequestError(
51
+ "field filter error. null characters are not allowed for %s"
52
+ % field_name
53
+ )
54
+
49
55
  if isinstance(field, TaggableManager):
50
56
  for tag in value.split(","):
51
57
  queryset = queryset.filter(**{field_name + "__name": tag})
@@ -590,7 +590,7 @@ class TestDocumentFind(TestCase):
590
590
  },
591
591
  WAGTAILAPI_BASE_URL="http://api.example.com",
592
592
  )
593
- @mock.patch("wagtail.contrib.frontend_cache.backends.HTTPBackend.purge")
593
+ @mock.patch("wagtail.contrib.frontend_cache.backends.http.HTTPBackend.purge")
594
594
  class TestDocumentCacheInvalidation(TestCase):
595
595
  fixtures = ["demosite.json"]
596
596
 
@@ -582,7 +582,7 @@ class TestImageFind(TestCase):
582
582
  },
583
583
  WAGTAILAPI_BASE_URL="http://api.example.com",
584
584
  )
585
- @mock.patch("wagtail.contrib.frontend_cache.backends.HTTPBackend.purge")
585
+ @mock.patch("wagtail.contrib.frontend_cache.backends.http.HTTPBackend.purge")
586
586
  class TestImageCacheInvalidation(TestCase):
587
587
  fixtures = ["demosite.json"]
588
588
 
@@ -683,6 +683,16 @@ class TestPageListing(WagtailTestUtils, TestCase):
683
683
  },
684
684
  )
685
685
 
686
+ def test_slug_field_containing_null_bytes_gives_error(self):
687
+ response = self.get_response(slug="\0")
688
+ content = json.loads(response.content.decode("UTF-8"))
689
+
690
+ self.assertEqual(response.status_code, 400)
691
+ self.assertEqual(
692
+ content,
693
+ {"message": "field filter error. null characters are not allowed for slug"},
694
+ )
695
+
686
696
  # CHILD OF FILTER
687
697
 
688
698
  def test_child_of_filter(self):
@@ -1848,7 +1858,7 @@ class TestPageDetailWithStreamField(TestCase):
1848
1858
  },
1849
1859
  WAGTAILAPI_BASE_URL="http://api.example.com",
1850
1860
  )
1851
- @mock.patch("wagtail.contrib.frontend_cache.backends.HTTPBackend.purge")
1861
+ @mock.patch("wagtail.contrib.frontend_cache.backends.http.HTTPBackend.purge")
1852
1862
  class TestPageCacheInvalidation(TestCase):
1853
1863
  fixtures = ["demosite.json"]
1854
1864
 
wagtail/api/v2/utils.py CHANGED
@@ -1,4 +1,4 @@
1
- from urllib.parse import urlparse
1
+ from urllib.parse import urlsplit
2
2
 
3
3
  from django.conf import settings
4
4
  from django.utils.encoding import force_str
@@ -21,7 +21,7 @@ def get_base_url(request=None):
21
21
 
22
22
  if base_url:
23
23
  # We only want the scheme and netloc
24
- base_url_parsed = urlparse(force_str(base_url))
24
+ base_url_parsed = urlsplit(force_str(base_url))
25
25
 
26
26
  return base_url_parsed.scheme + "://" + base_url_parsed.netloc
27
27
 
wagtail/blocks/base.py CHANGED
@@ -97,6 +97,17 @@ class Block(metaclass=BaseBlock):
97
97
 
98
98
  self.label = self.meta.label or ""
99
99
 
100
+ @classmethod
101
+ def construct_from_lookup(cls, lookup, *args, **kwargs):
102
+ """
103
+ See `wagtail.blocks.definition_lookup.BlockDefinitionLookup`.
104
+ Construct a block instance from the provided arguments, using the given BlockDefinitionLookup
105
+ object to perform any necessary lookups.
106
+ """
107
+ # In the base implementation, no lookups take place - args / kwargs are passed
108
+ # on to the constructor as-is
109
+ return cls(*args, **kwargs)
110
+
100
111
  def set_name(self, name):
101
112
  self.name = name
102
113
  if not self.meta.label:
@@ -376,7 +387,11 @@ class Block(metaclass=BaseBlock):
376
387
  """
377
388
  return False
378
389
 
379
- def deconstruct(self):
390
+ @cached_property
391
+ def canonical_module_path(self):
392
+ """
393
+ Return the module path string that should be used to refer to this block in migrations.
394
+ """
380
395
  # adapted from django.utils.deconstruct.deconstructible
381
396
  module_name = self.__module__
382
397
  name = self.__class__.__name__
@@ -394,16 +409,29 @@ class Block(metaclass=BaseBlock):
394
409
  # if the module defines a DECONSTRUCT_ALIASES dictionary, see if the class has an entry in there;
395
410
  # if so, use that instead of the real path
396
411
  try:
397
- path = module.DECONSTRUCT_ALIASES[self.__class__]
412
+ return module.DECONSTRUCT_ALIASES[self.__class__]
398
413
  except (AttributeError, KeyError):
399
- path = f"{module_name}.{name}"
414
+ return f"{module_name}.{name}"
400
415
 
416
+ def deconstruct(self):
401
417
  return (
402
- path,
418
+ self.canonical_module_path,
403
419
  self._constructor_args[0],
404
420
  self._constructor_args[1],
405
421
  )
406
422
 
423
+ def deconstruct_with_lookup(self, lookup):
424
+ """
425
+ Like `deconstruct`, but with a `wagtail.blocks.definition_lookup.BlockDefinitionLookupBuilder`
426
+ object available so that any block instances within the definition can be added to the lookup
427
+ table to obtain an ID (potentially shared with other matching block definitions, thus reducing
428
+ the overall definition size) to be used in place of the block. The resulting deconstructed form
429
+ returned here can then be restored into a block object using `Block.construct_from_lookup`.
430
+ """
431
+ # In the base implementation, no substitutions happen, so we ignore the lookup and just call
432
+ # deconstruct
433
+ return self.deconstruct()
434
+
407
435
  def __eq__(self, other):
408
436
  """
409
437
  Implement equality on block objects so that two blocks with matching definitions are considered
@@ -411,14 +439,9 @@ class Block(metaclass=BaseBlock):
411
439
  attributes identified in MUTABLE_META_ATTRIBUTES, so checking these along with the result of
412
440
  deconstruct (which captures the constructor arguments) is sufficient to identify (valid) differences.
413
441
 
414
- This was originally necessary as a workaround for https://code.djangoproject.com/ticket/24340
415
- in Django <1.9; the deep_deconstruct function used to detect changes for migrations did not
416
- recurse into the block lists, and left them as Block instances. This __eq__ method therefore
417
- came into play when identifying changes within migrations.
418
-
419
- As of Django >=1.9, this *probably* isn't required any more. However, it may be useful in
420
- future as a way of identifying blocks that can be re-used within StreamField definitions
421
- (https://github.com/wagtail/wagtail/issues/4298#issuecomment-367656028).
442
+ This was implemented as a workaround for a Django <1.9 bug and is quite possibly not used by Wagtail
443
+ any more, but has been retained as it provides a sensible definition of equality (and there's no
444
+ reason to break it).
422
445
  """
423
446
 
424
447
  if not isinstance(other, Block):
@@ -0,0 +1,85 @@
1
+ from collections import defaultdict
2
+ from importlib import import_module
3
+
4
+
5
+ class BlockDefinitionLookup:
6
+ """
7
+ A utility for constructing StreamField Block objects in migrations, starting from
8
+ a compact representation that avoids repeating the same definition whenever a
9
+ block is re-used in multiple places over the block definition tree.
10
+
11
+ The underlying data is a dict of block definitions, such as:
12
+ ```
13
+ {
14
+ 0: ("wagtail.blocks.CharBlock", [], {"required": True}),
15
+ 1: ("wagtail.blocks.RichTextBlock", [], {}),
16
+ 2: ("wagtail.blocks.StreamBlock", [
17
+ [
18
+ ("heading", 0),
19
+ ("paragraph", 1),
20
+ ],
21
+ ], {}),
22
+ }
23
+ ```
24
+
25
+ where each definition is a tuple of (module_path, args, kwargs) similar to that
26
+ returned by `deconstruct` - with the difference that any block objects appearing
27
+ in args / kwargs may be substituted with an index into the lookup table that
28
+ points to that block's definition. Any block class that wants to support such
29
+ substitutions should implement a static/class method
30
+ `construct_from_lookup(lookup, *args, **kwargs)`, where `lookup` is
31
+ the `BlockDefinitionLookup` instance. The method should return a block instance
32
+ constructed from the provided arguments (after performing any lookups).
33
+ """
34
+
35
+ def __init__(self, blocks):
36
+ self.blocks = blocks
37
+ self.block_classes = {}
38
+
39
+ def get_block(self, index):
40
+ path, args, kwargs = self.blocks[index]
41
+ try:
42
+ cls = self.block_classes[path]
43
+ except KeyError:
44
+ module_name, class_name = path.rsplit(".", 1)
45
+ module = import_module(module_name)
46
+ cls = self.block_classes[path] = getattr(module, class_name)
47
+
48
+ return cls.construct_from_lookup(self, *args, **kwargs)
49
+
50
+
51
+ class BlockDefinitionLookupBuilder:
52
+ """
53
+ Helper for constructing the lookup data used by BlockDefinitionLookup
54
+ """
55
+
56
+ def __init__(self):
57
+ self.blocks = []
58
+
59
+ # Lookup table mapping the deconstructed tuple forms of blocks (as obtained from
60
+ # `block.deconstruct_with_lookup`) to their index in the `blocks` list. These
61
+ # tuples can be compared for equality, but not hashed, so we cannot use them as
62
+ # dict keys; instead, we index them on the first tuple element (the module path)
63
+ # and maintain a list of (index, deconstructed_tuple) pairs for each one.
64
+ self.block_indexes_by_type = defaultdict(list)
65
+
66
+ def add_block(self, block):
67
+ """
68
+ Add a block to the lookup table, returning an index that can be used to refer to it
69
+ """
70
+ deconstructed = block.deconstruct_with_lookup(self)
71
+
72
+ # Check if we've already seen this block definition
73
+ block_indexes = self.block_indexes_by_type[deconstructed[0]]
74
+ for index, existing_deconstructed in block_indexes:
75
+ if existing_deconstructed == deconstructed:
76
+ return index
77
+
78
+ # If not, add it to the lookup table
79
+ index = len(self.blocks)
80
+ self.blocks.append(deconstructed)
81
+ block_indexes.append((index, deconstructed))
82
+ return index
83
+
84
+ def get_lookup_as_dict(self):
85
+ return dict(enumerate(self.blocks))
@@ -151,6 +151,12 @@ class ListBlock(Block):
151
151
  # Default to a list consisting of one empty (i.e. default-valued) child item
152
152
  self.meta.default = [self.child_block.get_default()]
153
153
 
154
+ @classmethod
155
+ def construct_from_lookup(cls, lookup, child_block, **kwargs):
156
+ if isinstance(child_block, int):
157
+ child_block = lookup.get_block(child_block)
158
+ return cls(child_block, **kwargs)
159
+
154
160
  def value_from_datadict(self, data, files, prefix):
155
161
  count = int(data["%s-count" % prefix])
156
162
  child_blocks_with_indexes = []
@@ -396,6 +402,12 @@ class ListBlock(Block):
396
402
  errors.extend(self.child_block.check(**kwargs))
397
403
  return errors
398
404
 
405
+ def deconstruct_with_lookup(self, lookup):
406
+ path, args, kwargs = super().deconstruct_with_lookup(lookup)
407
+ if isinstance(args[0], Block):
408
+ args = (lookup.add_block(args[0]),)
409
+ return path, args, kwargs
410
+
399
411
  class Meta:
400
412
  # No icon specified here, because that depends on the purpose that the
401
413
  # block is being used for. Feel encouraged to specify an icon in your
@@ -110,6 +110,8 @@ class MigrateStreamData(RunPython):
110
110
 
111
111
  updated_model_instances_buffer = []
112
112
  for instance in model_queryset.iterator(chunk_size=self.chunk_size):
113
+ if instance.raw_content is None:
114
+ continue
113
115
 
114
116
  revision_query_maker.append_instance_data_for_revision_query(instance)
115
117
 
@@ -89,6 +89,14 @@ class BaseStreamBlock(Block):
89
89
  block.set_name(name)
90
90
  self.child_blocks[name] = block
91
91
 
92
+ @classmethod
93
+ def construct_from_lookup(cls, lookup, child_blocks, **kwargs):
94
+ if child_blocks:
95
+ child_blocks = [
96
+ (name, lookup.get_block(index)) for name, index in child_blocks
97
+ ]
98
+ return cls(child_blocks, **kwargs)
99
+
92
100
  def empty_value(self, raw_text=None):
93
101
  return StreamValue(self, [], raw_text=raw_text)
94
102
 
@@ -433,6 +441,17 @@ class BaseStreamBlock(Block):
433
441
  kwargs = self._constructor_kwargs
434
442
  return (path, args, kwargs)
435
443
 
444
+ def deconstruct_with_lookup(self, lookup):
445
+ path = "wagtail.blocks.StreamBlock"
446
+ args = [
447
+ [
448
+ (name, lookup.add_block(block))
449
+ for name, block in self.child_blocks.items()
450
+ ]
451
+ ]
452
+ kwargs = self._constructor_kwargs
453
+ return (path, args, kwargs)
454
+
436
455
  def check(self, **kwargs):
437
456
  errors = super().check(**kwargs)
438
457
  for name, child_block in self.child_blocks.items():
@@ -119,6 +119,14 @@ class BaseStructBlock(Block):
119
119
  block.set_name(name)
120
120
  self.child_blocks[name] = block
121
121
 
122
+ @classmethod
123
+ def construct_from_lookup(cls, lookup, child_blocks, **kwargs):
124
+ if child_blocks:
125
+ child_blocks = [
126
+ (name, lookup.get_block(index)) for name, index in child_blocks
127
+ ]
128
+ return cls(child_blocks, **kwargs)
129
+
122
130
  def get_default(self):
123
131
  """
124
132
  Any default value passed in the constructor or self.meta is going to be a dict
@@ -312,6 +320,17 @@ class BaseStructBlock(Block):
312
320
  kwargs = self._constructor_kwargs
313
321
  return (path, args, kwargs)
314
322
 
323
+ def deconstruct_with_lookup(self, lookup):
324
+ path = "wagtail.blocks.StructBlock"
325
+ args = [
326
+ [
327
+ (name, lookup.add_block(block))
328
+ for name, block in self.child_blocks.items()
329
+ ]
330
+ ]
331
+ kwargs = self._constructor_kwargs
332
+ return (path, args, kwargs)
333
+
315
334
  def check(self, **kwargs):
316
335
  errors = super().check(**kwargs)
317
336
  for name, child_block in self.child_blocks.items():
@@ -8,7 +8,7 @@ msgid ""
8
8
  msgstr ""
9
9
  "Project-Id-Version: PACKAGE VERSION\n"
10
10
  "Report-Msgid-Bugs-To: \n"
11
- "POT-Creation-Date: 2024-04-18 17:28+0100\n"
11
+ "POT-Creation-Date: 2024-07-19 16:26+0100\n"
12
12
  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
13
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
14
  "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -0,0 +1,5 @@
1
+ from .base import * # noqa
2
+ from .azure import * # noqa
3
+ from .http import * # noqa
4
+ from .cloudflare import * # noqa
5
+ from .cloudfront import * # noqa
@@ -0,0 +1,179 @@
1
+ import logging
2
+ from urllib.parse import urlsplit, urlunsplit
3
+
4
+ from django.core.exceptions import ImproperlyConfigured
5
+
6
+ from .base import BaseBackend
7
+
8
+ logger = logging.getLogger("wagtail.frontendcache")
9
+
10
+
11
+ __all__ = ["AzureBaseBackend", "AzureFrontDoorBackend", "AzureCdnBackend"]
12
+
13
+
14
+ class AzureBaseBackend(BaseBackend):
15
+ def __init__(self, params):
16
+ super().__init__(params)
17
+ self._credentials = params.pop("CREDENTIALS", None)
18
+ self._subscription_id = params.pop("SUBSCRIPTION_ID", None)
19
+ try:
20
+ self._resource_group_name = params.pop("RESOURCE_GROUP_NAME")
21
+ except KeyError:
22
+ raise ImproperlyConfigured(
23
+ "The setting 'WAGTAILFRONTENDCACHE' requires 'RESOURCE_GROUP_NAME' to be specified."
24
+ )
25
+ self._custom_headers = params.pop("CUSTOM_HEADERS", None)
26
+
27
+ def purge_batch(self, urls):
28
+ self._purge_content([self._get_path(url) for url in urls])
29
+
30
+ def purge(self, url):
31
+ self.purge_batch([url])
32
+
33
+ def _get_default_credentials(self):
34
+ try:
35
+ from azure.identity import DefaultAzureCredential
36
+ except ImportError:
37
+ return
38
+ return DefaultAzureCredential()
39
+
40
+ def _get_credentials(self):
41
+ """
42
+ Use credentials object set by user. If not set, use the one configured
43
+ in the current environment.
44
+ """
45
+ user_credentials = self._credentials
46
+ if user_credentials:
47
+ return user_credentials
48
+ return self._get_default_credentials()
49
+
50
+ def _get_default_subscription_id(self):
51
+ """
52
+ Obtain subscription ID directly from Azure.
53
+ """
54
+ try:
55
+ from azure.mgmt.resource import SubscriptionClient
56
+ except ImportError:
57
+ return ""
58
+ credential = self._get_credentials()
59
+ subscription_client = SubscriptionClient(credential)
60
+ subscription = next(subscription_client.subscriptions.list())
61
+ return subscription.subscription_id
62
+
63
+ def _get_subscription_id(self):
64
+ """
65
+ Use subscription ID set in the user configuration. If not set, try to
66
+ retrieve one from Azure directly.
67
+ """
68
+ user_subscription_id = self._subscription_id
69
+ if user_subscription_id:
70
+ return user_subscription_id
71
+ return self._get_default_subscription_id()
72
+
73
+ def _get_client_kwargs(self):
74
+ return {
75
+ "credential": self._get_credentials(),
76
+ "subscription_id": self._get_subscription_id(),
77
+ }
78
+
79
+ def _get_path(self, url):
80
+ """
81
+ Split netloc from the URL and return path only.
82
+ """
83
+ # Delete scheme and netloc from the URL, that will result in only path being
84
+ # left.
85
+ url_parts = ("",) * 2 + urlsplit(url)[2:]
86
+ return urlunsplit(url_parts)
87
+
88
+ def _get_client(self):
89
+ """
90
+ Get Azure client instance.
91
+ """
92
+ klass = self._get_client_class()
93
+ kwargs = self._get_client_kwargs()
94
+ return klass(**kwargs)
95
+
96
+ def _get_purge_kwargs(self, paths):
97
+ """
98
+ Get keyword arguments passes to Azure purge content calls.
99
+ """
100
+ return {
101
+ "resource_group_name": self._resource_group_name,
102
+ "custom_headers": self._custom_headers,
103
+ "content_paths": paths,
104
+ }
105
+
106
+ def _purge_content(self, paths):
107
+ from msrest.exceptions import HttpOperationError
108
+
109
+ client = self._get_client()
110
+ try:
111
+ self._make_purge_call(client, paths)
112
+ except HttpOperationError as exception:
113
+ for path in paths:
114
+ logger.exception(
115
+ "Couldn't purge '%s' from %s cache. HttpOperationError: %r",
116
+ path,
117
+ type(self).__name__,
118
+ exception.response,
119
+ )
120
+
121
+
122
+ class AzureFrontDoorBackend(AzureBaseBackend):
123
+ def __init__(self, params):
124
+ super().__init__(params)
125
+ try:
126
+ self._front_door_name = params.pop("FRONT_DOOR_NAME")
127
+ except KeyError:
128
+ raise ImproperlyConfigured(
129
+ "The setting 'WAGTAILFRONTENDCACHE' requires 'FRONT_DOOR_NAME' to be specified."
130
+ )
131
+ self._front_door_service_url = params.pop("FRONT_DOOR_SERVICE_URL", None)
132
+
133
+ def _get_client_class(self):
134
+ from azure.mgmt.frontdoor import FrontDoorManagementClient
135
+
136
+ return FrontDoorManagementClient
137
+
138
+ def _get_client_kwargs(self):
139
+ kwargs = super()._get_client_kwargs()
140
+ kwargs.setdefault("base_url", self._front_door_service_url)
141
+
142
+ return kwargs
143
+
144
+ def _make_purge_call(self, client, paths):
145
+ return client.endpoints.purge_content(
146
+ **self._get_purge_kwargs(paths),
147
+ front_door_name=self._front_door_name,
148
+ )
149
+
150
+
151
+ class AzureCdnBackend(AzureBaseBackend):
152
+ def __init__(self, params):
153
+ super().__init__(params)
154
+ try:
155
+ self._cdn_profile_name = params.pop("CDN_PROFILE_NAME")
156
+ self._cdn_endpoint_name = params.pop("CDN_ENDPOINT_NAME")
157
+ except KeyError:
158
+ raise ImproperlyConfigured(
159
+ "The setting 'WAGTAILFRONTENDCACHE' requires 'CDN_PROFILE_NAME' and 'CDN_ENDPOINT_NAME' to be specified."
160
+ )
161
+ self._cdn_service_url = params.pop("CDN_SERVICE_URL", None)
162
+
163
+ def _get_client_class(self):
164
+ from azure.mgmt.cdn import CdnManagementClient
165
+
166
+ return CdnManagementClient
167
+
168
+ def _get_client_kwargs(self):
169
+ kwargs = super()._get_client_kwargs()
170
+ kwargs.setdefault("base_url", self._cdn_service_url)
171
+
172
+ return kwargs
173
+
174
+ def _make_purge_call(self, client, paths):
175
+ return client.endpoints.purge_content(
176
+ **self._get_purge_kwargs(paths),
177
+ profile_name=self._cdn_profile_name,
178
+ endpoint_name=self._cdn_endpoint_name,
179
+ )
@@ -0,0 +1,28 @@
1
+ import logging
2
+
3
+ from django.http.request import validate_host
4
+
5
+ logger = logging.getLogger("wagtail.frontendcache")
6
+
7
+
8
+ __all__ = ["BaseBackend"]
9
+
10
+
11
+ class BaseBackend:
12
+ def __init__(self, params):
13
+ # If unspecified, invalidate all hosts
14
+ self.hostnames = params.get("HOSTNAMES", ["*"])
15
+
16
+ def purge(self, url) -> None:
17
+ raise NotImplementedError
18
+
19
+ def purge_batch(self, urls) -> None:
20
+ # Fallback for backends that do not support batch purging
21
+ for url in urls:
22
+ self.purge(url)
23
+
24
+ def invalidates_hostname(self, hostname) -> bool:
25
+ """
26
+ Can `hostname` be invalidated by this backend?
27
+ """
28
+ return validate_host(hostname, self.hostnames)