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.
- wagtail/__init__.py +1 -1
- wagtail/actions/copy_for_translation.py +15 -1
- wagtail/admin/checks.py +20 -30
- wagtail/admin/forms/pages.py +10 -0
- wagtail/admin/icons.py +43 -0
- wagtail/admin/locale/en/LC_MESSAGES/django.po +405 -295
- wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +21 -3
- wagtail/admin/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/sl/LC_MESSAGES/django.po +30 -0
- wagtail/admin/menu.py +2 -2
- wagtail/admin/migrations/0004_editingsession.py +57 -0
- wagtail/admin/migrations/0005_editingsession_is_editing.py +18 -0
- wagtail/admin/models.py +36 -3
- wagtail/admin/rich_text/editors/draftail/__init__.py +2 -20
- wagtail/admin/static/wagtailadmin/css/core.css +1 -1
- wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
- wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
- wagtail/admin/static/wagtailadmin/js/core.js +1 -1
- wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
- wagtail/admin/static/wagtailadmin/js/expanding-formset.js +1 -1
- wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
- wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/preview-panel.js +2 -1
- wagtail/admin/static/wagtailadmin/js/preview-panel.js.LICENSE.txt +11 -0
- wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
- wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
- wagtail/admin/static/wagtailadmin/js/task-chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
- wagtail/admin/static/wagtailadmin/js/userbar.js +2 -1
- wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +11 -0
- wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +0 -12
- wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
- wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
- wagtail/admin/templates/wagtailadmin/collection_privacy/ancestor_privacy.html +2 -6
- wagtail/admin/templates/wagtailadmin/generic/index_results.html +1 -17
- wagtail/admin/templates/wagtailadmin/generic/listing_results.html +20 -1
- wagtail/admin/templates/wagtailadmin/home/workflow_objects_to_moderate.html +2 -11
- wagtail/admin/templates/wagtailadmin/page_privacy/ancestor_privacy.html +2 -6
- wagtail/admin/templates/wagtailadmin/page_privacy/no_privacy.html +2 -0
- wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -1
- wagtail/admin/templates/wagtailadmin/pages/action_menu/menu.html +1 -1
- wagtail/admin/templates/wagtailadmin/reports/aging_pages_results.html +54 -0
- wagtail/admin/templates/wagtailadmin/reports/base_page_report.html +1 -17
- wagtail/admin/templates/wagtailadmin/reports/base_page_report_results.html +10 -0
- wagtail/admin/templates/wagtailadmin/reports/base_report.html +1 -40
- wagtail/admin/templates/wagtailadmin/reports/base_report_results.html +1 -0
- wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_report.html +21 -27
- wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_types_usage.html +48 -54
- wagtail/admin/templates/wagtailadmin/reports/{locked_pages.html → locked_pages_results.html} +3 -3
- wagtail/admin/templates/wagtailadmin/reports/page_types_usage_results.html +10 -0
- wagtail/admin/templates/wagtailadmin/reports/site_history_results.html +53 -0
- wagtail/admin/templates/wagtailadmin/reports/workflow_results.html +74 -0
- wagtail/admin/templates/wagtailadmin/reports/workflow_tasks_results.html +56 -0
- wagtail/admin/templates/wagtailadmin/shared/_workflow_init.html +8 -44
- wagtail/admin/templates/wagtailadmin/shared/avatar.html +11 -1
- wagtail/admin/templates/wagtailadmin/shared/dialog/dialog.html +5 -4
- wagtail/admin/templates/wagtailadmin/shared/dropdown/dropdown_button.html +2 -1
- wagtail/admin/templates/wagtailadmin/shared/editing_sessions/list.html +132 -0
- wagtail/admin/templates/wagtailadmin/shared/editing_sessions/module.html +44 -0
- wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +7 -1
- wagtail/admin/templates/wagtailadmin/shared/page_status_tag_new.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html +32 -16
- wagtail/admin/templates/wagtailadmin/skeleton.html +1 -1
- wagtail/admin/templates/wagtailadmin/userbar/item_accessibility.html +9 -11
- wagtail/admin/templatetags/wagtailadmin_tags.py +13 -2
- wagtail/admin/tests/formats/en/__init__.py +0 -0
- wagtail/admin/tests/formats/en/formats.py +1 -0
- wagtail/admin/tests/pages/test_create_page.py +47 -0
- wagtail/admin/tests/pages/test_edit_page.py +10 -8
- wagtail/admin/tests/pages/test_parent_page_chooser_view.py +45 -1
- wagtail/admin/tests/test_checks.py +53 -3
- wagtail/admin/tests/test_collections_views.py +62 -1
- wagtail/admin/tests/test_edit_handlers.py +37 -0
- wagtail/admin/tests/test_editing_sessions.py +1336 -0
- wagtail/admin/tests/test_icon_sprite.py +12 -21
- wagtail/admin/tests/test_page_chooser.py +309 -7
- wagtail/admin/tests/test_privacy.py +82 -0
- wagtail/admin/tests/test_reports_views.py +464 -70
- wagtail/admin/tests/test_userbar.py +93 -6
- wagtail/admin/tests/test_workflows.py +223 -33
- wagtail/admin/tests/viewsets/test_model_viewset.py +151 -2
- wagtail/admin/ui/editing_sessions.py +57 -0
- wagtail/admin/urls/__init__.py +9 -15
- wagtail/admin/urls/editing_sessions.py +17 -0
- wagtail/admin/urls/reports.py +33 -1
- wagtail/admin/userbar.py +77 -20
- wagtail/admin/views/chooser.py +49 -22
- wagtail/admin/views/collections.py +0 -11
- wagtail/admin/views/editing_sessions.py +193 -0
- wagtail/admin/views/generic/__init__.py +1 -0
- wagtail/admin/views/generic/history.py +9 -3
- wagtail/admin/views/generic/mixins.py +44 -3
- wagtail/admin/views/generic/models.py +46 -72
- wagtail/admin/views/generic/permissions.py +20 -10
- wagtail/admin/views/home.py +2 -31
- wagtail/admin/views/page_privacy.py +20 -5
- wagtail/admin/views/pages/choose_parent.py +62 -0
- wagtail/admin/views/pages/edit.py +28 -0
- wagtail/admin/views/reports/aging_pages.py +6 -10
- wagtail/admin/views/reports/audit_logging.py +13 -42
- wagtail/admin/views/reports/base.py +31 -4
- wagtail/admin/views/reports/locked_pages.py +5 -8
- wagtail/admin/views/reports/page_types_usage.py +6 -10
- wagtail/admin/views/reports/workflows.py +36 -12
- wagtail/admin/viewsets/base.py +8 -3
- wagtail/admin/viewsets/chooser.py +1 -1
- wagtail/admin/viewsets/model.py +26 -1
- wagtail/admin/wagtail_hooks.py +2 -1
- wagtail/api/v2/filters.py +6 -0
- wagtail/api/v2/tests/test_documents.py +1 -1
- wagtail/api/v2/tests/test_images.py +1 -1
- wagtail/api/v2/tests/test_pages.py +11 -1
- wagtail/api/v2/utils.py +2 -2
- wagtail/blocks/base.py +35 -12
- wagtail/blocks/definition_lookup.py +85 -0
- wagtail/blocks/list_block.py +12 -0
- wagtail/blocks/migrations/migrate_operation.py +2 -0
- wagtail/blocks/stream_block.py +19 -0
- wagtail/blocks/struct_block.py +19 -0
- wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/frontend_cache/backends/__init__.py +5 -0
- wagtail/contrib/frontend_cache/backends/azure.py +179 -0
- wagtail/contrib/frontend_cache/backends/base.py +28 -0
- wagtail/contrib/frontend_cache/backends/cloudflare.py +114 -0
- wagtail/contrib/frontend_cache/backends/cloudfront.py +99 -0
- wagtail/contrib/frontend_cache/backends/http.py +64 -0
- wagtail/contrib/frontend_cache/tests.py +59 -17
- wagtail/contrib/frontend_cache/utils.py +26 -8
- wagtail/contrib/redirects/filters.py +15 -1
- wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +37 -72
- wagtail/contrib/redirects/models.py +6 -5
- wagtail/contrib/redirects/templates/wagtailredirects/edit.html +1 -38
- wagtail/contrib/redirects/tests/test_redirects.py +141 -1
- wagtail/contrib/redirects/urls.py +1 -2
- wagtail/contrib/redirects/views.py +39 -80
- wagtail/contrib/routable_page/models.py +6 -4
- wagtail/contrib/routable_page/tests.py +11 -0
- wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +4 -4
- wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +5 -1
- wagtail/contrib/simple_translation/models.py +2 -1
- wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
- wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
- wagtail/contrib/typed_table_block/blocks.py +19 -0
- wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
- wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
- wagtail/contrib/typed_table_block/tests.py +38 -0
- wagtail/coreutils.py +1 -1
- wagtail/documents/__init__.py +1 -1
- wagtail/documents/locale/en/LC_MESSAGES/django.po +8 -8
- wagtail/documents/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/sl/LC_MESSAGES/django.po +20 -0
- wagtail/documents/models.py +5 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
- wagtail/documents/tests/test_models.py +5 -1
- wagtail/embeds/apps.py +2 -0
- wagtail/embeds/embeds.py +12 -14
- wagtail/embeds/finders/__init__.py +2 -0
- wagtail/embeds/finders/facebook.py +17 -33
- wagtail/embeds/finders/instagram.py +19 -16
- wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/embeds/signal_handlers.py +13 -0
- wagtail/embeds/tests/test_embeds.py +7 -7
- wagtail/fields.py +58 -14
- wagtail/images/__init__.py +1 -1
- wagtail/images/locale/en/LC_MESSAGES/django.po +34 -34
- wagtail/images/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/sl/LC_MESSAGES/django.po +20 -0
- wagtail/images/models.py +2 -0
- wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
- wagtail/images/templates/wagtailimages/images/edit.html +4 -4
- wagtail/images/tests/test_admin_views.py +26 -2
- wagtail/images/views/chooser.py +6 -1
- wagtail/locale/en/LC_MESSAGES/django.po +84 -80
- wagtail/locales/locale/en/LC_MESSAGES/django.po +2 -2
- wagtail/locales/tests.py +16 -0
- wagtail/locales/wagtail_hooks.py +0 -9
- wagtail/migrations/0094_alter_page_locale.py +19 -0
- wagtail/models/__init__.py +11 -5
- wagtail/models/i18n.py +6 -1
- wagtail/project_template/requirements.txt +1 -1
- wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/signals.py +4 -0
- wagtail/sites/locale/en/LC_MESSAGES/django.po +2 -2
- wagtail/sites/tests.py +15 -0
- wagtail/sites/wagtail_hooks.py +0 -9
- wagtail/snippets/locale/en/LC_MESSAGES/django.po +9 -9
- wagtail/snippets/permissions.py +5 -3
- wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
- wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
- wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/menu.html +1 -1
- wagtail/snippets/tests/test_snippets.py +78 -12
- wagtail/snippets/tests/test_viewset.py +22 -0
- wagtail/snippets/views/snippets.py +19 -14
- wagtail/snippets/wagtail_hooks.py +2 -10
- wagtail/templatetags/wagtailcore_tags.py +3 -0
- wagtail/test/dummy_external_storage.py +1 -1
- wagtail/test/i18n/migrations/0003_alter_clusterabletestmodel_locale_and_more.py +40 -0
- wagtail/test/routablepage/models.py +4 -0
- wagtail/test/snippets/migrations/0012_alter_translatablesnippet_locale.py +20 -0
- wagtail/test/testapp/migrations/0038_sociallink.py +52 -0
- wagtail/test/testapp/migrations/0039_alter_eventcategory_locale_and_more.py +45 -0
- wagtail/test/testapp/models.py +24 -0
- wagtail/test/testapp/views.py +1 -0
- wagtail/test/testapp/wagtail_hooks.py +9 -0
- wagtail/test/urls_multilang.py +6 -1
- wagtail/test/urls_multilang_non_root.py +11 -0
- wagtail/tests/streamfield_migrations/test_migrations.py +53 -12
- wagtail/tests/test_audit_log.py +72 -2
- wagtail/tests/test_blocks.py +103 -0
- wagtail/tests/test_signals.py +49 -2
- wagtail/tests/test_streamfield.py +153 -0
- wagtail/tests/test_utils.py +14 -0
- wagtail/tests/tests.py +5 -0
- wagtail/users/apps.py +1 -0
- wagtail/users/forms.py +7 -0
- wagtail/users/locale/en/LC_MESSAGES/django.po +55 -50
- wagtail/users/models.py +1 -0
- wagtail/users/templates/wagtailusers/groups/includes/formatted_permissions.html +3 -2
- wagtail/users/templatetags/wagtailusers_tags.py +9 -0
- wagtail/users/tests/__init__.py +7 -1
- wagtail/users/tests/test_admin_views.py +117 -32
- wagtail/users/views/groups.py +4 -0
- wagtail/users/views/users.py +58 -14
- wagtail/users/wagtail_hooks.py +7 -123
- wagtail/utils/utils.py +1 -0
- wagtail/utils/version.py +5 -2
- {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/METADATA +3 -3
- {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/RECORD +248 -222
- wagtail/admin/templates/wagtailadmin/reports/aging_pages.html +0 -58
- wagtail/admin/templates/wagtailadmin/reports/page_types_usage.html +0 -18
- wagtail/admin/templates/wagtailadmin/reports/site_history.html +0 -57
- wagtail/admin/templates/wagtailadmin/reports/workflow.html +0 -81
- wagtail/admin/templates/wagtailadmin/reports/workflow_tasks.html +0 -63
- wagtail/contrib/frontend_cache/backends.py +0 -400
- wagtail/contrib/redirects/templates/wagtailredirects/list.html +0 -43
- wagtail/contrib/redirects/templates/wagtailredirects/reports/redirects_report.html +0 -14
- wagtail/contrib/redirects/tests/test_reports_view.py +0 -82
- {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/LICENSE +0 -0
- {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/WHEEL +0 -0
- {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/entry_points.txt +0 -0
- {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
5
|
+
|
|
6
|
+
from .base import BaseBackend
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("wagtail.frontendcache")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ["CloudflareBackend"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CloudflareBackend(BaseBackend):
|
|
15
|
+
CHUNK_SIZE = 30
|
|
16
|
+
|
|
17
|
+
def __init__(self, params):
|
|
18
|
+
super().__init__(params)
|
|
19
|
+
|
|
20
|
+
self.cloudflare_email = params.pop("EMAIL", None)
|
|
21
|
+
self.cloudflare_api_key = params.pop("TOKEN", None) or params.pop(
|
|
22
|
+
"API_KEY", None
|
|
23
|
+
)
|
|
24
|
+
self.cloudflare_token = params.pop("BEARER_TOKEN", None)
|
|
25
|
+
self.cloudflare_zoneid = params.pop("ZONEID")
|
|
26
|
+
self.cloudflare_purge_endpoint_url = (
|
|
27
|
+
"https://api.cloudflare.com/client/v4/zones/{}/purge_cache".format(
|
|
28
|
+
self.cloudflare_zoneid
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
(not self.cloudflare_email and self.cloudflare_api_key)
|
|
34
|
+
or (self.cloudflare_email and not self.cloudflare_api_key)
|
|
35
|
+
or (
|
|
36
|
+
not any(
|
|
37
|
+
[
|
|
38
|
+
self.cloudflare_email,
|
|
39
|
+
self.cloudflare_api_key,
|
|
40
|
+
self.cloudflare_token,
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
):
|
|
45
|
+
raise ImproperlyConfigured(
|
|
46
|
+
"The setting 'WAGTAILFRONTENDCACHE' requires both 'EMAIL' and 'API_KEY', or 'BEARER_TOKEN' to be specified."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _purge_urls(self, urls):
|
|
50
|
+
try:
|
|
51
|
+
purge_url = (
|
|
52
|
+
"https://api.cloudflare.com/client/v4/zones/{}/purge_cache".format(
|
|
53
|
+
self.cloudflare_zoneid
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
headers = {"Content-Type": "application/json"}
|
|
58
|
+
|
|
59
|
+
if self.cloudflare_token:
|
|
60
|
+
headers["Authorization"] = f"Bearer {self.cloudflare_token}"
|
|
61
|
+
else:
|
|
62
|
+
headers["X-Auth-Email"] = self.cloudflare_email
|
|
63
|
+
headers["X-Auth-Key"] = self.cloudflare_api_key
|
|
64
|
+
|
|
65
|
+
data = {"files": urls}
|
|
66
|
+
|
|
67
|
+
response = requests.delete(
|
|
68
|
+
purge_url,
|
|
69
|
+
json=data,
|
|
70
|
+
headers=headers,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
response_json = response.json()
|
|
75
|
+
except ValueError:
|
|
76
|
+
if response.status_code != 200:
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
else:
|
|
79
|
+
for url in urls:
|
|
80
|
+
logger.error(
|
|
81
|
+
"Couldn't purge '%s' from Cloudflare. Unexpected JSON parse error.",
|
|
82
|
+
url,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
except requests.exceptions.HTTPError as e:
|
|
86
|
+
for url in urls:
|
|
87
|
+
logging.exception(
|
|
88
|
+
"Couldn't purge '%s' from Cloudflare. HTTPError: %d",
|
|
89
|
+
url,
|
|
90
|
+
e.response.status_code,
|
|
91
|
+
)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if response_json["success"] is False:
|
|
95
|
+
error_messages = ", ".join(
|
|
96
|
+
[str(err["message"]) for err in response_json["errors"]]
|
|
97
|
+
)
|
|
98
|
+
for url in urls:
|
|
99
|
+
logger.error(
|
|
100
|
+
"Couldn't purge '%s' from Cloudflare. Cloudflare errors '%s'",
|
|
101
|
+
url,
|
|
102
|
+
error_messages,
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
def purge_batch(self, urls):
|
|
107
|
+
# Break the batched URLs in to chunks to fit within Cloudflare's maximum size for
|
|
108
|
+
# the purge_cache call (https://api.cloudflare.com/#zone-purge-files-by-url)
|
|
109
|
+
for i in range(0, len(urls), self.CHUNK_SIZE):
|
|
110
|
+
chunk = urls[i : i + self.CHUNK_SIZE]
|
|
111
|
+
self._purge_urls(chunk)
|
|
112
|
+
|
|
113
|
+
def purge(self, url):
|
|
114
|
+
self._purge_urls([url])
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
from warnings import warn
|
|
6
|
+
|
|
7
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
8
|
+
|
|
9
|
+
from wagtail.utils.deprecation import RemovedInWagtail70Warning
|
|
10
|
+
|
|
11
|
+
from .base import BaseBackend
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("wagtail.frontendcache")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["CloudfrontBackend"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CloudfrontBackend(BaseBackend):
|
|
20
|
+
def __init__(self, params):
|
|
21
|
+
import boto3
|
|
22
|
+
|
|
23
|
+
super().__init__(params)
|
|
24
|
+
|
|
25
|
+
self.client = boto3.client(
|
|
26
|
+
"cloudfront",
|
|
27
|
+
aws_access_key_id=params.get("AWS_ACCESS_KEY_ID"),
|
|
28
|
+
aws_secret_access_key=params.get("AWS_SECRET_ACCESS_KEY"),
|
|
29
|
+
aws_session_token=params.get("AWS_SESSION_TOKEN"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
self.cloudfront_distribution_id = params.pop("DISTRIBUTION_ID")
|
|
34
|
+
except KeyError:
|
|
35
|
+
raise ImproperlyConfigured(
|
|
36
|
+
"The setting 'WAGTAILFRONTENDCACHE' requires the object 'DISTRIBUTION_ID'."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Add known hostnames for hostname validation (if not already defined)
|
|
40
|
+
# RemovedInWagtail70Warning
|
|
41
|
+
if isinstance(self.cloudfront_distribution_id, dict):
|
|
42
|
+
if "HOSTNAMES" in params:
|
|
43
|
+
self.hostnames.extend(self.cloudfront_distribution_id.keys())
|
|
44
|
+
else:
|
|
45
|
+
self.hostnames = list(self.cloudfront_distribution_id.keys())
|
|
46
|
+
|
|
47
|
+
def purge_batch(self, urls):
|
|
48
|
+
paths_by_distribution_id = defaultdict(list)
|
|
49
|
+
|
|
50
|
+
for url in urls:
|
|
51
|
+
url_parsed = urlparse(url)
|
|
52
|
+
distribution_id = None
|
|
53
|
+
|
|
54
|
+
if isinstance(self.cloudfront_distribution_id, dict):
|
|
55
|
+
warn(
|
|
56
|
+
"Using a `DISTRIBUTION_ID` mapping is deprecated - use `HOSTNAMES` in combination with multiple backends instead.",
|
|
57
|
+
category=RemovedInWagtail70Warning,
|
|
58
|
+
)
|
|
59
|
+
host = url_parsed.hostname
|
|
60
|
+
if host in self.cloudfront_distribution_id:
|
|
61
|
+
distribution_id = self.cloudfront_distribution_id.get(host)
|
|
62
|
+
else:
|
|
63
|
+
logger.warning(
|
|
64
|
+
"Couldn't purge '%s' from CloudFront. Hostname '%s' not found in the DISTRIBUTION_ID mapping",
|
|
65
|
+
url,
|
|
66
|
+
host,
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
distribution_id = self.cloudfront_distribution_id
|
|
70
|
+
|
|
71
|
+
if distribution_id:
|
|
72
|
+
paths_by_distribution_id[distribution_id].append(url_parsed.path)
|
|
73
|
+
|
|
74
|
+
for distribution_id, paths in paths_by_distribution_id.items():
|
|
75
|
+
self._create_invalidation(distribution_id, paths)
|
|
76
|
+
|
|
77
|
+
def purge(self, url):
|
|
78
|
+
self.purge_batch([url])
|
|
79
|
+
|
|
80
|
+
def _create_invalidation(self, distribution_id, paths):
|
|
81
|
+
import botocore
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
self.client.create_invalidation(
|
|
85
|
+
DistributionId=distribution_id,
|
|
86
|
+
InvalidationBatch={
|
|
87
|
+
"Paths": {"Quantity": len(paths), "Items": paths},
|
|
88
|
+
"CallerReference": str(uuid.uuid4()),
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
except botocore.exceptions.ClientError as e:
|
|
92
|
+
for path in paths:
|
|
93
|
+
logger.error(
|
|
94
|
+
"Couldn't purge path '%s' from CloudFront (DistributionId=%s). ClientError: %s %s",
|
|
95
|
+
path,
|
|
96
|
+
distribution_id,
|
|
97
|
+
e.response["Error"]["Code"],
|
|
98
|
+
e.response["Error"]["Message"],
|
|
99
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from urllib.error import HTTPError, URLError
|
|
3
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
4
|
+
from urllib.request import Request, urlopen
|
|
5
|
+
|
|
6
|
+
from wagtail import __version__
|
|
7
|
+
|
|
8
|
+
from .base import BaseBackend
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("wagtail.frontendcache")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ["PurgeRequest", "HTTPBackend"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PurgeRequest(Request):
|
|
17
|
+
def get_method(self):
|
|
18
|
+
return "PURGE"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HTTPBackend(BaseBackend):
|
|
22
|
+
def __init__(self, params):
|
|
23
|
+
super().__init__(params)
|
|
24
|
+
location_url_parsed = urlsplit(params.pop("LOCATION"))
|
|
25
|
+
self.cache_scheme = location_url_parsed.scheme
|
|
26
|
+
self.cache_netloc = location_url_parsed.netloc
|
|
27
|
+
|
|
28
|
+
def purge(self, url):
|
|
29
|
+
url_parsed = urlsplit(url)
|
|
30
|
+
host = url_parsed.hostname
|
|
31
|
+
|
|
32
|
+
# Append port to host if it is set in the original URL
|
|
33
|
+
if url_parsed.port:
|
|
34
|
+
host += ":" + str(url_parsed.port)
|
|
35
|
+
|
|
36
|
+
request = PurgeRequest(
|
|
37
|
+
url=urlunsplit(
|
|
38
|
+
[
|
|
39
|
+
self.cache_scheme,
|
|
40
|
+
self.cache_netloc,
|
|
41
|
+
url_parsed.path,
|
|
42
|
+
url_parsed.query,
|
|
43
|
+
url_parsed.fragment,
|
|
44
|
+
]
|
|
45
|
+
),
|
|
46
|
+
headers={
|
|
47
|
+
"Host": host,
|
|
48
|
+
"User-Agent": "Wagtail-frontendcache/" + __version__,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
urlopen(request)
|
|
54
|
+
except HTTPError as e:
|
|
55
|
+
logger.error(
|
|
56
|
+
"Couldn't purge '%s' from HTTP cache. HTTPError: %d %s",
|
|
57
|
+
url,
|
|
58
|
+
e.code,
|
|
59
|
+
e.reason,
|
|
60
|
+
)
|
|
61
|
+
except URLError as e:
|
|
62
|
+
logger.error(
|
|
63
|
+
"Couldn't purge '%s' from HTTP cache. URLError: %s", url, e.reason
|
|
64
|
+
)
|
|
@@ -5,7 +5,7 @@ import requests
|
|
|
5
5
|
from azure.mgmt.cdn import CdnManagementClient
|
|
6
6
|
from azure.mgmt.frontdoor import FrontDoorManagementClient
|
|
7
7
|
from django.core.exceptions import ImproperlyConfigured
|
|
8
|
-
from django.test import TestCase
|
|
8
|
+
from django.test import SimpleTestCase, TestCase
|
|
9
9
|
from django.test.utils import override_settings
|
|
10
10
|
|
|
11
11
|
from wagtail.contrib.frontend_cache.backends import (
|
|
@@ -19,6 +19,7 @@ from wagtail.contrib.frontend_cache.backends import (
|
|
|
19
19
|
from wagtail.contrib.frontend_cache.utils import get_backends
|
|
20
20
|
from wagtail.models import Page
|
|
21
21
|
from wagtail.test.testapp.models import EventIndex
|
|
22
|
+
from wagtail.utils.deprecation import RemovedInWagtail70Warning
|
|
22
23
|
|
|
23
24
|
from .utils import (
|
|
24
25
|
PurgeBatch,
|
|
@@ -29,7 +30,7 @@ from .utils import (
|
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
class TestBackendConfiguration(
|
|
33
|
+
class TestBackendConfiguration(SimpleTestCase):
|
|
33
34
|
def test_default(self):
|
|
34
35
|
backends = get_backends()
|
|
35
36
|
|
|
@@ -81,6 +82,8 @@ class TestBackendConfiguration(TestCase):
|
|
|
81
82
|
"cloudfront": {
|
|
82
83
|
"BACKEND": "wagtail.contrib.frontend_cache.backends.CloudfrontBackend",
|
|
83
84
|
"DISTRIBUTION_ID": "frontend",
|
|
85
|
+
"AWS_ACCESS_KEY_ID": "my-access-key-id",
|
|
86
|
+
"AWS_SECRET_ACCESS_KEY": "my-secret-access-key",
|
|
84
87
|
},
|
|
85
88
|
}
|
|
86
89
|
)
|
|
@@ -90,6 +93,12 @@ class TestBackendConfiguration(TestCase):
|
|
|
90
93
|
|
|
91
94
|
self.assertEqual(backends["cloudfront"].cloudfront_distribution_id, "frontend")
|
|
92
95
|
|
|
96
|
+
credentials = backends["cloudfront"].client._request_signer._credentials
|
|
97
|
+
|
|
98
|
+
self.assertEqual(credentials.method, "explicit")
|
|
99
|
+
self.assertEqual(credentials.access_key, "my-access-key-id")
|
|
100
|
+
self.assertEqual(credentials.secret_key, "my-secret-access-key")
|
|
101
|
+
|
|
93
102
|
def test_azure_cdn(self):
|
|
94
103
|
backends = get_backends(
|
|
95
104
|
backend_settings={
|
|
@@ -172,7 +181,7 @@ class TestBackendConfiguration(TestCase):
|
|
|
172
181
|
self.assertIs(client._config.credential, mock_credentials)
|
|
173
182
|
|
|
174
183
|
@mock.patch(
|
|
175
|
-
"wagtail.contrib.frontend_cache.backends.AzureCdnBackend._make_purge_call"
|
|
184
|
+
"wagtail.contrib.frontend_cache.backends.azure.AzureCdnBackend._make_purge_call"
|
|
176
185
|
)
|
|
177
186
|
def test_azure_cdn_purge(self, make_purge_call_mock):
|
|
178
187
|
backends = get_backends(
|
|
@@ -214,7 +223,7 @@ class TestBackendConfiguration(TestCase):
|
|
|
214
223
|
self.assertEqual(call_args[1], ["/home/events/christmas/?test=1", "/blog/"])
|
|
215
224
|
|
|
216
225
|
@mock.patch(
|
|
217
|
-
"wagtail.contrib.frontend_cache.backends.AzureFrontDoorBackend._make_purge_call"
|
|
226
|
+
"wagtail.contrib.frontend_cache.backends.azure.AzureFrontDoorBackend._make_purge_call"
|
|
218
227
|
)
|
|
219
228
|
def test_azure_front_door_purge(self, make_purge_call_mock):
|
|
220
229
|
backends = get_backends(
|
|
@@ -285,7 +294,7 @@ class TestBackendConfiguration(TestCase):
|
|
|
285
294
|
log_output.output[0],
|
|
286
295
|
)
|
|
287
296
|
|
|
288
|
-
@mock.patch("wagtail.contrib.frontend_cache.backends.urlopen")
|
|
297
|
+
@mock.patch("wagtail.contrib.frontend_cache.backends.http.urlopen")
|
|
289
298
|
def _test_http_with_side_effect(self, urlopen_mock, urlopen_side_effect):
|
|
290
299
|
# given a backends configuration with one HTTP backend
|
|
291
300
|
backends = get_backends(
|
|
@@ -323,7 +332,7 @@ class TestBackendConfiguration(TestCase):
|
|
|
323
332
|
)
|
|
324
333
|
|
|
325
334
|
@mock.patch(
|
|
326
|
-
"wagtail.contrib.frontend_cache.backends.CloudfrontBackend._create_invalidation"
|
|
335
|
+
"wagtail.contrib.frontend_cache.backends.cloudfront.CloudfrontBackend._create_invalidation"
|
|
327
336
|
)
|
|
328
337
|
def test_cloudfront_distribution_id_mapping(self, _create_invalidation):
|
|
329
338
|
backends = get_backends(
|
|
@@ -336,15 +345,31 @@ class TestBackendConfiguration(TestCase):
|
|
|
336
345
|
},
|
|
337
346
|
}
|
|
338
347
|
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
348
|
+
with self.assertWarnsMessage(
|
|
349
|
+
RemovedInWagtail70Warning,
|
|
350
|
+
"Using a `DISTRIBUTION_ID` mapping is deprecated - use `HOSTNAMES` in combination with multiple backends instead.",
|
|
351
|
+
):
|
|
352
|
+
backends.get("cloudfront").purge(
|
|
353
|
+
"http://www.wagtail.org/home/events/christmas/"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
with self.assertWarnsMessage(
|
|
357
|
+
RemovedInWagtail70Warning,
|
|
358
|
+
"Using a `DISTRIBUTION_ID` mapping is deprecated - use `HOSTNAMES` in combination with multiple backends instead.",
|
|
359
|
+
):
|
|
360
|
+
backends.get("cloudfront").purge("http://torchbox.com/blog/")
|
|
343
361
|
|
|
344
362
|
_create_invalidation.assert_called_once_with(
|
|
345
363
|
"frontend", ["/home/events/christmas/"]
|
|
346
364
|
)
|
|
347
365
|
|
|
366
|
+
self.assertTrue(
|
|
367
|
+
backends.get("cloudfront").invalidates_hostname("www.wagtail.org")
|
|
368
|
+
)
|
|
369
|
+
self.assertFalse(
|
|
370
|
+
backends.get("cloudfront").invalidates_hostname("torchbox.com")
|
|
371
|
+
)
|
|
372
|
+
|
|
348
373
|
def test_multiple(self):
|
|
349
374
|
backends = get_backends(
|
|
350
375
|
backend_settings={
|
|
@@ -396,17 +421,11 @@ PURGED_URLS = []
|
|
|
396
421
|
|
|
397
422
|
|
|
398
423
|
class MockBackend(BaseBackend):
|
|
399
|
-
def __init__(self, config):
|
|
400
|
-
pass
|
|
401
|
-
|
|
402
424
|
def purge(self, url):
|
|
403
425
|
PURGED_URLS.append(url)
|
|
404
426
|
|
|
405
427
|
|
|
406
428
|
class MockCloudflareBackend(CloudflareBackend):
|
|
407
|
-
def __init__(self, config):
|
|
408
|
-
pass
|
|
409
|
-
|
|
410
429
|
def _purge_urls(self, urls):
|
|
411
430
|
if len(urls) > self.CHUNK_SIZE:
|
|
412
431
|
raise Exception("Cloudflare backend is not chunking requests as expected")
|
|
@@ -465,11 +484,34 @@ class TestCachePurgingFunctions(TestCase):
|
|
|
465
484
|
],
|
|
466
485
|
)
|
|
467
486
|
|
|
487
|
+
@override_settings(
|
|
488
|
+
WAGTAILFRONTENDCACHE={
|
|
489
|
+
"varnish": {
|
|
490
|
+
"BACKEND": "wagtail.contrib.frontend_cache.tests.MockBackend",
|
|
491
|
+
"HOSTNAMES": ["example.com"],
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
def test_invalidate_specific_location(self):
|
|
496
|
+
with self.assertLogs(level="INFO") as log_output:
|
|
497
|
+
purge_url_from_cache("http://localhost/foo")
|
|
498
|
+
|
|
499
|
+
self.assertEqual(PURGED_URLS, [])
|
|
500
|
+
self.assertIn(
|
|
501
|
+
"Unable to find purge backend for localhost",
|
|
502
|
+
log_output.output[0],
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
purge_url_from_cache("http://example.com/foo")
|
|
506
|
+
self.assertEqual(PURGED_URLS, ["http://example.com/foo"])
|
|
507
|
+
|
|
468
508
|
|
|
469
509
|
@override_settings(
|
|
470
510
|
WAGTAILFRONTENDCACHE={
|
|
471
511
|
"cloudflare": {
|
|
472
512
|
"BACKEND": "wagtail.contrib.frontend_cache.tests.MockCloudflareBackend",
|
|
513
|
+
"ZONEID": "zone",
|
|
514
|
+
"BEARER_TOKEN": "token",
|
|
473
515
|
},
|
|
474
516
|
}
|
|
475
517
|
)
|
|
@@ -634,7 +676,7 @@ class TestPurgeBatchClass(TestCase):
|
|
|
634
676
|
],
|
|
635
677
|
)
|
|
636
678
|
|
|
637
|
-
@mock.patch("wagtail.contrib.frontend_cache.backends.requests.delete")
|
|
679
|
+
@mock.patch("wagtail.contrib.frontend_cache.backends.cloudflare.requests.delete")
|
|
638
680
|
def test_http_error_on_cloudflare_purge_batch(self, requests_delete_mock):
|
|
639
681
|
backend_settings = {
|
|
640
682
|
"cloudflare": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
|
-
from
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
4
5
|
|
|
5
6
|
from django.conf import settings
|
|
6
7
|
from django.core.exceptions import ImproperlyConfigured
|
|
@@ -79,13 +80,12 @@ def purge_urls_from_cache(urls, backend_settings=None, backends=None):
|
|
|
79
80
|
# Purge the given url for each managed language
|
|
80
81
|
for isocode in languages:
|
|
81
82
|
for url in urls:
|
|
82
|
-
up =
|
|
83
|
-
new_url =
|
|
83
|
+
up = urlsplit(url)
|
|
84
|
+
new_url = urlunsplit(
|
|
84
85
|
(
|
|
85
86
|
up.scheme,
|
|
86
87
|
up.netloc,
|
|
87
88
|
re.sub(langs_regex, "/%s/" % isocode, up.path),
|
|
88
|
-
up.params,
|
|
89
89
|
up.query,
|
|
90
90
|
up.fragment,
|
|
91
91
|
)
|
|
@@ -100,11 +100,29 @@ def purge_urls_from_cache(urls, backend_settings=None, backends=None):
|
|
|
100
100
|
|
|
101
101
|
urls = new_urls
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
for url in urls:
|
|
105
|
-
logger.info("[%s] Purging URL: %s", backend_name, url)
|
|
103
|
+
urls_by_hostname = defaultdict(list)
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
for url in urls:
|
|
106
|
+
urls_by_hostname[urlsplit(url).netloc].append(url)
|
|
107
|
+
|
|
108
|
+
backends = get_backends(backend_settings, backends)
|
|
109
|
+
|
|
110
|
+
for hostname, urls in urls_by_hostname.items():
|
|
111
|
+
backends_for_hostname = {
|
|
112
|
+
backend_name: backend
|
|
113
|
+
for backend_name, backend in backends.items()
|
|
114
|
+
if backend.invalidates_hostname(hostname)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if not backends_for_hostname:
|
|
118
|
+
logger.info("Unable to find purge backend for %s", hostname)
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
for backend_name, backend in backends_for_hostname.items():
|
|
122
|
+
for url in urls:
|
|
123
|
+
logger.info("[%s] Purging URL: %s", backend_name, url)
|
|
124
|
+
|
|
125
|
+
backend.purge_batch(urls)
|
|
108
126
|
|
|
109
127
|
|
|
110
128
|
def _get_page_cached_urls(page):
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import django_filters
|
|
2
2
|
from django import forms
|
|
3
|
+
from django.db.models import QuerySet
|
|
3
4
|
from django.utils.translation import gettext as _
|
|
4
5
|
|
|
5
6
|
from wagtail.admin.filters import WagtailFilterSet
|
|
6
7
|
from wagtail.contrib.redirects.models import Redirect
|
|
7
|
-
from wagtail.models import Site
|
|
8
|
+
from wagtail.models import Page, Site
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_redirect_pages_queryset(request) -> QuerySet[Page]:
|
|
12
|
+
redirect_page_pks = (
|
|
13
|
+
Redirect.objects.filter(redirect_page__isnull=False)
|
|
14
|
+
.order_by()
|
|
15
|
+
.values_list("redirect_page", flat=True)
|
|
16
|
+
.distinct()
|
|
17
|
+
)
|
|
18
|
+
return Page.objects.filter(pk__in=redirect_page_pks)
|
|
8
19
|
|
|
9
20
|
|
|
10
21
|
class RedirectsReportFilterSet(WagtailFilterSet):
|
|
22
|
+
redirect_page = django_filters.ModelChoiceFilter(
|
|
23
|
+
field_name="redirect_page", queryset=get_redirect_pages_queryset
|
|
24
|
+
)
|
|
11
25
|
is_permanent = django_filters.ChoiceFilter(
|
|
12
26
|
label=_("Type"),
|
|
13
27
|
method="filter_type",
|