wagtail 6.4.1__py3-none-any.whl → 7.0rc1__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_page.py +4 -4
- wagtail/admin/action_menu.py +3 -0
- wagtail/admin/active_filters.py +218 -0
- wagtail/admin/auth.py +3 -39
- wagtail/admin/forms/choosers.py +8 -7
- wagtail/admin/forms/models.py +46 -1
- wagtail/admin/forms/pages.py +9 -0
- wagtail/admin/locale/ar/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ar/LC_MESSAGES/django.po +30 -12
- wagtail/admin/locale/ar/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ar/LC_MESSAGES/djangojs.po +5 -1
- wagtail/admin/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/be/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/be/LC_MESSAGES/django.po +27 -13
- wagtail/admin/locale/be/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/be/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/bg/LC_MESSAGES/django.po +3 -3
- wagtail/admin/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ca/LC_MESSAGES/django.po +30 -28
- wagtail/admin/locale/ca/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ca/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/cs/LC_MESSAGES/django.po +30 -19
- wagtail/admin/locale/cs/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/cs/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/cy/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/cy/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/cy/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/da/LC_MESSAGES/django.po +4 -4
- wagtail/admin/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/de/LC_MESSAGES/django.po +33 -30
- wagtail/admin/locale/de/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/de/LC_MESSAGES/djangojs.po +5 -1
- wagtail/admin/locale/dv/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/dv/LC_MESSAGES/django.po +7 -18
- wagtail/admin/locale/dv/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/dv/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/el/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/el/LC_MESSAGES/django.po +9 -8
- wagtail/admin/locale/el/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/el/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/en/LC_MESSAGES/django.po +386 -319
- wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +11 -7
- wagtail/admin/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/es/LC_MESSAGES/django.po +32 -22
- wagtail/admin/locale/es/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/es/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/es_419/LC_MESSAGES/django.po +2 -1
- wagtail/admin/locale/es_VE/LC_MESSAGES/django.po +2 -1
- wagtail/admin/locale/et/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/et/LC_MESSAGES/django.po +30 -19
- wagtail/admin/locale/et/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/et/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/fa/LC_MESSAGES/django.po +10 -9
- wagtail/admin/locale/fa/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/fa/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/fi/LC_MESSAGES/django.po +30 -21
- wagtail/admin/locale/fi/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/fi/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/fr/LC_MESSAGES/django.po +80 -24
- wagtail/admin/locale/fr/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/fr/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/gl/LC_MESSAGES/django.po +75 -22
- wagtail/admin/locale/gl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/gl/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/he_IL/LC_MESSAGES/django.po +5 -5
- wagtail/admin/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/hr_HR/LC_MESSAGES/django.po +30 -19
- wagtail/admin/locale/hr_HR/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/hr_HR/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/hu/LC_MESSAGES/django.po +350 -25
- wagtail/admin/locale/hu/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/hu/LC_MESSAGES/djangojs.po +8 -0
- wagtail/admin/locale/id_ID/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/id_ID/LC_MESSAGES/django.po +11 -11
- wagtail/admin/locale/is_IS/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/is_IS/LC_MESSAGES/django.po +30 -29
- wagtail/admin/locale/is_IS/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/is_IS/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/it/LC_MESSAGES/django.po +30 -29
- wagtail/admin/locale/it/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/it/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/ja/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ja/LC_MESSAGES/django.po +14 -15
- wagtail/admin/locale/ja/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ja/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ko/LC_MESSAGES/django.po +92 -19
- wagtail/admin/locale/ko/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ko/LC_MESSAGES/djangojs.po +16 -0
- wagtail/admin/locale/lt/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/lt/LC_MESSAGES/django.po +30 -15
- wagtail/admin/locale/lt/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/lt/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/lv/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/lv/LC_MESSAGES/django.po +10 -9
- wagtail/admin/locale/lv/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/lv/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/mi/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/mi/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/mn/LC_MESSAGES/django.po +12 -11
- wagtail/admin/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/nb/LC_MESSAGES/django.po +31 -15
- wagtail/admin/locale/nb/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/nb/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/nl/LC_MESSAGES/django.po +32 -29
- wagtail/admin/locale/nl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/nl/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/pl/LC_MESSAGES/django.po +30 -22
- wagtail/admin/locale/pl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/pl/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/pt_BR/LC_MESSAGES/django.po +33 -28
- wagtail/admin/locale/pt_BR/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/pt_BR/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/pt_PT/LC_MESSAGES/django.po +31 -18
- wagtail/admin/locale/pt_PT/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/pt_PT/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ro/LC_MESSAGES/django.po +30 -28
- wagtail/admin/locale/ro/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ro/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ru/LC_MESSAGES/django.po +34 -29
- wagtail/admin/locale/ru/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ru/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/sk_SK/LC_MESSAGES/django.po +6 -6
- wagtail/admin/locale/sk_SK/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/sk_SK/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/sl/LC_MESSAGES/django.po +31 -27
- wagtail/admin/locale/sl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/sl/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/sv/LC_MESSAGES/django.po +30 -18
- wagtail/admin/locale/sv/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/sv/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/tet/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/tet/LC_MESSAGES/django.po +5 -5
- wagtail/admin/locale/tet/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/tet/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/th/LC_MESSAGES/django.po +10 -17
- wagtail/admin/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/tr/LC_MESSAGES/django.po +28 -13
- wagtail/admin/locale/tr/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/tr/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/tr_TR/LC_MESSAGES/django.po +28 -13
- wagtail/admin/locale/tr_TR/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/tr_TR/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/ug/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ug/LC_MESSAGES/django.po +12 -29
- wagtail/admin/locale/ug/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ug/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/uk/LC_MESSAGES/django.po +30 -26
- wagtail/admin/locale/uk/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/uk/LC_MESSAGES/djangojs.po +6 -3
- wagtail/admin/locale/vi/LC_MESSAGES/django.po +2 -1
- wagtail/admin/locale/zh/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/zh/LC_MESSAGES/django.po +2 -2
- wagtail/admin/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/zh_Hans/LC_MESSAGES/django.po +33 -18
- wagtail/admin/locale/zh_Hans/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/zh_Hans/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/django.po +11 -14
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/djangojs.po +3 -0
- wagtail/admin/localization.py +42 -1
- wagtail/admin/menu.py +1 -10
- wagtail/admin/paginator.py +95 -0
- wagtail/admin/panels/field_panel.py +27 -6
- wagtail/admin/panels/inline_panel.py +28 -11
- wagtail/admin/panels/multiple_chooser_panel.py +3 -3
- wagtail/admin/rich_text/converters/html_to_contentstate.py +6 -6
- wagtail/admin/search.py +1 -10
- wagtail/admin/static/wagtailadmin/css/core.css +1 -1
- wagtail/admin/static/wagtailadmin/css/panels/draftail.css +1 -1
- wagtail/admin/static/wagtailadmin/js/bulk-actions.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/draftail.js +1 -1
- wagtail/admin/static/wagtailadmin/js/sidebar.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/vendor.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +32 -11
- wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
- wagtail/admin/templates/wagtailadmin/admin_base.html +1 -0
- wagtail/admin/templates/wagtailadmin/generic/index.html +7 -2
- wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +2 -3
- wagtail/admin/templates/wagtailadmin/pages/action_menu/publish.html +2 -2
- wagtail/admin/templates/wagtailadmin/pages/bulk_actions/confirm_bulk_move.html +0 -1
- wagtail/admin/templates/wagtailadmin/pages/move_choose_destination.html +0 -1
- wagtail/admin/templates/wagtailadmin/panels/publishing/schedule_publishing_panel.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/active_filters.html +8 -1
- wagtail/admin/templates/wagtailadmin/shared/pagination_nav.html +25 -13
- wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/privacy.html +1 -2
- wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/workflow.html +1 -1
- wagtail/admin/templates/wagtailadmin/skeleton.html +2 -1
- wagtail/admin/templates/wagtailadmin/tables/locale_cell.html +4 -0
- wagtail/admin/templates/wagtailadmin/userbar/base.html +10 -8
- wagtail/admin/templatetags/wagtailadmin_tags.py +57 -132
- wagtail/admin/tests/pages/test_bulk_actions/test_bulk_move.py +31 -0
- wagtail/admin/tests/pages/test_create_page.py +627 -0
- wagtail/admin/tests/pages/test_edit_page.py +102 -18
- wagtail/admin/tests/pages/test_explorer_view.py +55 -26
- wagtail/admin/tests/pages/test_page_search.py +10 -0
- wagtail/admin/tests/pages/test_page_usage.py +2 -1
- wagtail/admin/tests/pages/test_preview.py +176 -36
- wagtail/admin/tests/pages/test_revisions.py +4 -7
- wagtail/admin/tests/test_account_management.py +1 -1
- wagtail/admin/tests/test_admin_search.py +1 -0
- wagtail/admin/tests/test_buttons_hooks.py +9 -145
- wagtail/admin/tests/test_collections_views.py +60 -0
- wagtail/admin/tests/test_contentstate.py +1 -1
- wagtail/admin/tests/test_dashboard.py +25 -0
- wagtail/admin/tests/test_edit_handlers.py +64 -0
- wagtail/admin/tests/test_forms.py +38 -0
- wagtail/admin/tests/test_keyboard_shortcuts.py +141 -5
- wagtail/admin/tests/test_page_chooser.py +2 -2
- wagtail/admin/tests/test_paginator.py +86 -0
- wagtail/admin/tests/test_privacy.py +12 -1
- wagtail/admin/tests/test_reports_views.py +8 -43
- wagtail/admin/tests/test_rich_text.py +14 -0
- wagtail/admin/tests/test_templatetags.py +50 -36
- wagtail/admin/tests/test_userbar.py +79 -15
- wagtail/admin/tests/test_views.py +16 -1
- wagtail/admin/tests/test_widgets.py +47 -8
- wagtail/admin/tests/test_workflows.py +119 -6
- wagtail/admin/tests/tests.py +16 -15
- wagtail/admin/tests/ui/test_sidebar.py +0 -27
- wagtail/admin/tests/viewsets/test_chooser_viewset.py +38 -4
- wagtail/admin/tests/viewsets/test_model_viewset.py +1 -37
- wagtail/admin/ui/side_panels.py +14 -16
- wagtail/admin/ui/sidebar.py +5 -37
- wagtail/admin/ui/tables/__init__.py +34 -2
- wagtail/admin/ui/tables/pages.py +9 -0
- wagtail/admin/urls/__init__.py +2 -2
- wagtail/admin/urls/pages.py +3 -1
- wagtail/admin/userbar.py +3 -5
- wagtail/admin/utils.py +43 -4
- wagtail/admin/views/generic/base.py +16 -115
- wagtail/admin/views/generic/chooser.py +25 -8
- wagtail/admin/views/generic/mixins.py +45 -11
- wagtail/admin/views/generic/models.py +15 -30
- wagtail/admin/views/generic/preview.py +9 -3
- wagtail/admin/views/generic/workflow.py +11 -1
- wagtail/admin/views/i18n.py +14 -0
- wagtail/admin/views/pages/bulk_actions/move.py +1 -1
- wagtail/admin/views/pages/create.py +68 -6
- wagtail/admin/views/pages/edit.py +33 -14
- wagtail/admin/views/pages/listing.py +3 -2
- wagtail/admin/views/pages/move.py +43 -29
- wagtail/admin/views/pages/preview.py +15 -2
- wagtail/admin/views/pages/search.py +3 -0
- wagtail/admin/views/reports/base.py +0 -22
- wagtail/admin/views/reports/page_types_usage.py +2 -1
- wagtail/admin/views/tags.py +3 -1
- wagtail/admin/views/workflows.py +0 -3
- wagtail/admin/viewsets/model.py +5 -48
- wagtail/admin/wagtail_hooks.py +16 -17
- wagtail/admin/widgets/button.py +3 -33
- wagtail/admin/widgets/tags.py +31 -3
- wagtail/api/v2/views.py +4 -4
- wagtail/blocks/base.py +18 -15
- wagtail/blocks/field_block.py +2 -1
- wagtail/blocks/stream_block.py +15 -11
- wagtail/compat.py +17 -1
- wagtail/contrib/forms/forms.py +10 -7
- wagtail/contrib/forms/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/cs/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/forms/locale/cy/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/forms/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/de/LC_MESSAGES/django.po +3 -2
- wagtail/contrib/forms/locale/dv/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/forms/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +4 -4
- wagtail/contrib/forms/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/fr/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/forms/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/hy/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/it/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/ko/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/mn/LC_MESSAGES/django.po +3 -2
- wagtail/contrib/forms/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/forms/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/pl/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/forms/locale/pt_BR/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/forms/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/ro/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/forms/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/sl/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/forms/locale/sv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/zh_Hans/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/forms/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/tests/test_forms.py +62 -0
- wagtail/contrib/frontend_cache/backends/cloudfront.py +1 -29
- wagtail/contrib/frontend_cache/tests.py +112 -52
- wagtail/contrib/frontend_cache/utils.py +109 -17
- wagtail/contrib/redirects/forms.py +3 -2
- wagtail/contrib/redirects/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/cy/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/redirects/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/de/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/redirects/locale/dv/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/redirects/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +8 -8
- wagtail/contrib/redirects/locale/en_IN/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/es_VE/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/fa/LC_MESSAGES/django.po +3 -2
- wagtail/contrib/redirects/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/fi/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/locale/fr/LC_MESSAGES/django.po +3 -1
- wagtail/contrib/redirects/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/hi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/hy/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/it/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/ko/LC_MESSAGES/django.po +18 -2
- wagtail/contrib/redirects/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/nb/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/redirects/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/nl/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/pl/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/pt_BR/LC_MESSAGES/django.po +3 -2
- wagtail/contrib/redirects/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/ro/LC_MESSAGES/django.po +3 -2
- wagtail/contrib/redirects/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/ru/LC_MESSAGES/django.po +3 -2
- wagtail/contrib/redirects/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/sl/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/locale/sr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/sv/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/locale/ta/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/uk/LC_MESSAGES/django.po +3 -2
- wagtail/contrib/redirects/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/zh_Hans/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/tmp_storages.py +1 -0
- wagtail/contrib/routable_page/tests.py +3 -3
- wagtail/contrib/search_promotions/forms.py +3 -2
- wagtail/contrib/search_promotions/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ar/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ar/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/be/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/be/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/bg/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ca/LC_MESSAGES/django.po +3 -6
- wagtail/contrib/search_promotions/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/cs/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/cy/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/cy/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/de/LC_MESSAGES/django.po +3 -6
- wagtail/contrib/search_promotions/locale/dv/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/search_promotions/locale/el/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/el/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +19 -23
- wagtail/contrib/search_promotions/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/es/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/et/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/et/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/fa/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/fi/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/fr/LC_MESSAGES/django.po +3 -6
- wagtail/contrib/search_promotions/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/gl/LC_MESSAGES/django.po +3 -6
- wagtail/contrib/search_promotions/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/hr_HR/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/ht/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ht/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/hu/LC_MESSAGES/django.po +11 -5
- wagtail/contrib/search_promotions/locale/id_ID/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/id_ID/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/is_IS/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/is_IS/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/ja/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ja/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ko/LC_MESSAGES/django.po +5 -4
- wagtail/contrib/search_promotions/locale/lt/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/lt/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/lv/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/lv/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/mi/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/mi/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/mn/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/my/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/my/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/nb/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/nl/LC_MESSAGES/django.po +3 -6
- wagtail/contrib/search_promotions/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/pl/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/pt_BR/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/pt_PT/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ro/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ru/LC_MESSAGES/django.po +3 -6
- wagtail/contrib/search_promotions/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/sk_SK/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/sl/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/sv/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/tet/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/tet/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/th/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/tr_TR/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/ug/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ug/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/uk/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/vi/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/vi/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/zh/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/zh/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/zh_Hans/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/zh_Hant/LC_MESSAGES/django.po +1 -4
- wagtail/contrib/search_promotions/models.py +1 -7
- wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/queries/chooser/results.html +1 -1
- wagtail/contrib/search_promotions/tests.py +4 -5
- wagtail/contrib/search_promotions/views/settings.py +1 -2
- wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/registry.py +1 -11
- wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/simple_translation/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/hu/LC_MESSAGES/django.po +5 -2
- wagtail/contrib/styleguide/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/be/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/styleguide/locale/be/LC_MESSAGES/django.po +3 -2
- wagtail/contrib/styleguide/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/cy/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/styleguide/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/de/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/dv/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/styleguide/locale/el/LC_MESSAGES/django.po +2 -1
- wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
- wagtail/contrib/styleguide/locale/en_IN/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/es_VE/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/fr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/hi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/it/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ko/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ru/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/styleguide/locale/sk_SK/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/styleguide/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/sv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ta/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/views.py +2 -1
- wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/blocks.py +5 -0
- wagtail/contrib/typed_table_block/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/cy/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/de/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
- wagtail/contrib/typed_table_block/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/fr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/it/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/ko/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/ru/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/sv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/tests.py +27 -0
- wagtail/coreutils.py +0 -19
- wagtail/documents/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ar/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/ar/LC_MESSAGES/django.po +3 -2
- wagtail/documents/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/ca/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/cy/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/de/LC_MESSAGES/django.po +3 -3
- wagtail/documents/locale/dv/LC_MESSAGES/django.po +2 -1
- wagtail/documents/locale/el/LC_MESSAGES/django.po +2 -1
- wagtail/documents/locale/en/LC_MESSAGES/django.po +4 -4
- wagtail/documents/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/es/LC_MESSAGES/django.po +3 -2
- wagtail/documents/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/fi/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/fr/LC_MESSAGES/django.po +2 -1
- wagtail/documents/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/hu/LC_MESSAGES/django.po +18 -3
- wagtail/documents/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/it/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/ko/LC_MESSAGES/django.po +14 -2
- wagtail/documents/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/nb/LC_MESSAGES/django.po +3 -2
- wagtail/documents/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/nl/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/pl/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/pt_BR/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/ro/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/ru/LC_MESSAGES/django.po +5 -2
- wagtail/documents/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/sk_SK/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/sl/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/sv/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/zh_Hans/LC_MESSAGES/django.po +3 -2
- wagtail/documents/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/documents/rich_text/contentstate.py +1 -0
- wagtail/documents/rich_text/editor_html.py +1 -0
- wagtail/documents/tests/test_collection_privacy.py +1 -1
- wagtail/documents/tests/test_serializers.py +0 -1
- wagtail/documents/views/serve.py +0 -15
- wagtail/documents/wagtail_hooks.py +2 -17
- wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/embeds/rich_text/contentstate.py +1 -0
- wagtail/embeds/rich_text/editor_html.py +1 -0
- wagtail/embeds/wagtail_hooks.py +1 -1
- wagtail/images/formats.py +0 -11
- wagtail/images/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/ca/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/cs/LC_MESSAGES/django.po +3 -2
- wagtail/images/locale/cy/LC_MESSAGES/django.po +2 -1
- wagtail/images/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/de/LC_MESSAGES/django.po +3 -2
- wagtail/images/locale/dv/LC_MESSAGES/django.po +2 -1
- wagtail/images/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/en/LC_MESSAGES/django.po +4 -4
- wagtail/images/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/fa/LC_MESSAGES/django.po +3 -2
- wagtail/images/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/fi/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/fr/LC_MESSAGES/django.po +3 -2
- wagtail/images/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/gl/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/hu/LC_MESSAGES/django.po +23 -3
- wagtail/images/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/it/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/ko/LC_MESSAGES/django.po +31 -2
- wagtail/images/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/mn/LC_MESSAGES/django.po +3 -2
- wagtail/images/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/nb/LC_MESSAGES/django.po +3 -2
- wagtail/images/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/nl/LC_MESSAGES/django.po +4 -5
- wagtail/images/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/pl/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/pt_BR/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/ro/LC_MESSAGES/django.po +3 -2
- wagtail/images/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/ru/LC_MESSAGES/django.po +5 -2
- wagtail/images/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/sk_SK/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/sl/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/sv/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/uk/LC_MESSAGES/django.po +5 -4
- wagtail/images/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/zh_Hans/LC_MESSAGES/django.po +2 -1
- wagtail/images/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/images/management/commands/wagtail_update_image_renditions.py +1 -1
- wagtail/images/models.py +23 -7
- wagtail/images/tests/test_admin_views.py +1 -1
- wagtail/images/tests/test_models.py +33 -0
- wagtail/images/tests/tests.py +0 -6
- wagtail/images/wagtail_hooks.py +2 -2
- wagtail/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/locale/ar/LC_MESSAGES/django.po +50 -50
- wagtail/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/locale/be/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/be/LC_MESSAGES/django.po +52 -51
- wagtail/locale/bg/LC_MESSAGES/django.po +4 -4
- wagtail/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ca/LC_MESSAGES/django.po +234 -234
- wagtail/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/cs/LC_MESSAGES/django.po +172 -172
- wagtail/locale/cy/LC_MESSAGES/django.po +12 -12
- wagtail/locale/da/LC_MESSAGES/django.po +7 -7
- wagtail/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/de/LC_MESSAGES/django.po +237 -236
- wagtail/locale/dv/LC_MESSAGES/django.po +216 -215
- wagtail/locale/el/LC_MESSAGES/django.po +76 -75
- wagtail/locale/en/LC_MESSAGES/django.po +339 -339
- wagtail/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/es/LC_MESSAGES/django.po +192 -192
- wagtail/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/locale/et/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/et/LC_MESSAGES/django.po +173 -173
- wagtail/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/locale/fa/LC_MESSAGES/django.po +50 -50
- wagtail/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/fi/LC_MESSAGES/django.po +187 -187
- wagtail/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/fr/LC_MESSAGES/django.po +236 -235
- wagtail/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/gl/LC_MESSAGES/django.po +235 -235
- wagtail/locale/he_IL/LC_MESSAGES/django.po +9 -9
- wagtail/locale/hr_HR/LC_MESSAGES/django.po +184 -184
- wagtail/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/hu/LC_MESSAGES/django.po +251 -229
- wagtail/locale/id_ID/LC_MESSAGES/django.po +49 -49
- wagtail/locale/is_IS/LC_MESSAGES/django.po +232 -232
- wagtail/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/it/LC_MESSAGES/django.po +224 -224
- wagtail/locale/ja/LC_MESSAGES/django.po +110 -110
- wagtail/locale/ka/LC_MESSAGES/django.po +4 -4
- wagtail/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ko/LC_MESSAGES/django.po +88 -53
- wagtail/locale/lt/LC_MESSAGES/django.po +53 -53
- wagtail/locale/lv/LC_MESSAGES/django.po +22 -22
- wagtail/locale/mi/LC_MESSAGES/django.po +167 -167
- wagtail/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/mn/LC_MESSAGES/django.po +51 -50
- wagtail/locale/my/LC_MESSAGES/django.po +6 -6
- wagtail/locale/nb/LC_MESSAGES/django.po +50 -50
- wagtail/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/nl/LC_MESSAGES/django.po +233 -233
- wagtail/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/pl/LC_MESSAGES/django.po +232 -232
- wagtail/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/pt_BR/LC_MESSAGES/django.po +202 -202
- wagtail/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/pt_PT/LC_MESSAGES/django.po +180 -178
- wagtail/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ro/LC_MESSAGES/django.po +233 -232
- wagtail/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ru/LC_MESSAGES/django.po +237 -234
- wagtail/locale/sk_SK/LC_MESSAGES/django.po +47 -47
- wagtail/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/sl/LC_MESSAGES/django.po +233 -233
- wagtail/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/sv/LC_MESSAGES/django.po +207 -207
- wagtail/locale/tet/LC_MESSAGES/django.po +37 -37
- wagtail/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/th/LC_MESSAGES/django.po +54 -53
- wagtail/locale/tr/LC_MESSAGES/django.po +50 -50
- wagtail/locale/tr_TR/LC_MESSAGES/django.po +50 -50
- wagtail/locale/ug/LC_MESSAGES/django.po +232 -232
- wagtail/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/uk/LC_MESSAGES/django.po +185 -185
- wagtail/locale/vi/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/vi/LC_MESSAGES/django.po +20 -19
- wagtail/locale/zh/LC_MESSAGES/django.po +4 -4
- wagtail/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/zh_Hans/LC_MESSAGES/django.po +189 -189
- wagtail/locale/zh_Hant/LC_MESSAGES/django.po +161 -161
- wagtail/locales/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/cy/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/de/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/en/LC_MESSAGES/django.po +26 -14
- wagtail/locales/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/fr/LC_MESSAGES/django.po +14 -2
- wagtail/locales/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/gl/LC_MESSAGES/django.po +13 -3
- wagtail/locales/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/it/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ko/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ru/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/sv/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/locales/tests.py +32 -1
- wagtail/locales/views.py +39 -7
- wagtail/models/__init__.py +67 -5002
- wagtail/models/audit_log.py +1 -1
- wagtail/models/content_types.py +11 -0
- wagtail/models/draft_state.py +197 -0
- wagtail/models/locking.py +82 -0
- wagtail/models/orderable.py +10 -0
- wagtail/models/pages.py +2754 -0
- wagtail/models/preview.py +281 -0
- wagtail/models/reference_index.py +1 -1
- wagtail/models/revisions.py +433 -0
- wagtail/models/view_restrictions.py +3 -3
- wagtail/models/workflows.py +1348 -0
- wagtail/project_template/home/apps.py +6 -0
- wagtail/project_template/manage.py-tpl +22 -0
- wagtail/project_template/project_name/settings/base.py +1 -0
- wagtail/project_template/requirements.txt +2 -2
- wagtail/search/backends/base.py +4 -0
- wagtail/search/backends/database/mysql/query.py +8 -4
- wagtail/search/backends/elasticsearch7.py +17 -7
- wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/search/tests/elasticsearch_common_tests.py +43 -0
- wagtail/search/tests/test_backends.py +43 -13
- wagtail/search/tests/test_mysql_backend.py +22 -0
- wagtail/search/tests/test_page_search.py +2 -1
- wagtail/search/tests/test_queries.py +2 -2
- wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/action_menu.py +9 -30
- wagtail/snippets/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ar/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/az_AZ/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/be/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/be/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/bg/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/bn/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ca/LC_MESSAGES/django.po +5 -5
- wagtail/snippets/locale/cs/LC_MESSAGES/django.po +5 -4
- wagtail/snippets/locale/cy/LC_MESSAGES/django.po +5 -4
- wagtail/snippets/locale/da/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/de/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/dv/LC_MESSAGES/django.po +5 -4
- wagtail/snippets/locale/el/LC_MESSAGES/django.po +5 -4
- wagtail/snippets/locale/en/LC_MESSAGES/django.po +31 -23
- wagtail/snippets/locale/en_IN/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/es/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/es_419/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/es_VE/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/et/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/fa/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/fi/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/fr/LC_MESSAGES/django.po +13 -6
- wagtail/snippets/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/gl/LC_MESSAGES/django.po +11 -5
- wagtail/snippets/locale/he_IL/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/hi/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/hr_HR/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/hu/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/id_ID/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/is_IS/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/is_IS/LC_MESSAGES/django.po +5 -6
- wagtail/snippets/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/it/LC_MESSAGES/django.po +5 -5
- wagtail/snippets/locale/ja/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ja/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/ka/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/ko/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/lt/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/lv/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/mi/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/mn/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/my/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/nb/LC_MESSAGES/django.po +5 -4
- wagtail/snippets/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/nl/LC_MESSAGES/django.po +5 -5
- wagtail/snippets/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/pl/LC_MESSAGES/django.po +5 -5
- wagtail/snippets/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/pt_BR/LC_MESSAGES/django.po +7 -5
- wagtail/snippets/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/pt_PT/LC_MESSAGES/django.po +7 -5
- wagtail/snippets/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ro/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ru/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/sk_SK/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/sl/LC_MESSAGES/django.po +5 -5
- wagtail/snippets/locale/sr/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/sv/LC_MESSAGES/django.po +5 -5
- wagtail/snippets/locale/ta/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/tet/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/th/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/tr/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/tr_TR/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/ug/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/uk/LC_MESSAGES/django.po +6 -6
- wagtail/snippets/locale/vi/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/zh/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/zh_Hans/LC_MESSAGES/django.po +6 -5
- wagtail/snippets/locale/zh_Hant/LC_MESSAGES/django.po +4 -4
- wagtail/snippets/permissions.py +6 -4
- 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/publish.html +3 -5
- wagtail/snippets/tests/test_preview.py +62 -38
- wagtail/snippets/tests/test_snippets.py +313 -130
- wagtail/snippets/tests/test_viewset.py +69 -6
- wagtail/snippets/tests/test_workflows.py +0 -4
- wagtail/snippets/views/snippets.py +29 -53
- wagtail/snippets/wagtail_hooks.py +3 -7
- wagtail/test/apps.py +5 -0
- wagtail/test/context_processors.py +19 -0
- wagtail/test/customuser/forms.py +19 -0
- wagtail/test/customuser/viewsets.py +12 -0
- wagtail/test/settings.py +8 -9
- wagtail/test/testapp/migrations/0046_alter_custom_rendition_to_unique_constraint.py +24 -0
- wagtail/test/testapp/migrations/{0046_advertwithcustomuuidprimarykey_page.py → 0047_advertwithcustomuuidprimarykey_page.py} +1 -1
- wagtail/test/testapp/migrations/0048_requireddatepage.py +36 -0
- wagtail/test/testapp/migrations/0049_promotionalpage.py +35 -0
- wagtail/test/testapp/migrations/0050_headcountrelatedmodelusingpk_related_page.py +25 -0
- wagtail/test/testapp/migrations/0051_userapprovaltaskstate_userapprovaltask.py +59 -0
- wagtail/test/testapp/models.py +183 -26
- wagtail/test/testapp/templates/tests/workflows/approve_with_style.html +6 -0
- wagtail/test/testapp/views.py +7 -1
- wagtail/test/testapp/wagtail_hooks.py +6 -0
- wagtail/test/utils/form_data.py +1 -0
- wagtail/tests/test_blocks.py +0 -18
- wagtail/tests/test_management_commands.py +1 -1
- wagtail/tests/test_page_model.py +27 -0
- wagtail/tests/test_page_privacy.py +1 -1
- wagtail/tests/test_reference_index.py +26 -0
- wagtail/tests/test_streamfield.py +136 -0
- wagtail/tests/test_utils.py +1 -23
- wagtail/users/forms.py +3 -17
- wagtail/users/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/be/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/be/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/ca/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/cs/LC_MESSAGES/django.po +2 -1
- wagtail/users/locale/cy/LC_MESSAGES/django.po +2 -1
- wagtail/users/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/de/LC_MESSAGES/django.po +3 -3
- wagtail/users/locale/dv/LC_MESSAGES/django.po +2 -1
- wagtail/users/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/en/LC_MESSAGES/django.po +71 -71
- wagtail/users/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/fa/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/fi/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/fr/LC_MESSAGES/django.po +4 -2
- wagtail/users/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/gl/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/hr_HR/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/hu/LC_MESSAGES/django.po +45 -3
- wagtail/users/locale/hy/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/it/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/ko/LC_MESSAGES/django.po +47 -2
- wagtail/users/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/mn/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/nb/LC_MESSAGES/django.po +2 -1
- wagtail/users/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/nl/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/pl/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/pt_BR/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/pt_PT/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/ro/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/ru/LC_MESSAGES/django.po +4 -2
- wagtail/users/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/sk_SK/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/sl/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/sv/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/th/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/uk/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/vi/LC_MESSAGES/django.po +2 -1
- wagtail/users/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/zh_Hans/LC_MESSAGES/django.po +3 -2
- wagtail/users/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/users/models.py +7 -1
- wagtail/users/templatetags/wagtailusers_tags.py +0 -29
- wagtail/users/tests/__init__.py +0 -6
- wagtail/users/tests/test_admin_views.py +285 -611
- wagtail/users/views/groups.py +1 -28
- wagtail/users/views/users.py +3 -45
- wagtail/utils/deprecation.py +3 -3
- wagtail/whitelist.py +1 -0
- wagtail/workflows.py +2 -2
- wagtail-7.0rc1.dist-info/METADATA +224 -0
- {wagtail-6.4.1.dist-info → wagtail-7.0rc1.dist-info}/RECORD +1194 -1174
- {wagtail-6.4.1.dist-info → wagtail-7.0rc1.dist-info}/WHEEL +1 -1
- wagtail/admin/templates/wagtailadmin/shared/ajax_pagination_nav.html +0 -25
- wagtail/project_template/manage.py +0 -10
- wagtail/utils/setup.py +0 -90
- wagtail/utils/widgets.py +0 -46
- wagtail-6.4.1.dist-info/METADATA +0 -77
- {wagtail-6.4.1.dist-info → wagtail-7.0rc1.dist-info}/entry_points.txt +0 -0
- {wagtail-6.4.1.dist-info → wagtail-7.0rc1.dist-info/licenses}/LICENSE +0 -0
- {wagtail-6.4.1.dist-info → wagtail-7.0rc1.dist-info}/top_level.txt +0 -0
wagtail/models/pages.py
ADDED
|
@@ -0,0 +1,2754 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import posixpath
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.contrib.auth.models import Group, Permission
|
|
10
|
+
from django.contrib.contenttypes.fields import GenericRelation
|
|
11
|
+
from django.contrib.contenttypes.models import ContentType
|
|
12
|
+
from django.core import checks
|
|
13
|
+
from django.core.exceptions import (
|
|
14
|
+
FieldDoesNotExist,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
from django.db import models, transaction
|
|
18
|
+
from django.db.models import Q, Value
|
|
19
|
+
from django.db.models.expressions import Subquery
|
|
20
|
+
from django.db.models.functions import Concat, Substr
|
|
21
|
+
from django.dispatch import receiver
|
|
22
|
+
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotAllowed
|
|
23
|
+
from django.template.response import TemplateResponse
|
|
24
|
+
from django.urls import NoReverseMatch, reverse
|
|
25
|
+
from django.utils import translation as translation
|
|
26
|
+
from django.utils.encoding import force_bytes, force_str
|
|
27
|
+
from django.utils.functional import Promise, cached_property
|
|
28
|
+
from django.utils.text import capfirst, slugify
|
|
29
|
+
from django.utils.translation import gettext_lazy as _
|
|
30
|
+
from modelcluster.fields import ParentalKey
|
|
31
|
+
from modelcluster.models import (
|
|
32
|
+
ClusterableModel,
|
|
33
|
+
)
|
|
34
|
+
from treebeard.mp_tree import MP_Node
|
|
35
|
+
|
|
36
|
+
from wagtail.actions.copy_for_translation import CopyPageForTranslationAction
|
|
37
|
+
from wagtail.actions.copy_page import CopyPageAction
|
|
38
|
+
from wagtail.actions.create_alias import CreatePageAliasAction
|
|
39
|
+
from wagtail.actions.delete_page import DeletePageAction
|
|
40
|
+
from wagtail.actions.move_page import MovePageAction
|
|
41
|
+
from wagtail.actions.publish_page_revision import PublishPageRevisionAction
|
|
42
|
+
from wagtail.actions.unpublish_page import UnpublishPageAction
|
|
43
|
+
from wagtail.compat import HTTPMethod
|
|
44
|
+
from wagtail.coreutils import (
|
|
45
|
+
WAGTAIL_APPEND_SLASH,
|
|
46
|
+
camelcase_to_underscore,
|
|
47
|
+
get_supported_content_language_variant,
|
|
48
|
+
resolve_model_string,
|
|
49
|
+
safe_md5,
|
|
50
|
+
)
|
|
51
|
+
from wagtail.fields import StreamField
|
|
52
|
+
from wagtail.log_actions import log
|
|
53
|
+
from wagtail.query import PageQuerySet
|
|
54
|
+
from wagtail.search import index
|
|
55
|
+
from wagtail.signals import (
|
|
56
|
+
page_published,
|
|
57
|
+
page_slug_changed,
|
|
58
|
+
pre_validate_delete,
|
|
59
|
+
)
|
|
60
|
+
from wagtail.url_routing import RouteResult
|
|
61
|
+
from wagtail.utils.timestamps import ensure_utc
|
|
62
|
+
|
|
63
|
+
from .audit_log import BaseLogEntry, BaseLogEntryManager, LogEntryQuerySet
|
|
64
|
+
from .content_types import get_default_page_content_type
|
|
65
|
+
from .copying import _copy_m2m_relations
|
|
66
|
+
from .draft_state import DraftStateMixin
|
|
67
|
+
from .i18n import Locale, TranslatableMixin
|
|
68
|
+
from .locking import LockableMixin
|
|
69
|
+
from .panels import CommentPanelPlaceholder, PanelPlaceholder
|
|
70
|
+
from .preview import PreviewableMixin
|
|
71
|
+
from .revisions import Revision, RevisionMixin
|
|
72
|
+
from .sites import Site
|
|
73
|
+
from .specific import SpecificMixin
|
|
74
|
+
from .view_restrictions import BaseViewRestriction
|
|
75
|
+
from .workflows import WorkflowMixin
|
|
76
|
+
|
|
77
|
+
logger = logging.getLogger("wagtail")
|
|
78
|
+
|
|
79
|
+
PAGE_TEMPLATE_VAR = "page"
|
|
80
|
+
COMMENTS_RELATION_NAME = getattr(
|
|
81
|
+
settings, "WAGTAIL_COMMENTS_RELATION_NAME", "wagtail_admin_comments"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@receiver(pre_validate_delete, sender=Locale)
|
|
86
|
+
def reassign_root_page_locale_on_delete(sender, instance, **kwargs):
|
|
87
|
+
# if we're deleting the locale used on the root page node, reassign that to a new locale first
|
|
88
|
+
root_page_with_this_locale = Page.objects.filter(depth=1, locale=instance)
|
|
89
|
+
if root_page_with_this_locale.exists():
|
|
90
|
+
# Select the default locale, if one exists and isn't the one being deleted
|
|
91
|
+
try:
|
|
92
|
+
new_locale = Locale.get_default()
|
|
93
|
+
default_locale_is_ok = new_locale != instance
|
|
94
|
+
except (Locale.DoesNotExist, LookupError):
|
|
95
|
+
default_locale_is_ok = False
|
|
96
|
+
|
|
97
|
+
if not default_locale_is_ok:
|
|
98
|
+
# fall back on any remaining locale
|
|
99
|
+
new_locale = Locale.all_objects.exclude(pk=instance.pk).first()
|
|
100
|
+
|
|
101
|
+
root_page_with_this_locale.update(locale=new_locale)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
PAGE_MODEL_CLASSES = []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_page_models():
|
|
108
|
+
"""
|
|
109
|
+
Returns a list of all non-abstract Page model classes defined in this project.
|
|
110
|
+
"""
|
|
111
|
+
return PAGE_MODEL_CLASSES.copy()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_page_content_types(include_base_page_type=True):
|
|
115
|
+
"""
|
|
116
|
+
Returns a queryset of all ContentType objects corresponding to Page model classes.
|
|
117
|
+
"""
|
|
118
|
+
models = get_page_models()
|
|
119
|
+
if not include_base_page_type:
|
|
120
|
+
models.remove(Page)
|
|
121
|
+
|
|
122
|
+
content_type_ids = [
|
|
123
|
+
ct.pk for ct in ContentType.objects.get_for_models(*models).values()
|
|
124
|
+
]
|
|
125
|
+
return ContentType.objects.filter(pk__in=content_type_ids).order_by("model")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@functools.cache
|
|
129
|
+
def get_streamfield_names(model_class):
|
|
130
|
+
return tuple(
|
|
131
|
+
field.name
|
|
132
|
+
for field in model_class._meta.concrete_fields
|
|
133
|
+
if isinstance(field, StreamField)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class BasePageManager(models.Manager):
|
|
138
|
+
def get_queryset(self):
|
|
139
|
+
return self._queryset_class(self.model).order_by("path")
|
|
140
|
+
|
|
141
|
+
def first_common_ancestor_of(self, pages, include_self=False, strict=False):
|
|
142
|
+
"""
|
|
143
|
+
This is similar to ``PageQuerySet.first_common_ancestor`` but works
|
|
144
|
+
for a list of pages instead of a queryset.
|
|
145
|
+
"""
|
|
146
|
+
if not pages:
|
|
147
|
+
if strict:
|
|
148
|
+
raise self.model.DoesNotExist("Can not find ancestor of empty list")
|
|
149
|
+
return self.model.get_first_root_node()
|
|
150
|
+
|
|
151
|
+
if include_self:
|
|
152
|
+
paths = list({page.path for page in pages})
|
|
153
|
+
else:
|
|
154
|
+
paths = list({page.path[: -self.model.steplen] for page in pages})
|
|
155
|
+
|
|
156
|
+
# This method works on anything, not just file system paths.
|
|
157
|
+
common_parent_path = posixpath.commonprefix(paths)
|
|
158
|
+
extra_chars = len(common_parent_path) % self.model.steplen
|
|
159
|
+
if extra_chars != 0:
|
|
160
|
+
common_parent_path = common_parent_path[:-extra_chars]
|
|
161
|
+
|
|
162
|
+
if common_parent_path == "":
|
|
163
|
+
if strict:
|
|
164
|
+
raise self.model.DoesNotExist("No common ancestor found!")
|
|
165
|
+
|
|
166
|
+
return self.model.get_first_root_node()
|
|
167
|
+
|
|
168
|
+
return self.get(path=common_parent_path)
|
|
169
|
+
|
|
170
|
+
def annotate_parent_page(self, pages):
|
|
171
|
+
"""
|
|
172
|
+
Annotates each page with its parent page. This is implemented as a
|
|
173
|
+
manager-only method instead of a QuerySet method so it can be used with
|
|
174
|
+
search results.
|
|
175
|
+
|
|
176
|
+
If given a QuerySet, this method will evaluate it. Only use this method
|
|
177
|
+
when you are ready to consume the queryset, e.g. after pagination has
|
|
178
|
+
been applied. This is typically done in the view's ``get_context_data``
|
|
179
|
+
using ``context["object_list"]``.
|
|
180
|
+
|
|
181
|
+
This method does not return a new queryset, but modifies the existing one,
|
|
182
|
+
to ensure any references to the queryset in the view's context are updated
|
|
183
|
+
(e.g. when using ``context_object_name``).
|
|
184
|
+
"""
|
|
185
|
+
parent_page_paths = {
|
|
186
|
+
Page._get_parent_path_from_path(page.path) for page in pages
|
|
187
|
+
}
|
|
188
|
+
parent_pages_by_path = {
|
|
189
|
+
page.path: page
|
|
190
|
+
for page in Page.objects.filter(path__in=parent_page_paths).specific(
|
|
191
|
+
defer=True
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
for page in pages:
|
|
195
|
+
parent_page = parent_pages_by_path.get(
|
|
196
|
+
Page._get_parent_path_from_path(page.path)
|
|
197
|
+
)
|
|
198
|
+
page._parent_page = parent_page
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
PageManager = BasePageManager.from_queryset(PageQuerySet)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class PageBase(models.base.ModelBase):
|
|
205
|
+
"""Metaclass for Page"""
|
|
206
|
+
|
|
207
|
+
def __init__(cls, name, bases, dct):
|
|
208
|
+
super().__init__(name, bases, dct)
|
|
209
|
+
|
|
210
|
+
if "template" not in dct:
|
|
211
|
+
# Define a default template path derived from the app name and model name
|
|
212
|
+
cls.template = "{}/{}.html".format(
|
|
213
|
+
cls._meta.app_label,
|
|
214
|
+
camelcase_to_underscore(name),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if "ajax_template" not in dct:
|
|
218
|
+
cls.ajax_template = None
|
|
219
|
+
|
|
220
|
+
cls._clean_subpage_models = (
|
|
221
|
+
None # to be filled in on first call to cls.clean_subpage_models
|
|
222
|
+
)
|
|
223
|
+
cls._clean_parent_page_models = (
|
|
224
|
+
None # to be filled in on first call to cls.clean_parent_page_models
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# All pages should be creatable unless explicitly set otherwise.
|
|
228
|
+
# This attribute is not inheritable.
|
|
229
|
+
if "is_creatable" not in dct:
|
|
230
|
+
cls.is_creatable = not cls._meta.abstract
|
|
231
|
+
|
|
232
|
+
if not cls._meta.abstract:
|
|
233
|
+
# register this type in the list of page content types
|
|
234
|
+
PAGE_MODEL_CLASSES.append(cls)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class AbstractPage(
|
|
238
|
+
WorkflowMixin,
|
|
239
|
+
PreviewableMixin,
|
|
240
|
+
DraftStateMixin,
|
|
241
|
+
LockableMixin,
|
|
242
|
+
RevisionMixin,
|
|
243
|
+
TranslatableMixin,
|
|
244
|
+
SpecificMixin,
|
|
245
|
+
MP_Node,
|
|
246
|
+
):
|
|
247
|
+
"""
|
|
248
|
+
Abstract superclass for Page. According to Django's inheritance rules, managers set on
|
|
249
|
+
abstract models are inherited by subclasses, but managers set on concrete models that are extended
|
|
250
|
+
via multi-table inheritance are not. We therefore need to attach PageManager to an abstract
|
|
251
|
+
superclass to ensure that it is retained by subclasses of Page.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
objects = PageManager()
|
|
255
|
+
|
|
256
|
+
class Meta:
|
|
257
|
+
abstract = True
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Make sure that this list is sorted by the codename (first item in the tuple)
|
|
261
|
+
# so that we can follow the same order when querying the Permission objects.
|
|
262
|
+
PAGE_PERMISSION_TYPES = [
|
|
263
|
+
("add_page", _("Add"), _("Add/edit pages you own")),
|
|
264
|
+
("bulk_delete_page", _("Bulk delete"), _("Delete pages with children")),
|
|
265
|
+
("change_page", _("Edit"), _("Edit any page")),
|
|
266
|
+
("lock_page", _("Lock"), _("Lock/unlock pages you've locked")),
|
|
267
|
+
("publish_page", _("Publish"), _("Publish any page")),
|
|
268
|
+
("unlock_page", _("Unlock"), _("Unlock any page")),
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
PAGE_PERMISSION_TYPE_CHOICES = [
|
|
272
|
+
(identifier[:-5], long_label) for identifier, _, long_label in PAGE_PERMISSION_TYPES
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
PAGE_PERMISSION_CODENAMES = [identifier for identifier, *_ in PAGE_PERMISSION_TYPES]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
|
|
279
|
+
title = models.CharField(
|
|
280
|
+
verbose_name=_("title"),
|
|
281
|
+
max_length=255,
|
|
282
|
+
help_text=_("The page title as you'd like it to be seen by the public"),
|
|
283
|
+
)
|
|
284
|
+
title.required_on_save = True
|
|
285
|
+
# to reflect title of a current draft in the admin UI
|
|
286
|
+
draft_title = models.CharField(max_length=255, editable=False)
|
|
287
|
+
slug = models.SlugField(
|
|
288
|
+
verbose_name=_("slug"),
|
|
289
|
+
allow_unicode=True,
|
|
290
|
+
max_length=255,
|
|
291
|
+
help_text=_(
|
|
292
|
+
"The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/"
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
content_type = models.ForeignKey(
|
|
296
|
+
ContentType,
|
|
297
|
+
verbose_name=_("content type"),
|
|
298
|
+
related_name="pages",
|
|
299
|
+
on_delete=models.SET(get_default_page_content_type),
|
|
300
|
+
)
|
|
301
|
+
content_type.wagtail_reference_index_ignore = True
|
|
302
|
+
url_path = models.TextField(verbose_name=_("URL path"), blank=True, editable=False)
|
|
303
|
+
owner = models.ForeignKey(
|
|
304
|
+
settings.AUTH_USER_MODEL,
|
|
305
|
+
verbose_name=_("owner"),
|
|
306
|
+
null=True,
|
|
307
|
+
blank=True,
|
|
308
|
+
editable=True,
|
|
309
|
+
on_delete=models.SET_NULL,
|
|
310
|
+
related_name="owned_pages",
|
|
311
|
+
)
|
|
312
|
+
owner.wagtail_reference_index_ignore = True
|
|
313
|
+
|
|
314
|
+
seo_title = models.CharField(
|
|
315
|
+
verbose_name=_("title tag"),
|
|
316
|
+
max_length=255,
|
|
317
|
+
blank=True,
|
|
318
|
+
help_text=_(
|
|
319
|
+
"The name of the page displayed on search engine results as the clickable headline."
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
show_in_menus_default = False
|
|
324
|
+
show_in_menus = models.BooleanField(
|
|
325
|
+
verbose_name=_("show in menus"),
|
|
326
|
+
default=False,
|
|
327
|
+
help_text=_(
|
|
328
|
+
"Whether a link to this page will appear in automatically generated menus"
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
search_description = models.TextField(
|
|
332
|
+
verbose_name=_("meta description"),
|
|
333
|
+
blank=True,
|
|
334
|
+
help_text=_(
|
|
335
|
+
"The descriptive text displayed underneath a headline in search engine results."
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
latest_revision_created_at = models.DateTimeField(
|
|
340
|
+
verbose_name=_("latest revision created at"), null=True, editable=False
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
_revisions = GenericRelation("wagtailcore.Revision", related_query_name="page")
|
|
344
|
+
|
|
345
|
+
# Add GenericRelation to allow WorkflowState.objects.filter(page=...) queries.
|
|
346
|
+
# There is no need to override the workflow_states property, as the default
|
|
347
|
+
# implementation in WorkflowMixin already ensures that the queryset uses the
|
|
348
|
+
# base Page content type.
|
|
349
|
+
_workflow_states = GenericRelation(
|
|
350
|
+
"wagtailcore.WorkflowState",
|
|
351
|
+
content_type_field="base_content_type",
|
|
352
|
+
object_id_field="object_id",
|
|
353
|
+
related_query_name="page",
|
|
354
|
+
for_concrete_model=False,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# When using a specific queryset, accessing the _workflow_states GenericRelation
|
|
358
|
+
# will yield no results. This is because the _workflow_states GenericRelation
|
|
359
|
+
# uses the base_content_type as the content_type_field, which is not the same
|
|
360
|
+
# as the content type of the specific queryset. To work around this, we define
|
|
361
|
+
# a second GenericRelation that uses the specific content_type to be used
|
|
362
|
+
# when working with specific querysets.
|
|
363
|
+
_specific_workflow_states = GenericRelation(
|
|
364
|
+
"wagtailcore.WorkflowState",
|
|
365
|
+
content_type_field="content_type",
|
|
366
|
+
object_id_field="object_id",
|
|
367
|
+
related_query_name="page",
|
|
368
|
+
for_concrete_model=False,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# If non-null, this page is an alias of the linked page
|
|
372
|
+
# This means the page is kept in sync with the live version
|
|
373
|
+
# of the linked pages and is not editable by users.
|
|
374
|
+
alias_of = models.ForeignKey(
|
|
375
|
+
"self",
|
|
376
|
+
on_delete=models.SET_NULL,
|
|
377
|
+
null=True,
|
|
378
|
+
blank=True,
|
|
379
|
+
editable=False,
|
|
380
|
+
related_name="aliases",
|
|
381
|
+
)
|
|
382
|
+
alias_of.wagtail_reference_index_ignore = True
|
|
383
|
+
|
|
384
|
+
search_fields = [
|
|
385
|
+
index.SearchField("title", boost=2),
|
|
386
|
+
index.AutocompleteField("title"),
|
|
387
|
+
index.FilterField("title"),
|
|
388
|
+
index.FilterField("id"),
|
|
389
|
+
index.FilterField("live"),
|
|
390
|
+
index.FilterField("owner"),
|
|
391
|
+
index.FilterField("content_type"),
|
|
392
|
+
index.FilterField("path"),
|
|
393
|
+
index.FilterField("depth"),
|
|
394
|
+
index.FilterField("locked"),
|
|
395
|
+
index.FilterField("show_in_menus"),
|
|
396
|
+
index.FilterField("first_published_at"),
|
|
397
|
+
index.FilterField("last_published_at"),
|
|
398
|
+
index.FilterField("latest_revision_created_at"),
|
|
399
|
+
index.FilterField("locale"),
|
|
400
|
+
index.FilterField("translation_key"),
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
# Do not allow plain Page instances to be created through the Wagtail admin
|
|
404
|
+
is_creatable = False
|
|
405
|
+
|
|
406
|
+
# Define the maximum number of instances this page type can have. Default to unlimited.
|
|
407
|
+
max_count = None
|
|
408
|
+
|
|
409
|
+
# Define the maximum number of instances this page can have under a specific parent. Default to unlimited.
|
|
410
|
+
max_count_per_parent = None
|
|
411
|
+
|
|
412
|
+
# Set the default order for child pages to be shown in the Page index listing
|
|
413
|
+
admin_default_ordering = "-latest_revision_created_at"
|
|
414
|
+
|
|
415
|
+
# An array of additional field names that will not be included when a Page is copied.
|
|
416
|
+
exclude_fields_in_copy = []
|
|
417
|
+
default_exclude_fields_in_copy = [
|
|
418
|
+
"id",
|
|
419
|
+
"depth",
|
|
420
|
+
"numchild",
|
|
421
|
+
"url_path",
|
|
422
|
+
"path",
|
|
423
|
+
"postgres_index_entries",
|
|
424
|
+
"index_entries",
|
|
425
|
+
"latest_revision",
|
|
426
|
+
COMMENTS_RELATION_NAME,
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
# Real panel classes are defined in wagtail.admin.panels, which we can't import here
|
|
430
|
+
# because it would create a circular import. Instead, define them with placeholders
|
|
431
|
+
# to be replaced with the real classes by `wagtail.admin.panels.model_utils.expand_panel_list`.
|
|
432
|
+
content_panels = [
|
|
433
|
+
PanelPlaceholder("wagtail.admin.panels.TitleFieldPanel", ["title"], {}),
|
|
434
|
+
]
|
|
435
|
+
promote_panels = [
|
|
436
|
+
PanelPlaceholder(
|
|
437
|
+
"wagtail.admin.panels.MultiFieldPanel",
|
|
438
|
+
[
|
|
439
|
+
[
|
|
440
|
+
"slug",
|
|
441
|
+
"seo_title",
|
|
442
|
+
"search_description",
|
|
443
|
+
],
|
|
444
|
+
_("For search engines"),
|
|
445
|
+
],
|
|
446
|
+
{},
|
|
447
|
+
),
|
|
448
|
+
PanelPlaceholder(
|
|
449
|
+
"wagtail.admin.panels.MultiFieldPanel",
|
|
450
|
+
[
|
|
451
|
+
[
|
|
452
|
+
"show_in_menus",
|
|
453
|
+
],
|
|
454
|
+
_("For site menus"),
|
|
455
|
+
],
|
|
456
|
+
{},
|
|
457
|
+
),
|
|
458
|
+
]
|
|
459
|
+
settings_panels = [
|
|
460
|
+
PanelPlaceholder("wagtail.admin.panels.PublishingPanel", [], {}),
|
|
461
|
+
CommentPanelPlaceholder(),
|
|
462
|
+
]
|
|
463
|
+
|
|
464
|
+
# Privacy options for page
|
|
465
|
+
private_page_options = ["password", "groups", "login"]
|
|
466
|
+
|
|
467
|
+
# Allows page types to specify a list of HTTP method names that page instances will
|
|
468
|
+
# respond to. When the request type doesn't match, Wagtail should return a response
|
|
469
|
+
# with a status code of 405.
|
|
470
|
+
allowed_http_methods = [
|
|
471
|
+
HTTPMethod.DELETE,
|
|
472
|
+
HTTPMethod.GET,
|
|
473
|
+
HTTPMethod.HEAD,
|
|
474
|
+
HTTPMethod.OPTIONS,
|
|
475
|
+
HTTPMethod.PATCH,
|
|
476
|
+
HTTPMethod.POST,
|
|
477
|
+
HTTPMethod.PUT,
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def route_for_request(request: HttpRequest, path: str) -> RouteResult | None:
|
|
482
|
+
"""
|
|
483
|
+
Find the page route for the given HTTP request object, and URL path. The route
|
|
484
|
+
result (`page`, `args`, and `kwargs`) will be cached via
|
|
485
|
+
``request._wagtail_route_for_request``.
|
|
486
|
+
"""
|
|
487
|
+
if not hasattr(request, "_wagtail_route_for_request"):
|
|
488
|
+
try:
|
|
489
|
+
# we need a valid Site object for this request in order to proceed
|
|
490
|
+
if site := Site.find_for_request(request):
|
|
491
|
+
path_components = [
|
|
492
|
+
component for component in path.split("/") if component
|
|
493
|
+
]
|
|
494
|
+
request._wagtail_route_for_request = (
|
|
495
|
+
site.root_page.localized.specific.route(
|
|
496
|
+
request, path_components
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
request._wagtail_route_for_request = None
|
|
501
|
+
except Http404:
|
|
502
|
+
# .route() can raise Http404
|
|
503
|
+
request._wagtail_route_for_request = None
|
|
504
|
+
|
|
505
|
+
return request._wagtail_route_for_request
|
|
506
|
+
|
|
507
|
+
@staticmethod
|
|
508
|
+
def find_for_request(request: HttpRequest, path: str) -> Page | None:
|
|
509
|
+
"""
|
|
510
|
+
Find the page for the given HTTP request object, and URL path. The full
|
|
511
|
+
page route will be cached via ``request._wagtail_route_for_request``.
|
|
512
|
+
"""
|
|
513
|
+
result = Page.route_for_request(request, path)
|
|
514
|
+
if result is not None:
|
|
515
|
+
return result[0]
|
|
516
|
+
|
|
517
|
+
@classmethod
|
|
518
|
+
def allowed_http_method_names(cls):
|
|
519
|
+
return [
|
|
520
|
+
method.value if hasattr(method, "value") else method
|
|
521
|
+
for method in cls.allowed_http_methods
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
def __init__(self, *args, **kwargs):
|
|
525
|
+
super().__init__(*args, **kwargs)
|
|
526
|
+
if not self.id:
|
|
527
|
+
# this model is being newly created
|
|
528
|
+
# rather than retrieved from the db;
|
|
529
|
+
if not self.content_type_id:
|
|
530
|
+
# set content type to correctly represent the model class
|
|
531
|
+
# that this was created as
|
|
532
|
+
self.content_type = ContentType.objects.get_for_model(self)
|
|
533
|
+
if "show_in_menus" not in kwargs:
|
|
534
|
+
# if the value is not set on submit refer to the model setting
|
|
535
|
+
self.show_in_menus = self.show_in_menus_default
|
|
536
|
+
|
|
537
|
+
def __str__(self):
|
|
538
|
+
return self.title
|
|
539
|
+
|
|
540
|
+
@property
|
|
541
|
+
def revisions(self):
|
|
542
|
+
# Always use the specific page instance when querying for revisions as
|
|
543
|
+
# they are always saved with the specific content_type.
|
|
544
|
+
return self.specific_deferred._revisions
|
|
545
|
+
|
|
546
|
+
def get_base_content_type(self):
|
|
547
|
+
# We want to always use the default Page model's ContentType as the
|
|
548
|
+
# base_content_type so that we can query for page revisions without
|
|
549
|
+
# having to know the specific Page type.
|
|
550
|
+
return get_default_page_content_type()
|
|
551
|
+
|
|
552
|
+
def get_content_type(self):
|
|
553
|
+
return self.content_type
|
|
554
|
+
|
|
555
|
+
@classmethod
|
|
556
|
+
def get_streamfield_names(cls):
|
|
557
|
+
return get_streamfield_names(cls)
|
|
558
|
+
|
|
559
|
+
def set_url_path(self, parent):
|
|
560
|
+
"""
|
|
561
|
+
Populate the url_path field based on this page's slug and the specified parent page.
|
|
562
|
+
(We pass a parent in here, rather than retrieving it via get_parent, so that we can give
|
|
563
|
+
new unsaved pages a meaningful URL when previewing them; at that point the page has not
|
|
564
|
+
been assigned a position in the tree, as far as treebeard is concerned.
|
|
565
|
+
"""
|
|
566
|
+
if parent:
|
|
567
|
+
self.url_path = parent.url_path + self.slug + "/"
|
|
568
|
+
else:
|
|
569
|
+
# a page without a parent is the tree root, which always has a url_path of '/'
|
|
570
|
+
self.url_path = "/"
|
|
571
|
+
|
|
572
|
+
return self.url_path
|
|
573
|
+
|
|
574
|
+
@staticmethod
|
|
575
|
+
def _slug_is_available(slug, parent_page, page=None):
|
|
576
|
+
"""
|
|
577
|
+
Determine whether the given slug is available for use on a child page of
|
|
578
|
+
parent_page. If 'page' is passed, the slug is intended for use on that page
|
|
579
|
+
(and so it will be excluded from the duplicate check).
|
|
580
|
+
"""
|
|
581
|
+
if parent_page is None:
|
|
582
|
+
# the root page's slug can be whatever it likes...
|
|
583
|
+
return True
|
|
584
|
+
|
|
585
|
+
siblings = parent_page.get_children()
|
|
586
|
+
if page:
|
|
587
|
+
siblings = siblings.not_page(page)
|
|
588
|
+
|
|
589
|
+
return not siblings.filter(slug=slug).exists()
|
|
590
|
+
|
|
591
|
+
def _get_autogenerated_slug(self, base_slug):
|
|
592
|
+
candidate_slug = base_slug
|
|
593
|
+
suffix = 1
|
|
594
|
+
parent_page = self.get_parent()
|
|
595
|
+
|
|
596
|
+
while not Page._slug_is_available(candidate_slug, parent_page, self):
|
|
597
|
+
# try with incrementing suffix until we find a slug which is available
|
|
598
|
+
suffix += 1
|
|
599
|
+
candidate_slug = "%s-%d" % (base_slug, suffix)
|
|
600
|
+
|
|
601
|
+
return candidate_slug
|
|
602
|
+
|
|
603
|
+
def get_default_locale(self):
|
|
604
|
+
"""
|
|
605
|
+
Finds the default locale to use for this page.
|
|
606
|
+
|
|
607
|
+
This will be called just before the initial save.
|
|
608
|
+
"""
|
|
609
|
+
parent = self.get_parent()
|
|
610
|
+
if parent is not None:
|
|
611
|
+
return (
|
|
612
|
+
parent.specific_class.objects.defer()
|
|
613
|
+
.select_related("locale")
|
|
614
|
+
.get(id=parent.id)
|
|
615
|
+
.locale
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
return super().get_default_locale()
|
|
619
|
+
|
|
620
|
+
def get_admin_default_ordering(self):
|
|
621
|
+
"""
|
|
622
|
+
Determine the default ordering for child pages in the admin index listing.
|
|
623
|
+
Returns a string (e.g. 'latest_revision_created_at, title, ord' or 'live').
|
|
624
|
+
"""
|
|
625
|
+
return self.admin_default_ordering
|
|
626
|
+
|
|
627
|
+
def _set_core_field_defaults(self):
|
|
628
|
+
"""
|
|
629
|
+
Set default values for core fields (slug, draft_title, locale) that need to be
|
|
630
|
+
in place before validating or saving
|
|
631
|
+
"""
|
|
632
|
+
if not self.slug:
|
|
633
|
+
# Try to auto-populate slug from title
|
|
634
|
+
allow_unicode = getattr(settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True)
|
|
635
|
+
base_slug = slugify(self.title, allow_unicode=allow_unicode)
|
|
636
|
+
|
|
637
|
+
# only proceed if we get a non-empty base slug back from slugify
|
|
638
|
+
if base_slug:
|
|
639
|
+
self.slug = self._get_autogenerated_slug(base_slug)
|
|
640
|
+
|
|
641
|
+
if not self.draft_title:
|
|
642
|
+
self.draft_title = self.title
|
|
643
|
+
|
|
644
|
+
# Set the locale
|
|
645
|
+
if self.locale_id is None:
|
|
646
|
+
self.locale = self.get_default_locale()
|
|
647
|
+
|
|
648
|
+
def full_clean(self, *args, **kwargs):
|
|
649
|
+
self._set_core_field_defaults()
|
|
650
|
+
super().full_clean(*args, **kwargs)
|
|
651
|
+
|
|
652
|
+
def _check_slug_is_unique(self):
|
|
653
|
+
parent_page = self.get_parent()
|
|
654
|
+
if not Page._slug_is_available(self.slug, parent_page, self):
|
|
655
|
+
raise ValidationError(
|
|
656
|
+
{
|
|
657
|
+
"slug": _(
|
|
658
|
+
"The slug '%(page_slug)s' is already in use within the parent page at '%(parent_url_path)s'"
|
|
659
|
+
)
|
|
660
|
+
% {"page_slug": self.slug, "parent_url_path": parent_page.url}
|
|
661
|
+
}
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
def clean(self):
|
|
665
|
+
super().clean()
|
|
666
|
+
self._check_slug_is_unique()
|
|
667
|
+
|
|
668
|
+
def minimal_clean(self):
|
|
669
|
+
self._set_core_field_defaults()
|
|
670
|
+
self.title = self._meta.get_field("title").clean(self.title, self)
|
|
671
|
+
self._check_slug_is_unique()
|
|
672
|
+
|
|
673
|
+
def is_site_root(self):
|
|
674
|
+
"""
|
|
675
|
+
Returns True if this page is the root of any site.
|
|
676
|
+
|
|
677
|
+
This includes translations of site root pages as well.
|
|
678
|
+
"""
|
|
679
|
+
# `_is_site_root` may be populated by `annotate_site_root_state` on `PageQuerySet` as a
|
|
680
|
+
# performance optimisation
|
|
681
|
+
if hasattr(self, "_is_site_root"):
|
|
682
|
+
return self._is_site_root
|
|
683
|
+
|
|
684
|
+
return Site.objects.filter(
|
|
685
|
+
root_page__translation_key=self.translation_key
|
|
686
|
+
).exists()
|
|
687
|
+
|
|
688
|
+
@transaction.atomic
|
|
689
|
+
# ensure that changes are only committed when we have updated all descendant URL paths, to preserve consistency
|
|
690
|
+
def save(self, clean=True, user=None, log_action=False, **kwargs):
|
|
691
|
+
"""
|
|
692
|
+
Writes the page to the database, performing additional housekeeping tasks to ensure data
|
|
693
|
+
integrity:
|
|
694
|
+
|
|
695
|
+
* ``locale``, ``draft_title`` and ``slug`` are set to default values if not provided, with ``slug``
|
|
696
|
+
being generated from the title with a suffix to ensure uniqueness within the parent page
|
|
697
|
+
where necessary
|
|
698
|
+
* The ``url_path`` field is set based on the ``slug`` and the parent page
|
|
699
|
+
* If the ``slug`` has changed, the ``url_path`` of this page and all descendants is updated and
|
|
700
|
+
a :ref:`page_slug_changed` signal is sent
|
|
701
|
+
|
|
702
|
+
New pages should be saved by passing the unsaved page instance to the
|
|
703
|
+
:meth:`~treebeard.mp_tree.MP_Node.add_child`
|
|
704
|
+
or :meth:`~treebeard.mp_tree.MP_Node.add_sibling` method of an existing page, which will correctly update
|
|
705
|
+
the fields responsible for tracking the page's location in the tree.
|
|
706
|
+
|
|
707
|
+
If ``clean=False`` is passed, the page is saved without validation. This is appropriate for updates that only
|
|
708
|
+
change metadata such as `latest_revision` while keeping content and page location unchanged.
|
|
709
|
+
|
|
710
|
+
If ``clean=True`` is passed (the default), and the page has ``live=True`` set, the page is validated using
|
|
711
|
+
:meth:`~django.db.models.Model.full_clean` before saving.
|
|
712
|
+
|
|
713
|
+
If ``clean=True`` is passed, and the page has ``live=False`` set, only the title and slug fields are validated.
|
|
714
|
+
|
|
715
|
+
.. versionchanged:: 7.0
|
|
716
|
+
``clean=True`` now only performs full validation when the page is live. When the page is not live, only
|
|
717
|
+
the title and slug fields are validated. Previously, full validation was always performed.
|
|
718
|
+
"""
|
|
719
|
+
if clean:
|
|
720
|
+
if self.live:
|
|
721
|
+
self.full_clean()
|
|
722
|
+
else:
|
|
723
|
+
# Saving as draft; only perform the minimal validation to satisfy data integrity
|
|
724
|
+
self.minimal_clean()
|
|
725
|
+
|
|
726
|
+
slug_changed = False
|
|
727
|
+
is_new = self.id is None
|
|
728
|
+
|
|
729
|
+
if is_new:
|
|
730
|
+
# we are creating a record. If we're doing things properly, this should happen
|
|
731
|
+
# through a treebeard method like add_child, in which case the 'path' field
|
|
732
|
+
# has been set and so we can safely call get_parent
|
|
733
|
+
self.set_url_path(self.get_parent())
|
|
734
|
+
else:
|
|
735
|
+
# Check that we are committing the slug to the database
|
|
736
|
+
# Basically: If update_fields has been specified, and slug is not included, skip this step
|
|
737
|
+
if not (
|
|
738
|
+
"update_fields" in kwargs and "slug" not in kwargs["update_fields"]
|
|
739
|
+
):
|
|
740
|
+
# see if the slug has changed from the record in the db, in which case we need to
|
|
741
|
+
# update url_path of self and all descendants. Even though we might not need it,
|
|
742
|
+
# the specific page is fetched here for sending to the 'page_slug_changed' signal.
|
|
743
|
+
old_record = Page.objects.get(id=self.id).specific
|
|
744
|
+
if old_record.slug != self.slug:
|
|
745
|
+
self.set_url_path(self.get_parent())
|
|
746
|
+
slug_changed = True
|
|
747
|
+
old_url_path = old_record.url_path
|
|
748
|
+
new_url_path = self.url_path
|
|
749
|
+
|
|
750
|
+
result = super().save(**kwargs)
|
|
751
|
+
|
|
752
|
+
if slug_changed:
|
|
753
|
+
self._update_descendant_url_paths(old_url_path, new_url_path)
|
|
754
|
+
# Emit page_slug_changed signal on successful db commit
|
|
755
|
+
transaction.on_commit(
|
|
756
|
+
lambda: page_slug_changed.send(
|
|
757
|
+
sender=self.specific_class or self.__class__,
|
|
758
|
+
instance=self.specific,
|
|
759
|
+
instance_before=old_record,
|
|
760
|
+
)
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Check if this is a root page of any sites and clear the 'wagtail_site_root_paths' key if so
|
|
764
|
+
# Note: New translations of existing site roots are considered site roots as well, so we must
|
|
765
|
+
# always check if this page is a site root, even if it's new.
|
|
766
|
+
if self.is_site_root():
|
|
767
|
+
Site.clear_site_root_paths_cache()
|
|
768
|
+
|
|
769
|
+
# Log
|
|
770
|
+
if is_new:
|
|
771
|
+
cls = type(self)
|
|
772
|
+
logger.info(
|
|
773
|
+
'Page created: "%s" id=%d content_type=%s.%s path=%s',
|
|
774
|
+
self.title,
|
|
775
|
+
self.id,
|
|
776
|
+
cls._meta.app_label,
|
|
777
|
+
cls.__name__,
|
|
778
|
+
self.url_path,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
if log_action is not None:
|
|
782
|
+
# The default for log_action is False. i.e. don't log unless specifically instructed
|
|
783
|
+
# Page creation is a special case that we want logged by default, but allow skipping it
|
|
784
|
+
# explicitly by passing log_action=None
|
|
785
|
+
if is_new:
|
|
786
|
+
log(
|
|
787
|
+
instance=self,
|
|
788
|
+
action="wagtail.create",
|
|
789
|
+
user=user or self.owner,
|
|
790
|
+
content_changed=True,
|
|
791
|
+
)
|
|
792
|
+
elif log_action:
|
|
793
|
+
log(instance=self, action=log_action, user=user)
|
|
794
|
+
|
|
795
|
+
return result
|
|
796
|
+
|
|
797
|
+
def delete(self, *args, **kwargs):
|
|
798
|
+
user = kwargs.pop("user", None)
|
|
799
|
+
return DeletePageAction(self, user=user).execute(*args, **kwargs)
|
|
800
|
+
|
|
801
|
+
@classmethod
|
|
802
|
+
def check(cls, **kwargs):
|
|
803
|
+
errors = super().check(**kwargs)
|
|
804
|
+
|
|
805
|
+
# Check that foreign keys from pages are not configured to cascade
|
|
806
|
+
# This is the default Django behaviour which must be explicitly overridden
|
|
807
|
+
# to prevent pages disappearing unexpectedly and the tree being corrupted
|
|
808
|
+
|
|
809
|
+
# get names of foreign keys pointing to parent classes (such as page_ptr)
|
|
810
|
+
field_exceptions = [
|
|
811
|
+
field.name
|
|
812
|
+
for model in [cls] + list(cls._meta.get_parent_list())
|
|
813
|
+
for field in model._meta.parents.values()
|
|
814
|
+
if field
|
|
815
|
+
]
|
|
816
|
+
|
|
817
|
+
for field in cls._meta.fields:
|
|
818
|
+
if (
|
|
819
|
+
isinstance(field, models.ForeignKey)
|
|
820
|
+
and field.name not in field_exceptions
|
|
821
|
+
):
|
|
822
|
+
if field.remote_field.on_delete == models.CASCADE:
|
|
823
|
+
errors.append(
|
|
824
|
+
checks.Warning(
|
|
825
|
+
"Field hasn't specified on_delete action",
|
|
826
|
+
hint="Set on_delete=models.SET_NULL and make sure the field is nullable or set on_delete=models.PROTECT. Wagtail does not allow simple database CASCADE because it will corrupt its tree storage.",
|
|
827
|
+
obj=field,
|
|
828
|
+
id="wagtailcore.W001",
|
|
829
|
+
)
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
if not isinstance(cls.objects, PageManager):
|
|
833
|
+
errors.append(
|
|
834
|
+
checks.Error(
|
|
835
|
+
"Manager does not inherit from PageManager",
|
|
836
|
+
hint="Ensure that custom Page managers inherit from wagtail.models.PageManager",
|
|
837
|
+
obj=cls,
|
|
838
|
+
id="wagtailcore.E002",
|
|
839
|
+
)
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
try:
|
|
843
|
+
cls.clean_subpage_models()
|
|
844
|
+
except (ValueError, LookupError) as e:
|
|
845
|
+
errors.append(
|
|
846
|
+
checks.Error(
|
|
847
|
+
"Invalid subpage_types setting for %s" % cls,
|
|
848
|
+
hint=str(e),
|
|
849
|
+
id="wagtailcore.E002",
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
try:
|
|
854
|
+
cls.clean_parent_page_models()
|
|
855
|
+
except (ValueError, LookupError) as e:
|
|
856
|
+
errors.append(
|
|
857
|
+
checks.Error(
|
|
858
|
+
"Invalid parent_page_types setting for %s" % cls,
|
|
859
|
+
hint=str(e),
|
|
860
|
+
id="wagtailcore.E002",
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
return errors
|
|
865
|
+
|
|
866
|
+
def _update_descendant_url_paths(self, old_url_path, new_url_path):
|
|
867
|
+
(
|
|
868
|
+
Page.objects.filter(path__startswith=self.path)
|
|
869
|
+
.exclude(pk=self.pk)
|
|
870
|
+
.update(
|
|
871
|
+
url_path=Concat(
|
|
872
|
+
Value(new_url_path), Substr("url_path", len(old_url_path) + 1)
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
@property
|
|
878
|
+
def page_type_display_name(self):
|
|
879
|
+
"""
|
|
880
|
+
A human-readable version of this page's type.
|
|
881
|
+
"""
|
|
882
|
+
if not self.specific_class or self.is_root():
|
|
883
|
+
return ""
|
|
884
|
+
else:
|
|
885
|
+
return self.specific_class.get_verbose_name()
|
|
886
|
+
|
|
887
|
+
def route(self, request, path_components):
|
|
888
|
+
if path_components:
|
|
889
|
+
# request is for a child of this page
|
|
890
|
+
child_slug = path_components[0]
|
|
891
|
+
remaining_components = path_components[1:]
|
|
892
|
+
|
|
893
|
+
try:
|
|
894
|
+
subpage = self.get_children().get(slug=child_slug)
|
|
895
|
+
# Cache the parent page on the subpage to avoid another db query
|
|
896
|
+
# Treebeard's get_parent will use the `_cached_parent_obj` attribute if it exists
|
|
897
|
+
# And update = False
|
|
898
|
+
setattr(subpage, "_cached_parent_obj", self)
|
|
899
|
+
|
|
900
|
+
except Page.DoesNotExist:
|
|
901
|
+
raise Http404
|
|
902
|
+
|
|
903
|
+
return subpage.specific.route(request, remaining_components)
|
|
904
|
+
|
|
905
|
+
else:
|
|
906
|
+
# request is for this very page
|
|
907
|
+
if self.live:
|
|
908
|
+
return RouteResult(self)
|
|
909
|
+
else:
|
|
910
|
+
raise Http404
|
|
911
|
+
|
|
912
|
+
def get_admin_display_title(self):
|
|
913
|
+
"""
|
|
914
|
+
Return the title for this page as it should appear in the admin backend;
|
|
915
|
+
override this if you wish to display extra contextual information about the page,
|
|
916
|
+
such as language. By default, returns ``draft_title``.
|
|
917
|
+
"""
|
|
918
|
+
# Fall back on title if draft_title is blank (which may happen if the page was created
|
|
919
|
+
# in a fixture or migration that didn't explicitly handle draft_title)
|
|
920
|
+
return self.draft_title or self.title
|
|
921
|
+
|
|
922
|
+
def save_revision(
|
|
923
|
+
self,
|
|
924
|
+
user=None,
|
|
925
|
+
approved_go_live_at=None,
|
|
926
|
+
changed=True,
|
|
927
|
+
log_action=False,
|
|
928
|
+
previous_revision=None,
|
|
929
|
+
clean=True,
|
|
930
|
+
):
|
|
931
|
+
# Raise error if this is not the specific version of the page
|
|
932
|
+
if not isinstance(self, self.specific_class):
|
|
933
|
+
raise RuntimeError(
|
|
934
|
+
"page.save_revision() must be called on the specific version of the page. "
|
|
935
|
+
"Call page.specific.save_revision() instead."
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
# Raise an error if this page is an alias.
|
|
939
|
+
if self.alias_of_id:
|
|
940
|
+
raise RuntimeError(
|
|
941
|
+
"page.save_revision() was called on an alias page. "
|
|
942
|
+
"Revisions are not required for alias pages as they are an exact copy of another page."
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
if clean:
|
|
946
|
+
self.full_clean()
|
|
947
|
+
|
|
948
|
+
new_comments = getattr(self, COMMENTS_RELATION_NAME).filter(pk__isnull=True)
|
|
949
|
+
for comment in new_comments:
|
|
950
|
+
# We need to ensure comments have an id in the revision, so positions can be identified correctly
|
|
951
|
+
comment.save()
|
|
952
|
+
|
|
953
|
+
revision = Revision.objects.create(
|
|
954
|
+
content_object=self,
|
|
955
|
+
base_content_type=self.get_base_content_type(),
|
|
956
|
+
user=user,
|
|
957
|
+
approved_go_live_at=approved_go_live_at,
|
|
958
|
+
content=self.serializable_data(),
|
|
959
|
+
object_str=str(self),
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
for comment in new_comments:
|
|
963
|
+
comment.revision_created = revision
|
|
964
|
+
|
|
965
|
+
self.latest_revision_created_at = revision.created_at
|
|
966
|
+
self.draft_title = self.title
|
|
967
|
+
self.latest_revision = revision
|
|
968
|
+
|
|
969
|
+
update_fields = [
|
|
970
|
+
COMMENTS_RELATION_NAME,
|
|
971
|
+
"latest_revision_created_at",
|
|
972
|
+
"draft_title",
|
|
973
|
+
"latest_revision",
|
|
974
|
+
]
|
|
975
|
+
|
|
976
|
+
if changed:
|
|
977
|
+
self.has_unpublished_changes = True
|
|
978
|
+
update_fields.append("has_unpublished_changes")
|
|
979
|
+
|
|
980
|
+
if update_fields:
|
|
981
|
+
# clean=False because the fields we're updating don't need validation
|
|
982
|
+
self.save(update_fields=update_fields, clean=False)
|
|
983
|
+
|
|
984
|
+
# Log
|
|
985
|
+
logger.info(
|
|
986
|
+
'Page edited: "%s" id=%d revision_id=%d', self.title, self.id, revision.id
|
|
987
|
+
)
|
|
988
|
+
if log_action:
|
|
989
|
+
if not previous_revision:
|
|
990
|
+
log(
|
|
991
|
+
instance=self,
|
|
992
|
+
action=log_action
|
|
993
|
+
if isinstance(log_action, str)
|
|
994
|
+
else "wagtail.edit",
|
|
995
|
+
user=user,
|
|
996
|
+
revision=revision,
|
|
997
|
+
content_changed=changed,
|
|
998
|
+
)
|
|
999
|
+
else:
|
|
1000
|
+
log(
|
|
1001
|
+
instance=self,
|
|
1002
|
+
action=log_action
|
|
1003
|
+
if isinstance(log_action, str)
|
|
1004
|
+
else "wagtail.revert",
|
|
1005
|
+
user=user,
|
|
1006
|
+
data={
|
|
1007
|
+
"revision": {
|
|
1008
|
+
"id": previous_revision.id,
|
|
1009
|
+
"created": ensure_utc(previous_revision.created_at),
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
revision=revision,
|
|
1013
|
+
content_changed=changed,
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
return revision
|
|
1017
|
+
|
|
1018
|
+
def get_latest_revision_as_object(self):
|
|
1019
|
+
if not self.has_unpublished_changes:
|
|
1020
|
+
# Use the live database copy in preference to the revision record, as:
|
|
1021
|
+
# 1) this will pick up any changes that have been made directly to the model,
|
|
1022
|
+
# such as automated data imports;
|
|
1023
|
+
# 2) it ensures that inline child objects pick up real database IDs even if
|
|
1024
|
+
# those are absent from the revision data. (If this wasn't the case, the child
|
|
1025
|
+
# objects would be recreated with new IDs on next publish - see #1853)
|
|
1026
|
+
return self.specific
|
|
1027
|
+
|
|
1028
|
+
latest_revision = self.get_latest_revision()
|
|
1029
|
+
|
|
1030
|
+
if latest_revision:
|
|
1031
|
+
return latest_revision.as_object()
|
|
1032
|
+
else:
|
|
1033
|
+
return self.specific
|
|
1034
|
+
|
|
1035
|
+
def update_aliases(self, *, revision=None, _content=None, _updated_ids=None):
|
|
1036
|
+
"""
|
|
1037
|
+
Publishes all aliases that follow this page with the latest content from this page.
|
|
1038
|
+
|
|
1039
|
+
This is called by Wagtail whenever a page with aliases is published.
|
|
1040
|
+
|
|
1041
|
+
:param revision: The revision of the original page that we are updating to (used for logging purposes)
|
|
1042
|
+
:type revision: Revision, Optional
|
|
1043
|
+
"""
|
|
1044
|
+
specific_self = self.specific
|
|
1045
|
+
|
|
1046
|
+
# Only compute this if necessary since it's quite a heavy operation
|
|
1047
|
+
if _content is None:
|
|
1048
|
+
_content = self.serializable_data()
|
|
1049
|
+
|
|
1050
|
+
# A list of IDs that have already been updated. This is just in case someone has
|
|
1051
|
+
# created an alias loop (which is impossible to do with the UI Wagtail provides)
|
|
1052
|
+
_updated_ids = _updated_ids or []
|
|
1053
|
+
|
|
1054
|
+
for alias in self.specific_class.objects.filter(alias_of=self).exclude(
|
|
1055
|
+
id__in=_updated_ids
|
|
1056
|
+
):
|
|
1057
|
+
# FIXME: Switch to the same fields that are excluded from copy
|
|
1058
|
+
# We can't do this right now because we can't exclude fields from with_content_json
|
|
1059
|
+
exclude_fields = [
|
|
1060
|
+
"id",
|
|
1061
|
+
"path",
|
|
1062
|
+
"depth",
|
|
1063
|
+
"numchild",
|
|
1064
|
+
"url_path",
|
|
1065
|
+
"path",
|
|
1066
|
+
"index_entries",
|
|
1067
|
+
"postgres_index_entries",
|
|
1068
|
+
]
|
|
1069
|
+
|
|
1070
|
+
# Copy field content
|
|
1071
|
+
alias_updated = alias.with_content_json(_content)
|
|
1072
|
+
|
|
1073
|
+
# Publish the alias if it's currently in draft
|
|
1074
|
+
alias_updated.live = True
|
|
1075
|
+
alias_updated.has_unpublished_changes = False
|
|
1076
|
+
|
|
1077
|
+
# Copy child relations
|
|
1078
|
+
child_object_map = specific_self.copy_all_child_relations(
|
|
1079
|
+
target=alias_updated, exclude=exclude_fields
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
# Process child objects
|
|
1083
|
+
# This has two jobs:
|
|
1084
|
+
# - If the alias is in a different locale, this updates the
|
|
1085
|
+
# locale of any translatable child objects to match
|
|
1086
|
+
# - If the alias is not a translation of the original, this
|
|
1087
|
+
# changes the translation_key field of all child objects
|
|
1088
|
+
# so they do not clash
|
|
1089
|
+
if child_object_map:
|
|
1090
|
+
alias_is_translation = alias.translation_key == self.translation_key
|
|
1091
|
+
|
|
1092
|
+
def process_child_object(child_object):
|
|
1093
|
+
if isinstance(child_object, TranslatableMixin):
|
|
1094
|
+
# Child object's locale must always match the page
|
|
1095
|
+
child_object.locale = alias_updated.locale
|
|
1096
|
+
|
|
1097
|
+
# If the alias isn't a translation of the original page,
|
|
1098
|
+
# change the child object's translation_keys so they are
|
|
1099
|
+
# not either
|
|
1100
|
+
if not alias_is_translation:
|
|
1101
|
+
child_object.translation_key = uuid.uuid4()
|
|
1102
|
+
|
|
1103
|
+
for (rel, previous_id), child_objects in child_object_map.items():
|
|
1104
|
+
if previous_id is None:
|
|
1105
|
+
for child_object in child_objects:
|
|
1106
|
+
process_child_object(child_object)
|
|
1107
|
+
else:
|
|
1108
|
+
process_child_object(child_objects)
|
|
1109
|
+
|
|
1110
|
+
# Copy M2M relations
|
|
1111
|
+
_copy_m2m_relations(
|
|
1112
|
+
specific_self, alias_updated, exclude_fields=exclude_fields
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
# Don't change the aliases slug
|
|
1116
|
+
# Aliases can have their own slugs so they can be siblings of the original
|
|
1117
|
+
alias_updated.slug = alias.slug
|
|
1118
|
+
alias_updated.set_url_path(alias_updated.get_parent())
|
|
1119
|
+
|
|
1120
|
+
# Aliases don't have revisions, so update fields that would normally be updated by save_revision
|
|
1121
|
+
alias_updated.draft_title = alias_updated.title
|
|
1122
|
+
alias_updated.latest_revision_created_at = self.latest_revision_created_at
|
|
1123
|
+
|
|
1124
|
+
alias_updated.save(clean=False)
|
|
1125
|
+
|
|
1126
|
+
page_published.send(
|
|
1127
|
+
sender=alias_updated.specific_class,
|
|
1128
|
+
instance=alias_updated,
|
|
1129
|
+
revision=revision,
|
|
1130
|
+
alias=True,
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
# Update any aliases of that alias
|
|
1134
|
+
|
|
1135
|
+
# Design note:
|
|
1136
|
+
# It could be argued that this will be faster if we just changed these alias-of-alias
|
|
1137
|
+
# pages to all point to the original page and avoid having to update them recursively.
|
|
1138
|
+
#
|
|
1139
|
+
# But, it's useful to have a record of how aliases have been chained.
|
|
1140
|
+
# For example, In Wagtail Localize, we use aliases to create mirrored trees, but those
|
|
1141
|
+
# trees themselves could have aliases within them. If an alias within a tree is
|
|
1142
|
+
# converted to a regular page, we want the alias in the mirrored tree to follow that
|
|
1143
|
+
# new page and stop receiving updates from the original page.
|
|
1144
|
+
#
|
|
1145
|
+
# Doing it this way requires an extra lookup query per alias but this is small in
|
|
1146
|
+
# comparison to the work required to update the alias.
|
|
1147
|
+
|
|
1148
|
+
alias.update_aliases(
|
|
1149
|
+
revision=revision,
|
|
1150
|
+
_content=_content,
|
|
1151
|
+
_updated_ids=_updated_ids,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
update_aliases.alters_data = True
|
|
1155
|
+
|
|
1156
|
+
def publish(
|
|
1157
|
+
self,
|
|
1158
|
+
revision,
|
|
1159
|
+
user=None,
|
|
1160
|
+
changed=True,
|
|
1161
|
+
log_action=True,
|
|
1162
|
+
previous_revision=None,
|
|
1163
|
+
skip_permission_checks=False,
|
|
1164
|
+
):
|
|
1165
|
+
return PublishPageRevisionAction(
|
|
1166
|
+
revision,
|
|
1167
|
+
user=user,
|
|
1168
|
+
changed=changed,
|
|
1169
|
+
log_action=log_action,
|
|
1170
|
+
previous_revision=previous_revision,
|
|
1171
|
+
).execute(skip_permission_checks=skip_permission_checks)
|
|
1172
|
+
|
|
1173
|
+
def unpublish(self, set_expired=False, commit=True, user=None, log_action=True):
|
|
1174
|
+
return UnpublishPageAction(
|
|
1175
|
+
self,
|
|
1176
|
+
set_expired=set_expired,
|
|
1177
|
+
commit=commit,
|
|
1178
|
+
user=user,
|
|
1179
|
+
log_action=log_action,
|
|
1180
|
+
).execute()
|
|
1181
|
+
|
|
1182
|
+
context_object_name = None
|
|
1183
|
+
|
|
1184
|
+
def get_context(self, request, *args, **kwargs):
|
|
1185
|
+
context = {
|
|
1186
|
+
PAGE_TEMPLATE_VAR: self,
|
|
1187
|
+
"self": self,
|
|
1188
|
+
"request": request,
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if self.context_object_name:
|
|
1192
|
+
context[self.context_object_name] = self
|
|
1193
|
+
|
|
1194
|
+
return context
|
|
1195
|
+
|
|
1196
|
+
def get_preview_context(self, request, mode_name):
|
|
1197
|
+
return self.get_context(request)
|
|
1198
|
+
|
|
1199
|
+
def get_template(self, request, *args, **kwargs):
|
|
1200
|
+
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
|
1201
|
+
return self.ajax_template or self.template
|
|
1202
|
+
else:
|
|
1203
|
+
return self.template
|
|
1204
|
+
|
|
1205
|
+
def get_preview_template(self, request, mode_name):
|
|
1206
|
+
return self.get_template(request)
|
|
1207
|
+
|
|
1208
|
+
def serve(self, request, *args, **kwargs):
|
|
1209
|
+
request.is_preview = False
|
|
1210
|
+
|
|
1211
|
+
return TemplateResponse(
|
|
1212
|
+
request,
|
|
1213
|
+
self.get_template(request, *args, **kwargs),
|
|
1214
|
+
self.get_context(request, *args, **kwargs),
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
def check_request_method(self, request, *args, **kwargs):
|
|
1218
|
+
"""
|
|
1219
|
+
Checks the ``method`` attribute of the request against those supported
|
|
1220
|
+
by the page (as defined by :attr:`allowed_http_methods`) and responds
|
|
1221
|
+
accordingly.
|
|
1222
|
+
|
|
1223
|
+
If supported, ``None`` is returned, and the request is processed
|
|
1224
|
+
normally. If not, a warning is logged and an ``HttpResponseNotAllowed``
|
|
1225
|
+
is returned, and any further request handling is terminated.
|
|
1226
|
+
"""
|
|
1227
|
+
allowed_methods = self.allowed_http_method_names()
|
|
1228
|
+
if request.method not in allowed_methods:
|
|
1229
|
+
logger.warning(
|
|
1230
|
+
"Method Not Allowed (%s): %s",
|
|
1231
|
+
request.method,
|
|
1232
|
+
request.path,
|
|
1233
|
+
extra={"status_code": 405, "request": request},
|
|
1234
|
+
)
|
|
1235
|
+
return HttpResponseNotAllowed(allowed_methods)
|
|
1236
|
+
|
|
1237
|
+
def handle_options_request(self, request, *args, **kwargs):
|
|
1238
|
+
"""
|
|
1239
|
+
Returns an ``HttpResponse`` with an ``"Allow"`` header containing the list of
|
|
1240
|
+
supported HTTP methods for this page. This method is used instead of
|
|
1241
|
+
:meth:`serve` to handle requests when the ``OPTIONS`` HTTP verb is
|
|
1242
|
+
detected (and :class:`HTTPMethod.OPTIONS <python:http.HTTPMethod>` is
|
|
1243
|
+
present in :attr:`allowed_http_methods` for this type of page).
|
|
1244
|
+
"""
|
|
1245
|
+
return HttpResponse(
|
|
1246
|
+
headers={"Allow": ", ".join(self.allowed_http_method_names())}
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
def is_navigable(self):
|
|
1250
|
+
"""
|
|
1251
|
+
Return true if it's meaningful to browse subpages of this page -
|
|
1252
|
+
i.e. it currently has subpages,
|
|
1253
|
+
or it's at the top level (this rule necessary for empty out-of-the-box sites to have working navigation)
|
|
1254
|
+
"""
|
|
1255
|
+
return (not self.is_leaf()) or self.depth == 2
|
|
1256
|
+
|
|
1257
|
+
def _get_site_root_paths(self, request=None):
|
|
1258
|
+
"""
|
|
1259
|
+
Return ``Site.get_site_root_paths()``, using the cached copy on the
|
|
1260
|
+
request object if available.
|
|
1261
|
+
"""
|
|
1262
|
+
# if we have a request, use that to cache site_root_paths; otherwise, use self
|
|
1263
|
+
cache_object = request if request else self
|
|
1264
|
+
try:
|
|
1265
|
+
return cache_object._wagtail_cached_site_root_paths
|
|
1266
|
+
except AttributeError:
|
|
1267
|
+
cache_object._wagtail_cached_site_root_paths = Site.get_site_root_paths()
|
|
1268
|
+
return cache_object._wagtail_cached_site_root_paths
|
|
1269
|
+
|
|
1270
|
+
def _get_relevant_site_root_paths(self, cache_object=None):
|
|
1271
|
+
"""
|
|
1272
|
+
Returns a tuple of root paths for all sites this page belongs to.
|
|
1273
|
+
"""
|
|
1274
|
+
return tuple(
|
|
1275
|
+
srp
|
|
1276
|
+
for srp in self._get_site_root_paths(cache_object)
|
|
1277
|
+
if self.url_path.startswith(srp.root_path)
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
def get_url_parts(self, request=None):
|
|
1281
|
+
"""
|
|
1282
|
+
Determine the URL for this page and return it as a tuple of
|
|
1283
|
+
``(site_id, site_root_url, page_url_relative_to_site_root)``.
|
|
1284
|
+
Return ``None`` if the page is not routable.
|
|
1285
|
+
|
|
1286
|
+
This is used internally by the ``full_url``, ``url``, ``relative_url``
|
|
1287
|
+
and ``get_site`` properties and methods; pages with custom URL routing
|
|
1288
|
+
should override this method in order to have those operations return
|
|
1289
|
+
the custom URLs.
|
|
1290
|
+
|
|
1291
|
+
Accepts an optional keyword argument ``request``, which may be used
|
|
1292
|
+
to avoid repeated database / cache lookups. Typically, a page model
|
|
1293
|
+
that overrides ``get_url_parts`` should not need to deal with
|
|
1294
|
+
``request`` directly, and should just pass it to the original method
|
|
1295
|
+
when calling ``super``.
|
|
1296
|
+
"""
|
|
1297
|
+
|
|
1298
|
+
possible_sites = self._get_relevant_site_root_paths(request)
|
|
1299
|
+
|
|
1300
|
+
if not possible_sites:
|
|
1301
|
+
return None
|
|
1302
|
+
|
|
1303
|
+
# Thanks to the ordering applied by Site.get_site_root_paths(),
|
|
1304
|
+
# the first item is ideal in the vast majority of setups.
|
|
1305
|
+
site_id, root_path, root_url, language_code = possible_sites[0]
|
|
1306
|
+
|
|
1307
|
+
unique_site_ids = {values[0] for values in possible_sites}
|
|
1308
|
+
if len(unique_site_ids) > 1 and isinstance(request, HttpRequest):
|
|
1309
|
+
# The page somehow belongs to more than one site (rare, but possible).
|
|
1310
|
+
# If 'request' is indeed a HttpRequest, use it to identify the 'current'
|
|
1311
|
+
# site and prefer an option matching that (where present).
|
|
1312
|
+
site = Site.find_for_request(request)
|
|
1313
|
+
if site:
|
|
1314
|
+
for values in possible_sites:
|
|
1315
|
+
if values[0] == site.pk:
|
|
1316
|
+
site_id, root_path, root_url, language_code = values
|
|
1317
|
+
break
|
|
1318
|
+
|
|
1319
|
+
use_wagtail_i18n = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
|
1320
|
+
|
|
1321
|
+
if use_wagtail_i18n:
|
|
1322
|
+
# If the active language code is a variant of the page's language, then
|
|
1323
|
+
# use that instead
|
|
1324
|
+
# This is used when LANGUAGES contain more languages than WAGTAIL_CONTENT_LANGUAGES
|
|
1325
|
+
try:
|
|
1326
|
+
if (
|
|
1327
|
+
get_supported_content_language_variant(translation.get_language())
|
|
1328
|
+
== language_code
|
|
1329
|
+
):
|
|
1330
|
+
language_code = translation.get_language()
|
|
1331
|
+
except LookupError:
|
|
1332
|
+
# active language code is not a recognised content language, so leave
|
|
1333
|
+
# page's language code unchanged
|
|
1334
|
+
pass
|
|
1335
|
+
|
|
1336
|
+
# The page may not be routable because wagtail_serve is not registered
|
|
1337
|
+
# This may be the case if Wagtail is used headless
|
|
1338
|
+
try:
|
|
1339
|
+
if use_wagtail_i18n:
|
|
1340
|
+
with translation.override(language_code):
|
|
1341
|
+
page_path = reverse(
|
|
1342
|
+
"wagtail_serve", args=(self.url_path[len(root_path) :],)
|
|
1343
|
+
)
|
|
1344
|
+
else:
|
|
1345
|
+
page_path = reverse(
|
|
1346
|
+
"wagtail_serve", args=(self.url_path[len(root_path) :],)
|
|
1347
|
+
)
|
|
1348
|
+
except NoReverseMatch:
|
|
1349
|
+
return (site_id, None, None)
|
|
1350
|
+
|
|
1351
|
+
# Remove the trailing slash from the URL reverse generates if
|
|
1352
|
+
# WAGTAIL_APPEND_SLASH is False and we're not trying to serve
|
|
1353
|
+
# the root path
|
|
1354
|
+
if not WAGTAIL_APPEND_SLASH and page_path != "/":
|
|
1355
|
+
page_path = page_path.rstrip("/")
|
|
1356
|
+
|
|
1357
|
+
return (site_id, root_url, page_path)
|
|
1358
|
+
|
|
1359
|
+
def get_full_url(self, request=None):
|
|
1360
|
+
"""
|
|
1361
|
+
Return the full URL (including protocol / domain) to this page, or ``None`` if it is not routable.
|
|
1362
|
+
"""
|
|
1363
|
+
url_parts = self.get_url_parts(request=request)
|
|
1364
|
+
|
|
1365
|
+
if url_parts is None or url_parts[1] is None and url_parts[2] is None:
|
|
1366
|
+
# page is not routable
|
|
1367
|
+
return
|
|
1368
|
+
|
|
1369
|
+
site_id, root_url, page_path = url_parts
|
|
1370
|
+
|
|
1371
|
+
return root_url + page_path
|
|
1372
|
+
|
|
1373
|
+
full_url = property(get_full_url)
|
|
1374
|
+
|
|
1375
|
+
def get_url(self, request=None, current_site=None):
|
|
1376
|
+
"""
|
|
1377
|
+
Return the 'most appropriate' URL for referring to this page from the pages we serve,
|
|
1378
|
+
within the Wagtail backend and actual website templates;
|
|
1379
|
+
this is the local URL (starting with '/') if we're only running a single site
|
|
1380
|
+
(i.e. we know that whatever the current page is being served from, this link will be on the
|
|
1381
|
+
same domain), and the full URL (with domain) if not.
|
|
1382
|
+
Return ``None`` if the page is not routable.
|
|
1383
|
+
|
|
1384
|
+
Accepts an optional but recommended ``request`` keyword argument that, if provided, will
|
|
1385
|
+
be used to cache site-level URL information (thereby avoiding repeated database / cache
|
|
1386
|
+
lookups) and, via the ``Site.find_for_request()`` function, determine whether a relative
|
|
1387
|
+
or full URL is most appropriate.
|
|
1388
|
+
"""
|
|
1389
|
+
# ``current_site`` is purposefully undocumented, as one can simply pass the request and get
|
|
1390
|
+
# a relative URL based on ``Site.find_for_request()``. Nonetheless, support it here to avoid
|
|
1391
|
+
# copy/pasting the code to the ``relative_url`` method below.
|
|
1392
|
+
if current_site is None and request is not None:
|
|
1393
|
+
site = Site.find_for_request(request)
|
|
1394
|
+
current_site = site
|
|
1395
|
+
url_parts = self.get_url_parts(request=request)
|
|
1396
|
+
|
|
1397
|
+
if url_parts is None or url_parts[1] is None and url_parts[2] is None:
|
|
1398
|
+
# page is not routable
|
|
1399
|
+
return
|
|
1400
|
+
|
|
1401
|
+
site_id, root_url, page_path = url_parts
|
|
1402
|
+
|
|
1403
|
+
# Get number of unique sites in root paths
|
|
1404
|
+
# Note: there may be more root paths to sites if there are multiple languages
|
|
1405
|
+
num_sites = len(
|
|
1406
|
+
{root_path[0] for root_path in self._get_site_root_paths(request)}
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
if (current_site is not None and site_id == current_site.id) or num_sites == 1:
|
|
1410
|
+
# the site matches OR we're only running a single site, so a local URL is sufficient
|
|
1411
|
+
return page_path
|
|
1412
|
+
else:
|
|
1413
|
+
return root_url + page_path
|
|
1414
|
+
|
|
1415
|
+
url = property(get_url)
|
|
1416
|
+
|
|
1417
|
+
def relative_url(self, current_site, request=None):
|
|
1418
|
+
"""
|
|
1419
|
+
Return the 'most appropriate' URL for this page taking into account the site we're currently on;
|
|
1420
|
+
a local URL if the site matches, or a fully qualified one otherwise.
|
|
1421
|
+
Return ``None`` if the page is not routable.
|
|
1422
|
+
|
|
1423
|
+
Accepts an optional but recommended ``request`` keyword argument that, if provided, will
|
|
1424
|
+
be used to cache site-level URL information (thereby avoiding repeated database / cache
|
|
1425
|
+
lookups).
|
|
1426
|
+
"""
|
|
1427
|
+
return self.get_url(request=request, current_site=current_site)
|
|
1428
|
+
|
|
1429
|
+
def get_site(self):
|
|
1430
|
+
"""
|
|
1431
|
+
Return the Site object that this page belongs to.
|
|
1432
|
+
"""
|
|
1433
|
+
|
|
1434
|
+
url_parts = self.get_url_parts()
|
|
1435
|
+
|
|
1436
|
+
if url_parts is None:
|
|
1437
|
+
# page is not routable
|
|
1438
|
+
return
|
|
1439
|
+
|
|
1440
|
+
site_id, root_url, page_path = url_parts
|
|
1441
|
+
|
|
1442
|
+
return Site.objects.get(id=site_id)
|
|
1443
|
+
|
|
1444
|
+
@classmethod
|
|
1445
|
+
def get_indexed_objects(cls):
|
|
1446
|
+
content_type = ContentType.objects.get_for_model(cls)
|
|
1447
|
+
return super().get_indexed_objects().filter(content_type=content_type)
|
|
1448
|
+
|
|
1449
|
+
def get_indexed_instance(self):
|
|
1450
|
+
# This is accessed on save by the wagtailsearch signal handler, and in edge
|
|
1451
|
+
# cases (e.g. loading test fixtures), may be called before the specific instance's
|
|
1452
|
+
# entry has been created. In those cases, we aren't ready to be indexed yet, so
|
|
1453
|
+
# return None.
|
|
1454
|
+
try:
|
|
1455
|
+
return self.specific
|
|
1456
|
+
except self.specific_class.DoesNotExist:
|
|
1457
|
+
return None
|
|
1458
|
+
|
|
1459
|
+
def get_default_privacy_setting(self, request: HttpRequest):
|
|
1460
|
+
"""Set the default privacy setting for a page."""
|
|
1461
|
+
return {"type": BaseViewRestriction.NONE}
|
|
1462
|
+
|
|
1463
|
+
@classmethod
|
|
1464
|
+
def clean_subpage_models(cls):
|
|
1465
|
+
"""
|
|
1466
|
+
Returns the list of subpage types, normalized as model classes.
|
|
1467
|
+
Throws ValueError if any entry in subpage_types cannot be recognized as a model name,
|
|
1468
|
+
or LookupError if a model does not exist (or is not a Page subclass).
|
|
1469
|
+
"""
|
|
1470
|
+
if cls._clean_subpage_models is None:
|
|
1471
|
+
subpage_types = getattr(cls, "subpage_types", None)
|
|
1472
|
+
if subpage_types is None:
|
|
1473
|
+
# if subpage_types is not specified on the Page class, allow all page types as subpages
|
|
1474
|
+
cls._clean_subpage_models = get_page_models()
|
|
1475
|
+
else:
|
|
1476
|
+
cls._clean_subpage_models = [
|
|
1477
|
+
resolve_model_string(model_string, cls._meta.app_label)
|
|
1478
|
+
for model_string in subpage_types
|
|
1479
|
+
]
|
|
1480
|
+
|
|
1481
|
+
for model in cls._clean_subpage_models:
|
|
1482
|
+
if not issubclass(model, Page):
|
|
1483
|
+
raise LookupError("%s is not a Page subclass" % model)
|
|
1484
|
+
|
|
1485
|
+
return cls._clean_subpage_models
|
|
1486
|
+
|
|
1487
|
+
@classmethod
|
|
1488
|
+
def clean_parent_page_models(cls):
|
|
1489
|
+
"""
|
|
1490
|
+
Returns the list of parent page types, normalized as model classes.
|
|
1491
|
+
Throws ValueError if any entry in parent_page_types cannot be recognized as a model name,
|
|
1492
|
+
or LookupError if a model does not exist (or is not a Page subclass).
|
|
1493
|
+
"""
|
|
1494
|
+
|
|
1495
|
+
if cls._clean_parent_page_models is None:
|
|
1496
|
+
parent_page_types = getattr(cls, "parent_page_types", None)
|
|
1497
|
+
if parent_page_types is None:
|
|
1498
|
+
# if parent_page_types is not specified on the Page class, allow all page types as subpages
|
|
1499
|
+
cls._clean_parent_page_models = get_page_models()
|
|
1500
|
+
else:
|
|
1501
|
+
cls._clean_parent_page_models = [
|
|
1502
|
+
resolve_model_string(model_string, cls._meta.app_label)
|
|
1503
|
+
for model_string in parent_page_types
|
|
1504
|
+
]
|
|
1505
|
+
|
|
1506
|
+
for model in cls._clean_parent_page_models:
|
|
1507
|
+
if not issubclass(model, Page):
|
|
1508
|
+
raise LookupError("%s is not a Page subclass" % model)
|
|
1509
|
+
|
|
1510
|
+
return cls._clean_parent_page_models
|
|
1511
|
+
|
|
1512
|
+
@classmethod
|
|
1513
|
+
def allowed_parent_page_models(cls):
|
|
1514
|
+
"""
|
|
1515
|
+
Returns the list of page types that this page type can be a subpage of,
|
|
1516
|
+
as a list of model classes.
|
|
1517
|
+
"""
|
|
1518
|
+
return [
|
|
1519
|
+
parent_model
|
|
1520
|
+
for parent_model in cls.clean_parent_page_models()
|
|
1521
|
+
if cls in parent_model.clean_subpage_models()
|
|
1522
|
+
]
|
|
1523
|
+
|
|
1524
|
+
@classmethod
|
|
1525
|
+
def allowed_subpage_models(cls):
|
|
1526
|
+
"""
|
|
1527
|
+
Returns the list of page types that this page type can have as subpages,
|
|
1528
|
+
as a list of model classes.
|
|
1529
|
+
"""
|
|
1530
|
+
return [
|
|
1531
|
+
subpage_model
|
|
1532
|
+
for subpage_model in cls.clean_subpage_models()
|
|
1533
|
+
if cls in subpage_model.clean_parent_page_models()
|
|
1534
|
+
]
|
|
1535
|
+
|
|
1536
|
+
@classmethod
|
|
1537
|
+
def creatable_subpage_models(cls):
|
|
1538
|
+
"""
|
|
1539
|
+
Returns the list of page types that may be created under this page type,
|
|
1540
|
+
as a list of model classes.
|
|
1541
|
+
"""
|
|
1542
|
+
return [
|
|
1543
|
+
page_model
|
|
1544
|
+
for page_model in cls.allowed_subpage_models()
|
|
1545
|
+
if page_model.is_creatable
|
|
1546
|
+
]
|
|
1547
|
+
|
|
1548
|
+
@classmethod
|
|
1549
|
+
def can_exist_under(cls, parent):
|
|
1550
|
+
"""
|
|
1551
|
+
Checks if this page type can exist as a subpage under a parent page
|
|
1552
|
+
instance.
|
|
1553
|
+
|
|
1554
|
+
See also: :func:`Page.can_create_at` and :func:`Page.can_move_to`
|
|
1555
|
+
"""
|
|
1556
|
+
return cls in parent.specific_class.allowed_subpage_models()
|
|
1557
|
+
|
|
1558
|
+
@classmethod
|
|
1559
|
+
def can_create_at(cls, parent):
|
|
1560
|
+
"""
|
|
1561
|
+
Checks if this page type can be created as a subpage under a parent
|
|
1562
|
+
page instance.
|
|
1563
|
+
"""
|
|
1564
|
+
can_create = cls.is_creatable and cls.can_exist_under(parent)
|
|
1565
|
+
|
|
1566
|
+
if cls.max_count is not None:
|
|
1567
|
+
can_create = can_create and cls.objects.count() < cls.max_count
|
|
1568
|
+
|
|
1569
|
+
if cls.max_count_per_parent is not None:
|
|
1570
|
+
can_create = (
|
|
1571
|
+
can_create
|
|
1572
|
+
and parent.get_children().type(cls).count() < cls.max_count_per_parent
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
return can_create
|
|
1576
|
+
|
|
1577
|
+
def can_move_to(self, parent):
|
|
1578
|
+
"""
|
|
1579
|
+
Checks if this page instance can be moved to be a subpage of a parent
|
|
1580
|
+
page instance.
|
|
1581
|
+
"""
|
|
1582
|
+
# Prevent pages from being moved to different language sections
|
|
1583
|
+
# The only page that can have multi-lingual children is the root page
|
|
1584
|
+
parent_is_root = parent.depth == 1
|
|
1585
|
+
if not parent_is_root and parent.locale_id != self.locale_id:
|
|
1586
|
+
return False
|
|
1587
|
+
|
|
1588
|
+
return self.can_exist_under(parent)
|
|
1589
|
+
|
|
1590
|
+
@classmethod
|
|
1591
|
+
def get_verbose_name(cls):
|
|
1592
|
+
"""
|
|
1593
|
+
Returns the human-readable "verbose name" of this page model e.g "Blog page".
|
|
1594
|
+
"""
|
|
1595
|
+
# This is similar to doing cls._meta.verbose_name.title()
|
|
1596
|
+
# except this doesn't convert any characters to lowercase
|
|
1597
|
+
return capfirst(cls._meta.verbose_name)
|
|
1598
|
+
|
|
1599
|
+
@classmethod
|
|
1600
|
+
def get_page_description(cls):
|
|
1601
|
+
"""
|
|
1602
|
+
Returns a page description if it's set. For example "A multi-purpose web page".
|
|
1603
|
+
"""
|
|
1604
|
+
description = getattr(cls, "page_description", None)
|
|
1605
|
+
|
|
1606
|
+
# make sure that page_description is actually a string rather than a model field
|
|
1607
|
+
if isinstance(description, str):
|
|
1608
|
+
return description
|
|
1609
|
+
elif isinstance(description, Promise):
|
|
1610
|
+
# description is a lazy object (e.g. the result of gettext_lazy())
|
|
1611
|
+
return str(description)
|
|
1612
|
+
else:
|
|
1613
|
+
return ""
|
|
1614
|
+
|
|
1615
|
+
@property
|
|
1616
|
+
def approved_schedule(self):
|
|
1617
|
+
"""
|
|
1618
|
+
``_approved_schedule`` may be populated by ``annotate_approved_schedule`` on ``PageQuerySet`` as a
|
|
1619
|
+
performance optimization.
|
|
1620
|
+
"""
|
|
1621
|
+
if hasattr(self, "_approved_schedule"):
|
|
1622
|
+
return self._approved_schedule
|
|
1623
|
+
|
|
1624
|
+
return self.scheduled_revision is not None
|
|
1625
|
+
|
|
1626
|
+
def has_unpublished_subtree(self):
|
|
1627
|
+
"""
|
|
1628
|
+
An awkwardly-defined flag used in determining whether unprivileged editors have
|
|
1629
|
+
permission to delete this article. Returns true if and only if this page is non-live,
|
|
1630
|
+
and it has no live children.
|
|
1631
|
+
"""
|
|
1632
|
+
return (not self.live) and (
|
|
1633
|
+
not self.get_descendants().filter(live=True).exists()
|
|
1634
|
+
)
|
|
1635
|
+
|
|
1636
|
+
def move(self, target, pos=None, user=None):
|
|
1637
|
+
"""
|
|
1638
|
+
Extension to the treebeard 'move' method to ensure that url_path is updated,
|
|
1639
|
+
and to emit a 'pre_page_move' and 'post_page_move' signals.
|
|
1640
|
+
"""
|
|
1641
|
+
return MovePageAction(self, target, pos=pos, user=user).execute()
|
|
1642
|
+
|
|
1643
|
+
def copy(
|
|
1644
|
+
self,
|
|
1645
|
+
recursive=False,
|
|
1646
|
+
to=None,
|
|
1647
|
+
update_attrs=None,
|
|
1648
|
+
copy_revisions=True,
|
|
1649
|
+
keep_live=True,
|
|
1650
|
+
user=None,
|
|
1651
|
+
process_child_object=None,
|
|
1652
|
+
exclude_fields=None,
|
|
1653
|
+
log_action="wagtail.copy",
|
|
1654
|
+
reset_translation_key=True,
|
|
1655
|
+
):
|
|
1656
|
+
"""
|
|
1657
|
+
Copies a given page
|
|
1658
|
+
|
|
1659
|
+
:param log_action: flag for logging the action. Pass None to skip logging. Can be passed an action string. Defaults to ``'wagtail.copy'``.
|
|
1660
|
+
"""
|
|
1661
|
+
return CopyPageAction(
|
|
1662
|
+
self,
|
|
1663
|
+
to=to,
|
|
1664
|
+
update_attrs=update_attrs,
|
|
1665
|
+
exclude_fields=exclude_fields,
|
|
1666
|
+
recursive=recursive,
|
|
1667
|
+
copy_revisions=copy_revisions,
|
|
1668
|
+
keep_live=keep_live,
|
|
1669
|
+
user=user,
|
|
1670
|
+
process_child_object=process_child_object,
|
|
1671
|
+
log_action=log_action,
|
|
1672
|
+
reset_translation_key=reset_translation_key,
|
|
1673
|
+
).execute(skip_permission_checks=True)
|
|
1674
|
+
|
|
1675
|
+
copy.alters_data = True
|
|
1676
|
+
|
|
1677
|
+
def create_alias(
|
|
1678
|
+
self,
|
|
1679
|
+
*,
|
|
1680
|
+
recursive=False,
|
|
1681
|
+
parent=None,
|
|
1682
|
+
update_slug=None,
|
|
1683
|
+
update_locale=None,
|
|
1684
|
+
user=None,
|
|
1685
|
+
log_action="wagtail.create_alias",
|
|
1686
|
+
reset_translation_key=True,
|
|
1687
|
+
_mpnode_attrs=None,
|
|
1688
|
+
):
|
|
1689
|
+
return CreatePageAliasAction(
|
|
1690
|
+
self,
|
|
1691
|
+
recursive=recursive,
|
|
1692
|
+
parent=parent,
|
|
1693
|
+
update_slug=update_slug,
|
|
1694
|
+
update_locale=update_locale,
|
|
1695
|
+
user=user,
|
|
1696
|
+
log_action=log_action,
|
|
1697
|
+
reset_translation_key=reset_translation_key,
|
|
1698
|
+
_mpnode_attrs=_mpnode_attrs,
|
|
1699
|
+
).execute()
|
|
1700
|
+
|
|
1701
|
+
create_alias.alters_data = True
|
|
1702
|
+
|
|
1703
|
+
def copy_for_translation(
|
|
1704
|
+
self, locale, copy_parents=False, alias=False, exclude_fields=None
|
|
1705
|
+
):
|
|
1706
|
+
"""Creates a copy of this page in the specified locale."""
|
|
1707
|
+
|
|
1708
|
+
return CopyPageForTranslationAction(
|
|
1709
|
+
self,
|
|
1710
|
+
locale,
|
|
1711
|
+
copy_parents=copy_parents,
|
|
1712
|
+
alias=alias,
|
|
1713
|
+
exclude_fields=exclude_fields,
|
|
1714
|
+
).execute()
|
|
1715
|
+
|
|
1716
|
+
copy_for_translation.alters_data = True
|
|
1717
|
+
|
|
1718
|
+
def permissions_for_user(self, user):
|
|
1719
|
+
"""
|
|
1720
|
+
Return a PagePermissionsTester object defining what actions the user can perform on this page.
|
|
1721
|
+
"""
|
|
1722
|
+
# Allow specific classes to override this method, but only cast to the
|
|
1723
|
+
# specific instance if it's not already specific and if the method has
|
|
1724
|
+
# been overridden. This helps improve performance when working with
|
|
1725
|
+
# base Page querysets.
|
|
1726
|
+
is_overridden = (
|
|
1727
|
+
self.specific_class
|
|
1728
|
+
and self.specific_class.permissions_for_user
|
|
1729
|
+
!= type(self).permissions_for_user
|
|
1730
|
+
)
|
|
1731
|
+
if is_overridden and not isinstance(self, self.specific_class):
|
|
1732
|
+
return self.specific_deferred.permissions_for_user(user)
|
|
1733
|
+
return PagePermissionTester(user, self)
|
|
1734
|
+
|
|
1735
|
+
def is_previewable(self):
|
|
1736
|
+
"""Returns True if at least one preview mode is specified"""
|
|
1737
|
+
# It's possible that this will be called from a listing page using a plain Page queryset -
|
|
1738
|
+
# if so, checking self.preview_modes would incorrectly give us the default set from
|
|
1739
|
+
# Page.preview_modes. However, accessing self.specific.preview_modes would result in an N+1
|
|
1740
|
+
# query problem. To avoid this (at least in the general case), we'll call .specific only if
|
|
1741
|
+
# a check of the property at the class level indicates that preview_modes has been
|
|
1742
|
+
# overridden from whatever type we're currently in.
|
|
1743
|
+
page = self
|
|
1744
|
+
if page.specific_class.preview_modes != type(page).preview_modes:
|
|
1745
|
+
page = page.specific
|
|
1746
|
+
|
|
1747
|
+
return bool(page.preview_modes)
|
|
1748
|
+
|
|
1749
|
+
def get_route_paths(self):
|
|
1750
|
+
"""
|
|
1751
|
+
Returns a list of paths that this page can be viewed at.
|
|
1752
|
+
|
|
1753
|
+
These values are combined with the dynamic portion of the page URL to
|
|
1754
|
+
automatically create redirects when the page's URL changes.
|
|
1755
|
+
|
|
1756
|
+
.. note::
|
|
1757
|
+
|
|
1758
|
+
If using ``RoutablePageMixin``, you may want to override this method
|
|
1759
|
+
to include the paths of popular routes.
|
|
1760
|
+
|
|
1761
|
+
.. note::
|
|
1762
|
+
|
|
1763
|
+
Redirect paths are 'normalized' to apply consistent ordering to GET parameters,
|
|
1764
|
+
so you don't need to include every variation. Fragment identifiers are discarded
|
|
1765
|
+
too, so should be avoided.
|
|
1766
|
+
"""
|
|
1767
|
+
return ["/"]
|
|
1768
|
+
|
|
1769
|
+
def get_cached_paths(self):
|
|
1770
|
+
"""
|
|
1771
|
+
This returns a list of paths to invalidate in a frontend cache
|
|
1772
|
+
"""
|
|
1773
|
+
return ["/"]
|
|
1774
|
+
|
|
1775
|
+
def get_cache_key_components(self):
|
|
1776
|
+
"""
|
|
1777
|
+
The components of a :class:`Page` which make up the :attr:`cache_key`. Any change to a
|
|
1778
|
+
page should be reflected in a change to at least one of these components.
|
|
1779
|
+
"""
|
|
1780
|
+
|
|
1781
|
+
return [
|
|
1782
|
+
self.id,
|
|
1783
|
+
self.url_path,
|
|
1784
|
+
self.last_published_at.isoformat() if self.last_published_at else None,
|
|
1785
|
+
]
|
|
1786
|
+
|
|
1787
|
+
@property
|
|
1788
|
+
def cache_key(self):
|
|
1789
|
+
"""
|
|
1790
|
+
A generic cache key to identify a page in its current state.
|
|
1791
|
+
Should the page change, so will the key.
|
|
1792
|
+
|
|
1793
|
+
Customizations to the cache key should be made in :attr:`get_cache_key_components`.
|
|
1794
|
+
"""
|
|
1795
|
+
|
|
1796
|
+
hasher = safe_md5()
|
|
1797
|
+
|
|
1798
|
+
for component in self.get_cache_key_components():
|
|
1799
|
+
hasher.update(force_bytes(component))
|
|
1800
|
+
|
|
1801
|
+
return hasher.hexdigest()
|
|
1802
|
+
|
|
1803
|
+
def get_sitemap_urls(self, request=None):
|
|
1804
|
+
return [
|
|
1805
|
+
{
|
|
1806
|
+
"location": self.get_full_url(request),
|
|
1807
|
+
# fall back on latest_revision_created_at if last_published_at is null
|
|
1808
|
+
# (for backwards compatibility from before last_published_at was added)
|
|
1809
|
+
"lastmod": (self.last_published_at or self.latest_revision_created_at),
|
|
1810
|
+
}
|
|
1811
|
+
]
|
|
1812
|
+
|
|
1813
|
+
def get_ancestors(self, inclusive=False):
|
|
1814
|
+
"""
|
|
1815
|
+
Returns a queryset of the current page's ancestors, starting at the root page
|
|
1816
|
+
and descending to the parent, or to the current page itself if ``inclusive`` is true.
|
|
1817
|
+
"""
|
|
1818
|
+
return Page.objects.ancestor_of(self, inclusive)
|
|
1819
|
+
|
|
1820
|
+
def get_descendants(self, inclusive=False):
|
|
1821
|
+
"""
|
|
1822
|
+
Returns a queryset of all pages underneath the current page, any number of levels deep.
|
|
1823
|
+
If ``inclusive`` is true, the current page itself is included in the queryset.
|
|
1824
|
+
"""
|
|
1825
|
+
return Page.objects.descendant_of(self, inclusive)
|
|
1826
|
+
|
|
1827
|
+
def get_siblings(self, inclusive=True):
|
|
1828
|
+
"""
|
|
1829
|
+
Returns a queryset of all other pages with the same parent as the current page.
|
|
1830
|
+
If ``inclusive`` is true, the current page itself is included in the queryset.
|
|
1831
|
+
"""
|
|
1832
|
+
return Page.objects.sibling_of(self, inclusive)
|
|
1833
|
+
|
|
1834
|
+
def get_next_siblings(self, inclusive=False):
|
|
1835
|
+
return self.get_siblings(inclusive).filter(path__gte=self.path).order_by("path")
|
|
1836
|
+
|
|
1837
|
+
def get_prev_siblings(self, inclusive=False):
|
|
1838
|
+
return (
|
|
1839
|
+
self.get_siblings(inclusive).filter(path__lte=self.path).order_by("-path")
|
|
1840
|
+
)
|
|
1841
|
+
|
|
1842
|
+
def get_view_restrictions(self):
|
|
1843
|
+
"""
|
|
1844
|
+
Return a query set of all page view restrictions that apply to this page.
|
|
1845
|
+
|
|
1846
|
+
This checks the current page and all ancestor pages for page view restrictions.
|
|
1847
|
+
|
|
1848
|
+
If any of those pages are aliases, it will resolve them to their source pages
|
|
1849
|
+
before querying PageViewRestrictions so alias pages use the same view restrictions
|
|
1850
|
+
as their source page and they cannot have their own.
|
|
1851
|
+
"""
|
|
1852
|
+
page_ids_to_check = set()
|
|
1853
|
+
|
|
1854
|
+
def add_page_to_check_list(page):
|
|
1855
|
+
# If the page is an alias, add the source page to the check list instead
|
|
1856
|
+
if page.alias_of:
|
|
1857
|
+
add_page_to_check_list(page.alias_of)
|
|
1858
|
+
else:
|
|
1859
|
+
page_ids_to_check.add(page.id)
|
|
1860
|
+
|
|
1861
|
+
# Check current page for view restrictions
|
|
1862
|
+
add_page_to_check_list(self)
|
|
1863
|
+
|
|
1864
|
+
# Check each ancestor for view restrictions as well
|
|
1865
|
+
for page in self.get_ancestors().only("alias_of"):
|
|
1866
|
+
add_page_to_check_list(page)
|
|
1867
|
+
|
|
1868
|
+
return PageViewRestriction.objects.filter(page_id__in=page_ids_to_check)
|
|
1869
|
+
|
|
1870
|
+
password_required_template = None
|
|
1871
|
+
|
|
1872
|
+
def serve_password_required_response(self, request, form, action_url):
|
|
1873
|
+
"""
|
|
1874
|
+
Serve a response indicating that the user has been denied access to view this page,
|
|
1875
|
+
and must supply a password.
|
|
1876
|
+
``form`` = a Django form object containing the password input
|
|
1877
|
+
(and zero or more hidden fields that also need to be output on the template)
|
|
1878
|
+
``action_url`` = URL that this form should be POSTed to
|
|
1879
|
+
"""
|
|
1880
|
+
|
|
1881
|
+
password_required_template = self.password_required_template or getattr(
|
|
1882
|
+
settings,
|
|
1883
|
+
"WAGTAIL_PASSWORD_REQUIRED_TEMPLATE",
|
|
1884
|
+
"wagtailcore/password_required.html",
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
context = self.get_context(request)
|
|
1888
|
+
context["form"] = form
|
|
1889
|
+
context["action_url"] = action_url
|
|
1890
|
+
return TemplateResponse(request, password_required_template, context)
|
|
1891
|
+
|
|
1892
|
+
def with_content_json(self, content):
|
|
1893
|
+
"""
|
|
1894
|
+
Returns a new version of the page with field values updated to reflect changes
|
|
1895
|
+
in the provided ``content`` (which usually comes from a previously-saved
|
|
1896
|
+
page revision).
|
|
1897
|
+
|
|
1898
|
+
Certain field values are preserved in order to prevent errors if the returned
|
|
1899
|
+
page is saved, such as ``id``, ``content_type`` and some tree-related values.
|
|
1900
|
+
The following field values are also preserved, as they are considered to be
|
|
1901
|
+
meaningful to the page as a whole, rather than to a specific revision:
|
|
1902
|
+
|
|
1903
|
+
* ``draft_title``
|
|
1904
|
+
* ``live``
|
|
1905
|
+
* ``has_unpublished_changes``
|
|
1906
|
+
* ``owner``
|
|
1907
|
+
* ``locked``
|
|
1908
|
+
* ``locked_by``
|
|
1909
|
+
* ``locked_at``
|
|
1910
|
+
* ``latest_revision``
|
|
1911
|
+
* ``latest_revision_created_at``
|
|
1912
|
+
* ``first_published_at``
|
|
1913
|
+
* ``alias_of``
|
|
1914
|
+
* ``wagtail_admin_comments`` (COMMENTS_RELATION_NAME)
|
|
1915
|
+
"""
|
|
1916
|
+
|
|
1917
|
+
# Old revisions (pre Wagtail 2.15) may have saved comment data under the name 'comments'
|
|
1918
|
+
# rather than the current relation name as set by COMMENTS_RELATION_NAME;
|
|
1919
|
+
# if a 'comments' field exists and looks like our comments model, alter the data to use
|
|
1920
|
+
# COMMENTS_RELATION_NAME before restoring
|
|
1921
|
+
if (
|
|
1922
|
+
COMMENTS_RELATION_NAME not in content
|
|
1923
|
+
and "comments" in content
|
|
1924
|
+
and isinstance(content["comments"], list)
|
|
1925
|
+
and len(content["comments"])
|
|
1926
|
+
and isinstance(content["comments"][0], dict)
|
|
1927
|
+
and "contentpath" in content["comments"][0]
|
|
1928
|
+
):
|
|
1929
|
+
content[COMMENTS_RELATION_NAME] = content["comments"]
|
|
1930
|
+
del content["comments"]
|
|
1931
|
+
|
|
1932
|
+
obj = self.specific_class.from_serializable_data(content)
|
|
1933
|
+
|
|
1934
|
+
# These should definitely never change between revisions
|
|
1935
|
+
obj.id = self.id
|
|
1936
|
+
obj.pk = self.pk
|
|
1937
|
+
obj.content_type_id = self.content_type_id
|
|
1938
|
+
|
|
1939
|
+
# Override possibly-outdated tree parameter fields
|
|
1940
|
+
obj.path = self.path
|
|
1941
|
+
obj.depth = self.depth
|
|
1942
|
+
obj.numchild = self.numchild
|
|
1943
|
+
|
|
1944
|
+
# Update url_path to reflect potential slug changes, but maintaining the page's
|
|
1945
|
+
# existing tree position
|
|
1946
|
+
obj.set_url_path(self.get_parent())
|
|
1947
|
+
|
|
1948
|
+
# Ensure other values that are meaningful for the page as a whole (rather than
|
|
1949
|
+
# to a specific revision) are preserved
|
|
1950
|
+
obj.draft_title = self.draft_title
|
|
1951
|
+
obj.live = self.live
|
|
1952
|
+
obj.has_unpublished_changes = self.has_unpublished_changes
|
|
1953
|
+
obj.owner_id = self.owner_id
|
|
1954
|
+
obj.locked = self.locked
|
|
1955
|
+
obj.locked_by_id = self.locked_by_id
|
|
1956
|
+
obj.locked_at = self.locked_at
|
|
1957
|
+
obj.latest_revision_id = self.latest_revision_id
|
|
1958
|
+
obj.latest_revision_created_at = self.latest_revision_created_at
|
|
1959
|
+
obj.first_published_at = self.first_published_at
|
|
1960
|
+
obj.translation_key = self.translation_key
|
|
1961
|
+
obj.locale_id = self.locale_id
|
|
1962
|
+
obj.alias_of_id = self.alias_of_id
|
|
1963
|
+
revision_comment_positions = dict(
|
|
1964
|
+
getattr(obj, COMMENTS_RELATION_NAME).values_list("id", "position")
|
|
1965
|
+
)
|
|
1966
|
+
page_comments = (
|
|
1967
|
+
getattr(self, COMMENTS_RELATION_NAME)
|
|
1968
|
+
.filter(resolved_at__isnull=True)
|
|
1969
|
+
.defer("position")
|
|
1970
|
+
)
|
|
1971
|
+
for comment in page_comments:
|
|
1972
|
+
# attempt to retrieve the comment position from the revision's stored version
|
|
1973
|
+
# of the comment
|
|
1974
|
+
try:
|
|
1975
|
+
comment.position = revision_comment_positions[comment.id]
|
|
1976
|
+
except KeyError:
|
|
1977
|
+
pass
|
|
1978
|
+
setattr(obj, COMMENTS_RELATION_NAME, page_comments)
|
|
1979
|
+
|
|
1980
|
+
return obj
|
|
1981
|
+
|
|
1982
|
+
@property
|
|
1983
|
+
def has_workflow(self):
|
|
1984
|
+
"""
|
|
1985
|
+
Returns ``True`` if the page or an ancestor has an active workflow assigned, otherwise ``False``.
|
|
1986
|
+
"""
|
|
1987
|
+
if not getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
|
|
1988
|
+
return False
|
|
1989
|
+
return (
|
|
1990
|
+
self.get_ancestors(inclusive=True)
|
|
1991
|
+
.filter(workflowpage__isnull=False)
|
|
1992
|
+
.filter(workflowpage__workflow__active=True)
|
|
1993
|
+
.exists()
|
|
1994
|
+
)
|
|
1995
|
+
|
|
1996
|
+
def get_workflow(self):
|
|
1997
|
+
"""
|
|
1998
|
+
Returns the active workflow assigned to the page or its nearest ancestor.
|
|
1999
|
+
"""
|
|
2000
|
+
if not getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
|
|
2001
|
+
return None
|
|
2002
|
+
|
|
2003
|
+
if hasattr(self, "workflowpage") and self.workflowpage.workflow.active:
|
|
2004
|
+
return self.workflowpage.workflow
|
|
2005
|
+
else:
|
|
2006
|
+
try:
|
|
2007
|
+
workflow = (
|
|
2008
|
+
self.get_ancestors()
|
|
2009
|
+
.filter(workflowpage__isnull=False)
|
|
2010
|
+
.filter(workflowpage__workflow__active=True)
|
|
2011
|
+
.order_by("-depth")
|
|
2012
|
+
.first()
|
|
2013
|
+
.workflowpage.workflow
|
|
2014
|
+
)
|
|
2015
|
+
except AttributeError:
|
|
2016
|
+
workflow = None
|
|
2017
|
+
return workflow
|
|
2018
|
+
|
|
2019
|
+
class Meta:
|
|
2020
|
+
verbose_name = _("page")
|
|
2021
|
+
verbose_name_plural = _("pages")
|
|
2022
|
+
unique_together = [("translation_key", "locale")]
|
|
2023
|
+
# Make sure that we auto-create Permission objects that are defined in
|
|
2024
|
+
# PAGE_PERMISSION_TYPES, skipping the default_permissions from Django.
|
|
2025
|
+
permissions = [
|
|
2026
|
+
(codename, name)
|
|
2027
|
+
for codename, _, name in PAGE_PERMISSION_TYPES
|
|
2028
|
+
if codename not in {"add_page", "change_page", "delete_page", "view_page"}
|
|
2029
|
+
]
|
|
2030
|
+
|
|
2031
|
+
|
|
2032
|
+
# set module path of Page so that when Sphinx autodoc sees Page in type annotations
|
|
2033
|
+
# it won't complain that there's no target for wagtail.models.pages.Page
|
|
2034
|
+
Page.__module__ = "wagtail.models"
|
|
2035
|
+
|
|
2036
|
+
|
|
2037
|
+
class GroupPagePermissionManager(models.Manager):
|
|
2038
|
+
def create(self, **kwargs):
|
|
2039
|
+
# Simplify creation of GroupPagePermission objects by allowing one
|
|
2040
|
+
# of permission or permission_type to be passed in.
|
|
2041
|
+
permission = kwargs.get("permission")
|
|
2042
|
+
permission_type = kwargs.pop("permission_type", None)
|
|
2043
|
+
if not permission and permission_type:
|
|
2044
|
+
kwargs["permission"] = Permission.objects.get(
|
|
2045
|
+
content_type=get_default_page_content_type(),
|
|
2046
|
+
codename=f"{permission_type}_page",
|
|
2047
|
+
)
|
|
2048
|
+
return super().create(**kwargs)
|
|
2049
|
+
|
|
2050
|
+
|
|
2051
|
+
class GroupPagePermission(models.Model):
|
|
2052
|
+
group = models.ForeignKey(
|
|
2053
|
+
Group,
|
|
2054
|
+
verbose_name=_("group"),
|
|
2055
|
+
related_name="page_permissions",
|
|
2056
|
+
on_delete=models.CASCADE,
|
|
2057
|
+
)
|
|
2058
|
+
page = models.ForeignKey(
|
|
2059
|
+
"Page",
|
|
2060
|
+
verbose_name=_("page"),
|
|
2061
|
+
related_name="group_permissions",
|
|
2062
|
+
on_delete=models.CASCADE,
|
|
2063
|
+
)
|
|
2064
|
+
permission = models.ForeignKey(
|
|
2065
|
+
Permission,
|
|
2066
|
+
verbose_name=_("permission"),
|
|
2067
|
+
on_delete=models.CASCADE,
|
|
2068
|
+
)
|
|
2069
|
+
|
|
2070
|
+
objects = GroupPagePermissionManager()
|
|
2071
|
+
|
|
2072
|
+
class Meta:
|
|
2073
|
+
constraints = [
|
|
2074
|
+
models.UniqueConstraint(
|
|
2075
|
+
fields=("group", "page", "permission"),
|
|
2076
|
+
name="unique_permission",
|
|
2077
|
+
),
|
|
2078
|
+
]
|
|
2079
|
+
verbose_name = _("group page permission")
|
|
2080
|
+
verbose_name_plural = _("group page permissions")
|
|
2081
|
+
|
|
2082
|
+
def __str__(self):
|
|
2083
|
+
return "Group %d ('%s') has permission '%s' on page %d ('%s')" % (
|
|
2084
|
+
self.group.id,
|
|
2085
|
+
self.group,
|
|
2086
|
+
self.permission.codename,
|
|
2087
|
+
self.page.id,
|
|
2088
|
+
self.page,
|
|
2089
|
+
)
|
|
2090
|
+
|
|
2091
|
+
|
|
2092
|
+
class PagePermissionTester:
|
|
2093
|
+
def __init__(self, user, page):
|
|
2094
|
+
from wagtail.permissions import page_permission_policy
|
|
2095
|
+
|
|
2096
|
+
self.user = user
|
|
2097
|
+
self.permission_policy = page_permission_policy
|
|
2098
|
+
self.page = page
|
|
2099
|
+
self.page_is_root = page.depth == 1 # Equivalent to page.is_root()
|
|
2100
|
+
|
|
2101
|
+
if self.user.is_active and not self.user.is_superuser:
|
|
2102
|
+
self.permissions = {
|
|
2103
|
+
# Get the 'action' part of the permission codename, e.g.
|
|
2104
|
+
# 'add' instead of 'add_page'
|
|
2105
|
+
perm.permission.codename.rsplit("_", maxsplit=1)[0]
|
|
2106
|
+
for perm in self.permission_policy.get_cached_permissions_for_user(user)
|
|
2107
|
+
if self.page.path.startswith(perm.page.path)
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
def user_has_lock(self):
|
|
2111
|
+
return self.page.locked_by_id == self.user.pk
|
|
2112
|
+
|
|
2113
|
+
def page_locked(self):
|
|
2114
|
+
lock = self.page.get_lock()
|
|
2115
|
+
return lock and lock.for_user(self.user)
|
|
2116
|
+
|
|
2117
|
+
def can_add_subpage(self):
|
|
2118
|
+
if not self.user.is_active:
|
|
2119
|
+
return False
|
|
2120
|
+
specific_class = self.page.specific_class
|
|
2121
|
+
if specific_class is None or not specific_class.creatable_subpage_models():
|
|
2122
|
+
return False
|
|
2123
|
+
return self.user.is_superuser or ("add" in self.permissions)
|
|
2124
|
+
|
|
2125
|
+
def can_edit(self):
|
|
2126
|
+
if not self.user.is_active:
|
|
2127
|
+
return False
|
|
2128
|
+
|
|
2129
|
+
if (
|
|
2130
|
+
self.page_is_root
|
|
2131
|
+
): # root node is not a page and can never be edited, even by superusers
|
|
2132
|
+
return False
|
|
2133
|
+
|
|
2134
|
+
if self.user.is_superuser:
|
|
2135
|
+
return True
|
|
2136
|
+
|
|
2137
|
+
if "change" in self.permissions:
|
|
2138
|
+
return True
|
|
2139
|
+
|
|
2140
|
+
if "add" in self.permissions and self.page.owner_id == self.user.pk:
|
|
2141
|
+
return True
|
|
2142
|
+
|
|
2143
|
+
current_workflow_task = self.page.current_workflow_task
|
|
2144
|
+
if current_workflow_task:
|
|
2145
|
+
if current_workflow_task.user_can_access_editor(self.page, self.user):
|
|
2146
|
+
return True
|
|
2147
|
+
|
|
2148
|
+
return False
|
|
2149
|
+
|
|
2150
|
+
def can_delete(self, ignore_bulk=False):
|
|
2151
|
+
if not self.user.is_active:
|
|
2152
|
+
return False
|
|
2153
|
+
|
|
2154
|
+
if (
|
|
2155
|
+
self.page_is_root
|
|
2156
|
+
): # root node is not a page and can never be deleted, even by superusers
|
|
2157
|
+
return False
|
|
2158
|
+
|
|
2159
|
+
if self.user.is_superuser:
|
|
2160
|
+
# superusers require no further checks
|
|
2161
|
+
return True
|
|
2162
|
+
|
|
2163
|
+
# if the user does not have bulk_delete permission, they may only delete leaf pages
|
|
2164
|
+
if (
|
|
2165
|
+
"bulk_delete" not in self.permissions
|
|
2166
|
+
and not self.page.is_leaf()
|
|
2167
|
+
and not ignore_bulk
|
|
2168
|
+
):
|
|
2169
|
+
return False
|
|
2170
|
+
|
|
2171
|
+
if "change" in self.permissions:
|
|
2172
|
+
# if the user does not have publish permission, we also need to confirm that there
|
|
2173
|
+
# are no published pages here
|
|
2174
|
+
if "publish" not in self.permissions:
|
|
2175
|
+
pages_to_delete = self.page.get_descendants(inclusive=True)
|
|
2176
|
+
if pages_to_delete.live().exists():
|
|
2177
|
+
return False
|
|
2178
|
+
|
|
2179
|
+
return True
|
|
2180
|
+
|
|
2181
|
+
elif "add" in self.permissions:
|
|
2182
|
+
pages_to_delete = self.page.get_descendants(inclusive=True)
|
|
2183
|
+
if "publish" in self.permissions:
|
|
2184
|
+
# we don't care about live state, but all pages must be owned by this user
|
|
2185
|
+
# (i.e. eliminating pages owned by this user must give us the empty set)
|
|
2186
|
+
return not pages_to_delete.exclude(owner=self.user).exists()
|
|
2187
|
+
else:
|
|
2188
|
+
# all pages must be owned by this user and non-live
|
|
2189
|
+
# (i.e. eliminating non-live pages owned by this user must give us the empty set)
|
|
2190
|
+
return not pages_to_delete.exclude(live=False, owner=self.user).exists()
|
|
2191
|
+
|
|
2192
|
+
else:
|
|
2193
|
+
return False
|
|
2194
|
+
|
|
2195
|
+
def can_unpublish(self):
|
|
2196
|
+
if not self.user.is_active:
|
|
2197
|
+
return False
|
|
2198
|
+
if (not self.page.live) or self.page_is_root:
|
|
2199
|
+
return False
|
|
2200
|
+
if self.page_locked():
|
|
2201
|
+
return False
|
|
2202
|
+
|
|
2203
|
+
return self.user.is_superuser or ("publish" in self.permissions)
|
|
2204
|
+
|
|
2205
|
+
def can_publish(self):
|
|
2206
|
+
if not self.user.is_active:
|
|
2207
|
+
return False
|
|
2208
|
+
if self.page_is_root:
|
|
2209
|
+
return False
|
|
2210
|
+
|
|
2211
|
+
return self.user.is_superuser or ("publish" in self.permissions)
|
|
2212
|
+
|
|
2213
|
+
def can_submit_for_moderation(self):
|
|
2214
|
+
return (
|
|
2215
|
+
not self.page_locked()
|
|
2216
|
+
and self.page.has_workflow
|
|
2217
|
+
and not self.page.workflow_in_progress
|
|
2218
|
+
)
|
|
2219
|
+
|
|
2220
|
+
def can_set_view_restrictions(self):
|
|
2221
|
+
return self.can_publish()
|
|
2222
|
+
|
|
2223
|
+
def can_unschedule(self):
|
|
2224
|
+
return self.can_publish()
|
|
2225
|
+
|
|
2226
|
+
def can_lock(self):
|
|
2227
|
+
if self.user.is_superuser:
|
|
2228
|
+
return True
|
|
2229
|
+
current_workflow_task = self.page.current_workflow_task
|
|
2230
|
+
if current_workflow_task:
|
|
2231
|
+
return current_workflow_task.user_can_lock(self.page, self.user)
|
|
2232
|
+
|
|
2233
|
+
if "lock" in self.permissions:
|
|
2234
|
+
return True
|
|
2235
|
+
|
|
2236
|
+
return False
|
|
2237
|
+
|
|
2238
|
+
def can_unlock(self):
|
|
2239
|
+
if self.user.is_superuser:
|
|
2240
|
+
return True
|
|
2241
|
+
|
|
2242
|
+
if self.user_has_lock():
|
|
2243
|
+
return True
|
|
2244
|
+
|
|
2245
|
+
current_workflow_task = self.page.current_workflow_task
|
|
2246
|
+
if current_workflow_task:
|
|
2247
|
+
return current_workflow_task.user_can_unlock(self.page, self.user)
|
|
2248
|
+
|
|
2249
|
+
if "unlock" in self.permissions:
|
|
2250
|
+
return True
|
|
2251
|
+
|
|
2252
|
+
return False
|
|
2253
|
+
|
|
2254
|
+
def can_publish_subpage(self):
|
|
2255
|
+
"""
|
|
2256
|
+
Niggly special case for creating and publishing a page in one go.
|
|
2257
|
+
Differs from can_publish in that we want to be able to publish subpages of root, but not
|
|
2258
|
+
to be able to publish root itself. (Also, can_publish_subpage returns false if the page
|
|
2259
|
+
does not allow subpages at all.)
|
|
2260
|
+
"""
|
|
2261
|
+
if not self.user.is_active:
|
|
2262
|
+
return False
|
|
2263
|
+
specific_class = self.page.specific_class
|
|
2264
|
+
if specific_class is None or not specific_class.creatable_subpage_models():
|
|
2265
|
+
return False
|
|
2266
|
+
|
|
2267
|
+
return self.user.is_superuser or ("publish" in self.permissions)
|
|
2268
|
+
|
|
2269
|
+
def can_reorder_children(self):
|
|
2270
|
+
"""
|
|
2271
|
+
Reorder permission checking is similar to publishing a subpage, since it immediately
|
|
2272
|
+
affects published pages. However, it shouldn't care about the 'creatability' of
|
|
2273
|
+
page types, because the action only ever updates existing pages.
|
|
2274
|
+
"""
|
|
2275
|
+
if not self.user.is_active:
|
|
2276
|
+
return False
|
|
2277
|
+
return self.user.is_superuser or ("publish" in self.permissions)
|
|
2278
|
+
|
|
2279
|
+
def can_move(self):
|
|
2280
|
+
"""
|
|
2281
|
+
Moving a page should be logically equivalent to deleting and re-adding it (and all its children).
|
|
2282
|
+
As such, the permission test for 'can this be moved at all?' should be the same as for deletion.
|
|
2283
|
+
(Further constraints will then apply on where it can be moved *to*.)
|
|
2284
|
+
"""
|
|
2285
|
+
return self.can_delete(ignore_bulk=True)
|
|
2286
|
+
|
|
2287
|
+
def can_copy(self):
|
|
2288
|
+
return not self.page_is_root
|
|
2289
|
+
|
|
2290
|
+
def can_move_to(self, destination):
|
|
2291
|
+
# reject the logically impossible cases first
|
|
2292
|
+
if self.page == destination or destination.is_descendant_of(self.page):
|
|
2293
|
+
return False
|
|
2294
|
+
|
|
2295
|
+
# reject moves that are forbidden by subpage_types / parent_page_types rules
|
|
2296
|
+
# (these rules apply to superusers too)
|
|
2297
|
+
# – but only check this if the page is not already under the target parent.
|
|
2298
|
+
# If it already is, then the user is just reordering the page, and we want
|
|
2299
|
+
# to allow it even if the page currently violates the subpage_type /
|
|
2300
|
+
# parent_page_type rules. This can happen if it was either created before
|
|
2301
|
+
# the rules were specified, or it was done programmatically (e.g. to
|
|
2302
|
+
# predefine a set of pages and disallow the creation of new subpages by
|
|
2303
|
+
# setting subpage_types = []).
|
|
2304
|
+
|
|
2305
|
+
if (not self.page.is_child_of(destination)) and (
|
|
2306
|
+
not self.page.specific.can_move_to(destination)
|
|
2307
|
+
):
|
|
2308
|
+
return False
|
|
2309
|
+
|
|
2310
|
+
# shortcut the trivial 'everything' / 'nothing' permissions
|
|
2311
|
+
if not self.user.is_active:
|
|
2312
|
+
return False
|
|
2313
|
+
if self.user.is_superuser:
|
|
2314
|
+
return True
|
|
2315
|
+
|
|
2316
|
+
# check that the page can be moved at all
|
|
2317
|
+
if not self.can_move():
|
|
2318
|
+
return False
|
|
2319
|
+
|
|
2320
|
+
# Inspect permissions on the destination
|
|
2321
|
+
destination_perms = destination.permissions_for_user(self.user)
|
|
2322
|
+
|
|
2323
|
+
# we always need at least add permission in the target
|
|
2324
|
+
if "add" not in destination_perms.permissions:
|
|
2325
|
+
return False
|
|
2326
|
+
|
|
2327
|
+
if self.page.live or self.page.get_descendants().filter(live=True).exists():
|
|
2328
|
+
# moving this page will entail publishing within the destination section
|
|
2329
|
+
return "publish" in destination_perms.permissions
|
|
2330
|
+
else:
|
|
2331
|
+
# no publishing required, so the already-tested 'add' permission is sufficient
|
|
2332
|
+
return True
|
|
2333
|
+
|
|
2334
|
+
def can_copy_to(self, destination, recursive=False):
|
|
2335
|
+
# reject the logically impossible cases first
|
|
2336
|
+
# recursive can't copy to the same tree otherwise it will be on infinite loop
|
|
2337
|
+
if recursive and (
|
|
2338
|
+
self.page == destination or destination.is_descendant_of(self.page)
|
|
2339
|
+
):
|
|
2340
|
+
return False
|
|
2341
|
+
|
|
2342
|
+
# reject inactive users early
|
|
2343
|
+
if not self.user.is_active:
|
|
2344
|
+
return False
|
|
2345
|
+
|
|
2346
|
+
# reject early if pages of this type cannot be created at the destination
|
|
2347
|
+
if not self.page.specific_class.can_create_at(destination):
|
|
2348
|
+
return False
|
|
2349
|
+
|
|
2350
|
+
# skip permission checking for super users
|
|
2351
|
+
if self.user.is_superuser:
|
|
2352
|
+
return True
|
|
2353
|
+
|
|
2354
|
+
# Inspect permissions on the destination
|
|
2355
|
+
destination_perms = destination.permissions_for_user(self.user)
|
|
2356
|
+
|
|
2357
|
+
if not destination.specific_class.creatable_subpage_models():
|
|
2358
|
+
return False
|
|
2359
|
+
|
|
2360
|
+
# we always need at least add permission in the target
|
|
2361
|
+
if "add" not in destination_perms.permissions:
|
|
2362
|
+
return False
|
|
2363
|
+
|
|
2364
|
+
return True
|
|
2365
|
+
|
|
2366
|
+
def can_view_revisions(self):
|
|
2367
|
+
return not self.page_is_root
|
|
2368
|
+
|
|
2369
|
+
|
|
2370
|
+
class PageViewRestriction(BaseViewRestriction):
|
|
2371
|
+
page = models.ForeignKey(
|
|
2372
|
+
"Page",
|
|
2373
|
+
verbose_name=_("page"),
|
|
2374
|
+
related_name="view_restrictions",
|
|
2375
|
+
on_delete=models.CASCADE,
|
|
2376
|
+
)
|
|
2377
|
+
|
|
2378
|
+
passed_view_restrictions_session_key = "passed_page_view_restrictions"
|
|
2379
|
+
|
|
2380
|
+
class Meta:
|
|
2381
|
+
verbose_name = _("page view restriction")
|
|
2382
|
+
verbose_name_plural = _("page view restrictions")
|
|
2383
|
+
|
|
2384
|
+
def save(self, user=None, **kwargs):
|
|
2385
|
+
"""
|
|
2386
|
+
Custom save handler to include logging.
|
|
2387
|
+
:param user: the user add/updating the view restriction
|
|
2388
|
+
:param specific_instance: the specific model instance the restriction applies to
|
|
2389
|
+
"""
|
|
2390
|
+
specific_instance = self.page.specific
|
|
2391
|
+
is_new = self.id is None
|
|
2392
|
+
super().save(**kwargs)
|
|
2393
|
+
|
|
2394
|
+
if specific_instance:
|
|
2395
|
+
log(
|
|
2396
|
+
instance=specific_instance,
|
|
2397
|
+
action="wagtail.view_restriction.create"
|
|
2398
|
+
if is_new
|
|
2399
|
+
else "wagtail.view_restriction.edit",
|
|
2400
|
+
user=user,
|
|
2401
|
+
data={
|
|
2402
|
+
"restriction": {
|
|
2403
|
+
"type": self.restriction_type,
|
|
2404
|
+
"title": force_str(
|
|
2405
|
+
dict(self.RESTRICTION_CHOICES).get(self.restriction_type)
|
|
2406
|
+
),
|
|
2407
|
+
}
|
|
2408
|
+
},
|
|
2409
|
+
)
|
|
2410
|
+
|
|
2411
|
+
def delete(self, user=None, **kwargs):
|
|
2412
|
+
"""
|
|
2413
|
+
Custom delete handler to aid in logging.
|
|
2414
|
+
:param user: the user removing the view restriction
|
|
2415
|
+
"""
|
|
2416
|
+
specific_instance = self.page.specific
|
|
2417
|
+
if specific_instance:
|
|
2418
|
+
removed_restriction_type = PageViewRestriction.objects.filter(
|
|
2419
|
+
id=self.id
|
|
2420
|
+
).values_list("restriction_type", flat=True)[0]
|
|
2421
|
+
log(
|
|
2422
|
+
instance=specific_instance,
|
|
2423
|
+
action="wagtail.view_restriction.delete",
|
|
2424
|
+
user=user,
|
|
2425
|
+
data={
|
|
2426
|
+
"restriction": {
|
|
2427
|
+
"type": self.restriction_type,
|
|
2428
|
+
"title": force_str(
|
|
2429
|
+
dict(self.RESTRICTION_CHOICES).get(removed_restriction_type)
|
|
2430
|
+
),
|
|
2431
|
+
}
|
|
2432
|
+
},
|
|
2433
|
+
)
|
|
2434
|
+
return super().delete(**kwargs)
|
|
2435
|
+
|
|
2436
|
+
|
|
2437
|
+
class WorkflowPage(models.Model):
|
|
2438
|
+
page = models.OneToOneField(
|
|
2439
|
+
"Page",
|
|
2440
|
+
verbose_name=_("page"),
|
|
2441
|
+
on_delete=models.CASCADE,
|
|
2442
|
+
primary_key=True,
|
|
2443
|
+
unique=True,
|
|
2444
|
+
)
|
|
2445
|
+
workflow = models.ForeignKey(
|
|
2446
|
+
"Workflow",
|
|
2447
|
+
related_name="workflow_pages",
|
|
2448
|
+
verbose_name=_("workflow"),
|
|
2449
|
+
on_delete=models.CASCADE,
|
|
2450
|
+
)
|
|
2451
|
+
|
|
2452
|
+
def get_pages(self):
|
|
2453
|
+
"""
|
|
2454
|
+
Returns a queryset of pages that are affected by this ``WorkflowPage`` link.
|
|
2455
|
+
|
|
2456
|
+
This includes all descendants of the page excluding any that have other ``WorkflowPage``(s).
|
|
2457
|
+
"""
|
|
2458
|
+
descendant_pages = Page.objects.descendant_of(self.page, inclusive=True)
|
|
2459
|
+
descendant_workflow_pages = WorkflowPage.objects.filter(
|
|
2460
|
+
page_id__in=descendant_pages.values_list("id", flat=True)
|
|
2461
|
+
).exclude(pk=self.pk)
|
|
2462
|
+
|
|
2463
|
+
for path, depth in descendant_workflow_pages.values_list(
|
|
2464
|
+
"page__path", "page__depth"
|
|
2465
|
+
):
|
|
2466
|
+
descendant_pages = descendant_pages.exclude(
|
|
2467
|
+
path__startswith=path, depth__gte=depth
|
|
2468
|
+
)
|
|
2469
|
+
|
|
2470
|
+
return descendant_pages
|
|
2471
|
+
|
|
2472
|
+
class Meta:
|
|
2473
|
+
verbose_name = _("workflow page")
|
|
2474
|
+
verbose_name_plural = _("workflow pages")
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
class PageLogEntryQuerySet(LogEntryQuerySet):
|
|
2478
|
+
def get_content_type_ids(self):
|
|
2479
|
+
# for reporting purposes, pages of all types are combined under a single "Page"
|
|
2480
|
+
# object type
|
|
2481
|
+
if self.exists():
|
|
2482
|
+
return {ContentType.objects.get_for_model(Page).pk}
|
|
2483
|
+
else:
|
|
2484
|
+
return set()
|
|
2485
|
+
|
|
2486
|
+
def filter_on_content_type(self, content_type):
|
|
2487
|
+
if content_type == ContentType.objects.get_for_model(Page):
|
|
2488
|
+
return self
|
|
2489
|
+
else:
|
|
2490
|
+
return self.none()
|
|
2491
|
+
|
|
2492
|
+
|
|
2493
|
+
class PageLogEntryManager(BaseLogEntryManager):
|
|
2494
|
+
def get_queryset(self):
|
|
2495
|
+
return PageLogEntryQuerySet(self.model, using=self._db)
|
|
2496
|
+
|
|
2497
|
+
def get_instance_title(self, instance):
|
|
2498
|
+
return instance.specific_deferred.get_admin_display_title()
|
|
2499
|
+
|
|
2500
|
+
def log_action(self, instance, action, **kwargs):
|
|
2501
|
+
kwargs.update(page=instance)
|
|
2502
|
+
return super().log_action(instance, action, **kwargs)
|
|
2503
|
+
|
|
2504
|
+
def viewable_by_user(self, user):
|
|
2505
|
+
from wagtail.permissions import page_permission_policy
|
|
2506
|
+
|
|
2507
|
+
explorable_instances = page_permission_policy.explorable_instances(user)
|
|
2508
|
+
q = Q(page__in=explorable_instances.values_list("pk", flat=True))
|
|
2509
|
+
|
|
2510
|
+
root_page_permissions = Page.get_first_root_node().permissions_for_user(user)
|
|
2511
|
+
if (
|
|
2512
|
+
user.is_superuser
|
|
2513
|
+
or root_page_permissions.can_add_subpage()
|
|
2514
|
+
or root_page_permissions.can_edit()
|
|
2515
|
+
):
|
|
2516
|
+
# Include deleted entries
|
|
2517
|
+
q = q | Q(
|
|
2518
|
+
page_id__in=Subquery(
|
|
2519
|
+
PageLogEntry.objects.filter(deleted=True).values("page_id")
|
|
2520
|
+
)
|
|
2521
|
+
)
|
|
2522
|
+
|
|
2523
|
+
return PageLogEntry.objects.filter(q)
|
|
2524
|
+
|
|
2525
|
+
def for_instance(self, instance):
|
|
2526
|
+
return self.filter(page=instance)
|
|
2527
|
+
|
|
2528
|
+
|
|
2529
|
+
class PageLogEntry(BaseLogEntry):
|
|
2530
|
+
page = models.ForeignKey(
|
|
2531
|
+
"wagtailcore.Page",
|
|
2532
|
+
on_delete=models.DO_NOTHING,
|
|
2533
|
+
db_constraint=False,
|
|
2534
|
+
related_name="+",
|
|
2535
|
+
)
|
|
2536
|
+
|
|
2537
|
+
objects = PageLogEntryManager()
|
|
2538
|
+
|
|
2539
|
+
class Meta:
|
|
2540
|
+
ordering = ["-timestamp", "-id"]
|
|
2541
|
+
verbose_name = _("page log entry")
|
|
2542
|
+
verbose_name_plural = _("page log entries")
|
|
2543
|
+
|
|
2544
|
+
def __str__(self):
|
|
2545
|
+
return "PageLogEntry %d: '%s' on '%s' with id %s" % (
|
|
2546
|
+
self.pk,
|
|
2547
|
+
self.action,
|
|
2548
|
+
self.object_verbose_name(),
|
|
2549
|
+
self.page_id,
|
|
2550
|
+
)
|
|
2551
|
+
|
|
2552
|
+
@cached_property
|
|
2553
|
+
def object_id(self):
|
|
2554
|
+
return self.page_id
|
|
2555
|
+
|
|
2556
|
+
@cached_property
|
|
2557
|
+
def message(self):
|
|
2558
|
+
# for page log entries, the 'edit' action should show as 'Draft saved'
|
|
2559
|
+
if self.action == "wagtail.edit":
|
|
2560
|
+
return _("Draft saved")
|
|
2561
|
+
else:
|
|
2562
|
+
return super().message
|
|
2563
|
+
|
|
2564
|
+
|
|
2565
|
+
class Comment(ClusterableModel):
|
|
2566
|
+
"""
|
|
2567
|
+
A comment on a field, or a field within a streamfield block
|
|
2568
|
+
"""
|
|
2569
|
+
|
|
2570
|
+
page = ParentalKey(
|
|
2571
|
+
Page, on_delete=models.CASCADE, related_name=COMMENTS_RELATION_NAME
|
|
2572
|
+
)
|
|
2573
|
+
user = models.ForeignKey(
|
|
2574
|
+
settings.AUTH_USER_MODEL,
|
|
2575
|
+
on_delete=models.CASCADE,
|
|
2576
|
+
related_name=COMMENTS_RELATION_NAME,
|
|
2577
|
+
)
|
|
2578
|
+
text = models.TextField()
|
|
2579
|
+
|
|
2580
|
+
contentpath = models.TextField()
|
|
2581
|
+
# This stores the field or field within a streamfield block that the comment is applied on, in the form: 'field', or 'field.block_id.field'
|
|
2582
|
+
# This must be unchanging across all revisions, so we will not support (current-format) ListBlock or the contents of InlinePanels initially.
|
|
2583
|
+
|
|
2584
|
+
position = models.TextField(blank=True)
|
|
2585
|
+
# This stores the position within a field, to be interpreted by the field's frontend widget. It may change between revisions
|
|
2586
|
+
|
|
2587
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
2588
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
2589
|
+
|
|
2590
|
+
revision_created = models.ForeignKey(
|
|
2591
|
+
Revision,
|
|
2592
|
+
on_delete=models.CASCADE,
|
|
2593
|
+
related_name="created_comments",
|
|
2594
|
+
null=True,
|
|
2595
|
+
blank=True,
|
|
2596
|
+
)
|
|
2597
|
+
|
|
2598
|
+
resolved_at = models.DateTimeField(null=True, blank=True)
|
|
2599
|
+
resolved_by = models.ForeignKey(
|
|
2600
|
+
settings.AUTH_USER_MODEL,
|
|
2601
|
+
on_delete=models.SET_NULL,
|
|
2602
|
+
related_name="comments_resolved",
|
|
2603
|
+
null=True,
|
|
2604
|
+
blank=True,
|
|
2605
|
+
)
|
|
2606
|
+
|
|
2607
|
+
class Meta:
|
|
2608
|
+
verbose_name = _("comment")
|
|
2609
|
+
verbose_name_plural = _("comments")
|
|
2610
|
+
|
|
2611
|
+
def __str__(self):
|
|
2612
|
+
return "Comment on Page '{}', left by {}: '{}'".format(
|
|
2613
|
+
self.page, self.user, self.text
|
|
2614
|
+
)
|
|
2615
|
+
|
|
2616
|
+
def save(self, update_position=False, **kwargs):
|
|
2617
|
+
# Don't save the position unless specifically instructed to, as the position will normally be retrieved from the revision
|
|
2618
|
+
update_fields = kwargs.pop("update_fields", None)
|
|
2619
|
+
if not update_position and (
|
|
2620
|
+
not update_fields or "position" not in update_fields
|
|
2621
|
+
):
|
|
2622
|
+
if self.id:
|
|
2623
|
+
# The instance is already saved; we can use `update_fields`
|
|
2624
|
+
update_fields = (
|
|
2625
|
+
update_fields if update_fields else self._meta.get_fields()
|
|
2626
|
+
)
|
|
2627
|
+
update_fields = [
|
|
2628
|
+
field.name
|
|
2629
|
+
for field in update_fields
|
|
2630
|
+
if field.name not in {"position", "id"}
|
|
2631
|
+
]
|
|
2632
|
+
else:
|
|
2633
|
+
# This is a new instance, we have to preserve and then restore the position via a variable
|
|
2634
|
+
position = self.position
|
|
2635
|
+
result = super().save(**kwargs)
|
|
2636
|
+
self.position = position
|
|
2637
|
+
return result
|
|
2638
|
+
return super().save(update_fields=update_fields, **kwargs)
|
|
2639
|
+
|
|
2640
|
+
def _log(self, action, page_revision=None, user=None):
|
|
2641
|
+
log(
|
|
2642
|
+
instance=self.page,
|
|
2643
|
+
action=action,
|
|
2644
|
+
user=user,
|
|
2645
|
+
revision=page_revision,
|
|
2646
|
+
data={
|
|
2647
|
+
"comment": {
|
|
2648
|
+
"id": self.pk,
|
|
2649
|
+
"contentpath": self.contentpath,
|
|
2650
|
+
"text": self.text,
|
|
2651
|
+
}
|
|
2652
|
+
},
|
|
2653
|
+
)
|
|
2654
|
+
|
|
2655
|
+
def log_create(self, **kwargs):
|
|
2656
|
+
self._log("wagtail.comments.create", **kwargs)
|
|
2657
|
+
|
|
2658
|
+
def log_edit(self, **kwargs):
|
|
2659
|
+
self._log("wagtail.comments.edit", **kwargs)
|
|
2660
|
+
|
|
2661
|
+
def log_resolve(self, **kwargs):
|
|
2662
|
+
self._log("wagtail.comments.resolve", **kwargs)
|
|
2663
|
+
|
|
2664
|
+
def log_delete(self, **kwargs):
|
|
2665
|
+
self._log("wagtail.comments.delete", **kwargs)
|
|
2666
|
+
|
|
2667
|
+
def has_valid_contentpath(self, page):
|
|
2668
|
+
"""
|
|
2669
|
+
Return True if this comment's contentpath corresponds to a valid field or
|
|
2670
|
+
StreamField block on the given page object.
|
|
2671
|
+
"""
|
|
2672
|
+
field_name, *remainder = self.contentpath.split(".")
|
|
2673
|
+
try:
|
|
2674
|
+
field = page._meta.get_field(field_name)
|
|
2675
|
+
except FieldDoesNotExist:
|
|
2676
|
+
return False
|
|
2677
|
+
|
|
2678
|
+
if not remainder:
|
|
2679
|
+
# comment applies to the field as a whole
|
|
2680
|
+
return True
|
|
2681
|
+
|
|
2682
|
+
if not isinstance(field, StreamField):
|
|
2683
|
+
# only StreamField supports content paths that are deeper than one level
|
|
2684
|
+
return False
|
|
2685
|
+
|
|
2686
|
+
stream_value = getattr(page, field_name)
|
|
2687
|
+
block = field.get_block_by_content_path(stream_value, remainder)
|
|
2688
|
+
# content path is valid if this returns a BoundBlock rather than None
|
|
2689
|
+
return bool(block)
|
|
2690
|
+
|
|
2691
|
+
|
|
2692
|
+
class CommentReply(models.Model):
|
|
2693
|
+
comment = ParentalKey(Comment, on_delete=models.CASCADE, related_name="replies")
|
|
2694
|
+
user = models.ForeignKey(
|
|
2695
|
+
settings.AUTH_USER_MODEL,
|
|
2696
|
+
on_delete=models.CASCADE,
|
|
2697
|
+
related_name="comment_replies",
|
|
2698
|
+
)
|
|
2699
|
+
text = models.TextField()
|
|
2700
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
2701
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
2702
|
+
|
|
2703
|
+
class Meta:
|
|
2704
|
+
verbose_name = _("comment reply")
|
|
2705
|
+
verbose_name_plural = _("comment replies")
|
|
2706
|
+
|
|
2707
|
+
def __str__(self):
|
|
2708
|
+
return f"CommentReply left by '{self.user}': '{self.text}'"
|
|
2709
|
+
|
|
2710
|
+
def _log(self, action, page_revision=None, user=None):
|
|
2711
|
+
log(
|
|
2712
|
+
instance=self.comment.page,
|
|
2713
|
+
action=action,
|
|
2714
|
+
user=user,
|
|
2715
|
+
revision=page_revision,
|
|
2716
|
+
data={
|
|
2717
|
+
"comment": {
|
|
2718
|
+
"id": self.comment.pk,
|
|
2719
|
+
"contentpath": self.comment.contentpath,
|
|
2720
|
+
"text": self.comment.text,
|
|
2721
|
+
},
|
|
2722
|
+
"reply": {
|
|
2723
|
+
"id": self.pk,
|
|
2724
|
+
"text": self.text,
|
|
2725
|
+
},
|
|
2726
|
+
},
|
|
2727
|
+
)
|
|
2728
|
+
|
|
2729
|
+
def log_create(self, **kwargs):
|
|
2730
|
+
self._log("wagtail.comments.create_reply", **kwargs)
|
|
2731
|
+
|
|
2732
|
+
def log_edit(self, **kwargs):
|
|
2733
|
+
self._log("wagtail.comments.edit_reply", **kwargs)
|
|
2734
|
+
|
|
2735
|
+
def log_delete(self, **kwargs):
|
|
2736
|
+
self._log("wagtail.comments.delete_reply", **kwargs)
|
|
2737
|
+
|
|
2738
|
+
|
|
2739
|
+
class PageSubscription(models.Model):
|
|
2740
|
+
user = models.ForeignKey(
|
|
2741
|
+
settings.AUTH_USER_MODEL,
|
|
2742
|
+
on_delete=models.CASCADE,
|
|
2743
|
+
related_name="page_subscriptions",
|
|
2744
|
+
)
|
|
2745
|
+
page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name="subscribers")
|
|
2746
|
+
|
|
2747
|
+
comment_notifications = models.BooleanField()
|
|
2748
|
+
|
|
2749
|
+
wagtail_reference_index_ignore = True
|
|
2750
|
+
|
|
2751
|
+
class Meta:
|
|
2752
|
+
unique_together = [
|
|
2753
|
+
("page", "user"),
|
|
2754
|
+
]
|