wagtail 7.2.2__py3-none-any.whl → 7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wagtail/__init__.py +1 -1
- wagtail/actions/copy_for_translation.py +4 -2
- wagtail/admin/action_menu.py +4 -1
- wagtail/admin/api/actions/convert_alias.py +2 -2
- wagtail/admin/api/actions/copy.py +2 -2
- wagtail/admin/api/actions/copy_for_translation.py +2 -2
- wagtail/admin/api/actions/create_alias.py +2 -2
- wagtail/admin/api/actions/delete.py +1 -1
- wagtail/admin/api/actions/move.py +1 -1
- wagtail/admin/api/actions/publish.py +2 -2
- wagtail/admin/api/actions/revert_to_page_revision.py +2 -2
- wagtail/admin/api/actions/unpublish.py +1 -1
- wagtail/admin/api/filters.py +2 -2
- wagtail/admin/compare.py +22 -0
- wagtail/admin/forms/account.py +52 -1
- wagtail/admin/forms/comments.py +53 -0
- wagtail/admin/forms/models.py +36 -0
- wagtail/admin/forms/pages.py +7 -0
- wagtail/admin/forms/workflows.py +5 -2
- wagtail/admin/icons.py +4 -3
- wagtail/admin/locale/af/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/af/LC_MESSAGES/djangojs.po +8 -1
- wagtail/admin/locale/ar/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ar/LC_MESSAGES/django.po +42 -16
- wagtail/admin/locale/ar/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ar/LC_MESSAGES/djangojs.po +20 -11
- wagtail/admin/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/az_AZ/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/az_AZ/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/be/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/be/LC_MESSAGES/djangojs.po +21 -3
- wagtail/admin/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/bg/LC_MESSAGES/django.po +299 -2
- wagtail/admin/locale/bg/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/bg/LC_MESSAGES/djangojs.po +37 -3
- wagtail/admin/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/bn/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/bn/LC_MESSAGES/djangojs.po +9 -3
- wagtail/admin/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ca/LC_MESSAGES/django.po +36 -18
- wagtail/admin/locale/ca/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ca/LC_MESSAGES/djangojs.po +34 -12
- wagtail/admin/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/cs/LC_MESSAGES/django.po +1 -7
- wagtail/admin/locale/cs/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/cs/LC_MESSAGES/djangojs.po +22 -4
- 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 +15 -3
- wagtail/admin/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/da/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/da/LC_MESSAGES/djangojs.po +19 -4
- wagtail/admin/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/de/LC_MESSAGES/django.po +201 -21
- wagtail/admin/locale/de/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/de/LC_MESSAGES/djangojs.po +53 -14
- wagtail/admin/locale/dv/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/dv/LC_MESSAGES/django.po +7 -16
- wagtail/admin/locale/dv/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/dv/LC_MESSAGES/djangojs.po +20 -2
- wagtail/admin/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/el/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/el/LC_MESSAGES/djangojs.po +22 -13
- wagtail/admin/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/en/LC_MESSAGES/django.po +262 -234
- wagtail/admin/locale/en/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +72 -43
- wagtail/admin/locale/en_IN/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/en_IN/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/es/LC_MESSAGES/django.po +1 -10
- wagtail/admin/locale/es/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/es/LC_MESSAGES/djangojs.po +22 -13
- wagtail/admin/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/es_419/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/es_419/LC_MESSAGES/djangojs.po +8 -2
- wagtail/admin/locale/es_VE/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/es_VE/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/es_VE/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/et/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/et/LC_MESSAGES/djangojs.po +17 -2
- wagtail/admin/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/eu/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/eu/LC_MESSAGES/djangojs.po +8 -2
- wagtail/admin/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/fa/LC_MESSAGES/django.po +5 -11
- wagtail/admin/locale/fa/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/fa/LC_MESSAGES/djangojs.po +18 -3
- wagtail/admin/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/fi/LC_MESSAGES/django.po +1 -12
- wagtail/admin/locale/fi/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/fi/LC_MESSAGES/djangojs.po +22 -12
- wagtail/admin/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/fr/LC_MESSAGES/django.po +38 -21
- wagtail/admin/locale/fr/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/fr/LC_MESSAGES/djangojs.po +35 -14
- wagtail/admin/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/gl/LC_MESSAGES/django.po +36 -18
- wagtail/admin/locale/gl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/gl/LC_MESSAGES/djangojs.po +34 -12
- wagtail/admin/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/he_IL/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/he_IL/LC_MESSAGES/djangojs.po +20 -3
- wagtail/admin/locale/hi/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/hi/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/hr_HR/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/hr_HR/LC_MESSAGES/djangojs.po +17 -2
- wagtail/admin/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/ht/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ht/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/hu/LC_MESSAGES/django.po +7 -18
- wagtail/admin/locale/hu/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/hu/LC_MESSAGES/djangojs.po +21 -11
- wagtail/admin/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/id_ID/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/id_ID/LC_MESSAGES/djangojs.po +18 -3
- wagtail/admin/locale/is_IS/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/is_IS/LC_MESSAGES/django.po +7 -17
- wagtail/admin/locale/is_IS/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/is_IS/LC_MESSAGES/djangojs.po +21 -12
- wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/it/LC_MESSAGES/django.po +599 -19
- wagtail/admin/locale/it/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/it/LC_MESSAGES/djangojs.po +73 -13
- wagtail/admin/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/ja/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ja/LC_MESSAGES/djangojs.po +23 -3
- wagtail/admin/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/ka/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ka/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ko/LC_MESSAGES/django.po +1 -11
- wagtail/admin/locale/ko/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ko/LC_MESSAGES/djangojs.po +23 -13
- wagtail/admin/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/lt/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/lt/LC_MESSAGES/djangojs.po +21 -12
- wagtail/admin/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/lv/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/lv/LC_MESSAGES/djangojs.po +22 -4
- 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 +14 -2
- wagtail/admin/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/mn/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/mn/LC_MESSAGES/djangojs.po +17 -2
- wagtail/admin/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/my/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/my/LC_MESSAGES/djangojs.po +8 -2
- wagtail/admin/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/nb/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/nb/LC_MESSAGES/djangojs.po +18 -3
- wagtail/admin/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/nl/LC_MESSAGES/django.po +38 -21
- wagtail/admin/locale/nl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/nl/LC_MESSAGES/djangojs.po +36 -13
- wagtail/admin/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/pl/LC_MESSAGES/django.po +1 -10
- wagtail/admin/locale/pl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/pl/LC_MESSAGES/djangojs.po +21 -12
- wagtail/admin/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/pt_BR/LC_MESSAGES/django.po +7 -16
- wagtail/admin/locale/pt_BR/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/pt_BR/LC_MESSAGES/djangojs.po +21 -13
- wagtail/admin/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/pt_PT/LC_MESSAGES/django.po +1 -7
- wagtail/admin/locale/pt_PT/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/pt_PT/LC_MESSAGES/djangojs.po +18 -12
- wagtail/admin/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ro/LC_MESSAGES/django.po +7 -16
- wagtail/admin/locale/ro/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ro/LC_MESSAGES/djangojs.po +20 -11
- wagtail/admin/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ru/LC_MESSAGES/django.po +186 -22
- wagtail/admin/locale/ru/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ru/LC_MESSAGES/djangojs.po +53 -14
- wagtail/admin/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/sk_SK/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/sk_SK/LC_MESSAGES/djangojs.po +20 -3
- wagtail/admin/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/sl/LC_MESSAGES/django.po +7 -16
- wagtail/admin/locale/sl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/sl/LC_MESSAGES/djangojs.po +21 -12
- wagtail/admin/locale/sr/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/sr/LC_MESSAGES/djangojs.po +15 -0
- wagtail/admin/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/sv/LC_MESSAGES/django.po +329 -23
- wagtail/admin/locale/sv/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/sv/LC_MESSAGES/djangojs.po +23 -13
- wagtail/admin/locale/ta/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ta/LC_MESSAGES/djangojs.po +5 -2
- wagtail/admin/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/tet/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/tet/LC_MESSAGES/djangojs.po +19 -4
- wagtail/admin/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/th/LC_MESSAGES/django.po +1 -4
- wagtail/admin/locale/th/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/th/LC_MESSAGES/djangojs.po +22 -3
- wagtail/admin/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/tr/LC_MESSAGES/django.po +4 -10
- wagtail/admin/locale/tr/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/tr/LC_MESSAGES/djangojs.po +23 -5
- wagtail/admin/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/tr_TR/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/tr_TR/LC_MESSAGES/djangojs.po +19 -4
- wagtail/admin/locale/ug/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ug/LC_MESSAGES/django.po +7 -16
- wagtail/admin/locale/ug/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/ug/LC_MESSAGES/djangojs.po +20 -11
- wagtail/admin/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/uk/LC_MESSAGES/django.po +5 -11
- wagtail/admin/locale/uk/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/uk/LC_MESSAGES/djangojs.po +22 -14
- wagtail/admin/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/vi/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/vi/LC_MESSAGES/djangojs.po +9 -2
- wagtail/admin/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/admin/locale/zh/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/zh/LC_MESSAGES/djangojs.po +20 -5
- wagtail/admin/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/zh_Hans/LC_MESSAGES/django.po +1 -7
- wagtail/admin/locale/zh_Hans/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/zh_Hans/LC_MESSAGES/djangojs.po +23 -13
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/django.po +1 -7
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/djangojs.po +21 -3
- wagtail/admin/panels/comment_panel.py +1 -51
- wagtail/admin/panels/title_field_panel.py +3 -1
- wagtail/admin/static/wagtailadmin/css/core.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/core.js.LICENSE.txt +1 -1
- wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
- wagtail/admin/static/wagtailadmin/js/privacy-switch.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/userbar.js +1 -1
- wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
- wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
- wagtail/admin/templates/wagtailadmin/base.html +1 -1
- wagtail/admin/templates/wagtailadmin/generic/edit_partials.html +100 -0
- wagtail/admin/templates/wagtailadmin/generic/form.html +26 -5
- wagtail/admin/templates/wagtailadmin/generic/includes/_loaded_revision_inputs.html +3 -0
- wagtail/admin/templates/wagtailadmin/generic/listing_results.html +1 -17
- wagtail/admin/templates/wagtailadmin/pages/create.html +14 -4
- wagtail/admin/templates/wagtailadmin/pages/edit.html +16 -3
- wagtail/admin/templates/wagtailadmin/pages/edit_partials.html +19 -0
- wagtail/admin/templates/wagtailadmin/pages/index_results.html +1 -9
- wagtail/admin/templates/wagtailadmin/shared/autosave/indicator.html +36 -0
- wagtail/admin/templates/wagtailadmin/shared/breadcrumbs.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/editing_sessions/list.html +2 -2
- wagtail/admin/templates/wagtailadmin/shared/editing_sessions/module.html +19 -3
- wagtail/admin/templates/wagtailadmin/shared/headers/_actions.html +5 -0
- wagtail/admin/templates/wagtailadmin/shared/headers/_history_icon_link.html +2 -2
- wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +8 -9
- wagtail/admin/templates/wagtailadmin/shared/listing/filter_partials.html +19 -0
- wagtail/admin/templates/wagtailadmin/shared/side_panel_toggle.html +3 -3
- wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/side_panel.html +3 -0
- wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/privacy.html +18 -6
- wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/usage.html +11 -4
- wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/workflow.html +22 -1
- wagtail/admin/templates/wagtailadmin/shared/side_panels/preview.html +1 -0
- wagtail/admin/templates/wagtailadmin/shared/side_panels.html +1 -2
- wagtail/admin/templates/wagtailadmin/shared/unsaved_changes_warning.html +20 -20
- wagtail/admin/templates/wagtailadmin/userbar/base.html +2 -0
- wagtail/admin/templates/wagtailadmin/userbar/item_accessibility.html +1 -1
- wagtail/admin/templatetags/wagtailadmin_tags.py +6 -14
- wagtail/admin/tests/pages/test_create_page.py +359 -1
- wagtail/admin/tests/pages/test_custom_listing.py +48 -2
- wagtail/admin/tests/pages/test_edit_page.py +987 -10
- wagtail/admin/tests/pages/test_explorer_view.py +9 -5
- wagtail/admin/tests/pages/test_page_search.py +15 -0
- wagtail/admin/tests/pages/test_revisions.py +4 -0
- wagtail/admin/tests/test_account_management.py +88 -2
- wagtail/admin/tests/test_collections_views.py +15 -15
- wagtail/admin/tests/test_compare.py +34 -0
- wagtail/admin/tests/test_editing_sessions.py +230 -8
- wagtail/admin/tests/test_forms.py +192 -1
- wagtail/admin/tests/test_icon_sprite.py +22 -2
- wagtail/admin/tests/test_page_chooser.py +13 -13
- wagtail/admin/tests/test_reports_views.py +4 -1
- wagtail/admin/tests/test_userbar.py +69 -5
- wagtail/admin/tests/test_views_generic.py +5 -5
- wagtail/admin/tests/test_workflows.py +14 -12
- wagtail/admin/tests/viewsets/test_model_viewset.py +13 -0
- wagtail/admin/ui/autosave.py +5 -0
- wagtail/admin/ui/editing_sessions.py +3 -0
- wagtail/admin/ui/side_panels.py +19 -20
- wagtail/admin/userbar.py +48 -27
- wagtail/admin/views/bulk_action/dispatcher.py +2 -2
- wagtail/admin/views/chooser.py +6 -6
- wagtail/admin/views/editing_sessions.py +20 -7
- wagtail/admin/views/generic/__init__.py +1 -0
- wagtail/admin/views/generic/chooser.py +5 -5
- wagtail/admin/views/generic/mixins.py +143 -26
- wagtail/admin/views/generic/models.py +157 -27
- wagtail/admin/views/generic/ordering.py +1 -1
- wagtail/admin/views/generic/preview.py +4 -4
- wagtail/admin/views/home.py +3 -1
- wagtail/admin/views/pages/create.py +78 -29
- wagtail/admin/views/pages/edit.py +162 -34
- wagtail/admin/views/pages/preview.py +4 -4
- wagtail/admin/views/pages/revisions.py +3 -0
- wagtail/admin/views/pages/search.py +4 -4
- wagtail/admin/views/pages/usage.py +2 -2
- wagtail/admin/views/tags.py +2 -2
- wagtail/admin/views/workflows.py +73 -54
- wagtail/admin/viewsets/model.py +34 -8
- wagtail/admin/widgets/button.py +11 -4
- wagtail/admin/widgets/chooser.py +6 -4
- wagtail/admin/widgets/slug.py +1 -1
- wagtail/api/v2/filters.py +27 -21
- wagtail/api/v2/pagination.py +4 -4
- wagtail/api/v2/views.py +7 -7
- wagtail/blocks/list_block.py +0 -8
- wagtail/blocks/migrations/migrate_operation.py +8 -1
- wagtail/blocks/stream_block.py +2 -10
- wagtail/blocks/struct_block.py +192 -12
- wagtail/compat.py +2 -2
- wagtail/contrib/forms/locale/af/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/az_AZ/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/bg/LC_MESSAGES/django.po +4 -1
- wagtail/contrib/forms/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/forms/locale/eu/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/ht/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/hy/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/it/LC_MESSAGES/django.po +40 -2
- wagtail/contrib/forms/locale/ka/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/frontend_cache/backends/azure.py +6 -6
- wagtail/contrib/frontend_cache/backends/cloudfront.py +2 -2
- wagtail/contrib/frontend_cache/utils.py +1 -1
- wagtail/contrib/redirects/forms.py +1 -1
- 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.mo +0 -0
- wagtail/contrib/redirects/locale/bg/LC_MESSAGES/django.po +7 -1
- wagtail/contrib/redirects/locale/bn/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/cy/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/de/LC_MESSAGES/django.po +15 -3
- wagtail/contrib/redirects/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +14 -14
- wagtail/contrib/redirects/locale/en_IN/LC_MESSAGES/django.mo +0 -0
- 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.mo +0 -0
- wagtail/contrib/redirects/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/es_VE/LC_MESSAGES/django.mo +0 -0
- 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.mo +0 -0
- wagtail/contrib/redirects/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/fr/LC_MESSAGES/django.po +1 -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.mo +0 -0
- 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.mo +0 -0
- 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.mo +0 -0
- 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.mo +0 -0
- wagtail/contrib/redirects/locale/it/LC_MESSAGES/django.po +17 -4
- wagtail/contrib/redirects/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ka/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ko/LC_MESSAGES/django.po +1 -1
- 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 +1 -1
- wagtail/contrib/redirects/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/ru/LC_MESSAGES/django.po +15 -2
- wagtail/contrib/redirects/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/locale/sr/LC_MESSAGES/django.mo +0 -0
- 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 +17 -4
- wagtail/contrib/redirects/locale/ta/LC_MESSAGES/django.mo +0 -0
- 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.po +1 -1
- 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.po +1 -1
- wagtail/contrib/redirects/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/redirects/middleware.py +11 -7
- wagtail/contrib/redirects/models.py +17 -1
- wagtail/contrib/redirects/tests/test_import_admin_views.py +3 -3
- wagtail/contrib/redirects/tests/test_redirects.py +56 -8
- wagtail/contrib/search_promotions/locale/af/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/az_AZ/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/bn/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/cy/LC_MESSAGES/django.po +1 -1
- 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 +9 -3
- wagtail/contrib/search_promotions/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +6 -6
- wagtail/contrib/search_promotions/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/es_419/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/fr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.po +44 -3
- wagtail/contrib/search_promotions/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ko/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/ru/LC_MESSAGES/django.po +9 -3
- wagtail/contrib/search_promotions/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/sv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/search_promotions/tests.py +1 -1
- wagtail/contrib/search_promotions/views/settings.py +24 -25
- wagtail/contrib/settings/jinja2tags.py +2 -2
- wagtail/contrib/settings/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/az_AZ/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/{locale/af → contrib/settings/locale/bg}/LC_MESSAGES/django.po +7 -16
- wagtail/contrib/settings/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/cy/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/de/LC_MESSAGES/django.po +7 -3
- wagtail/contrib/settings/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/settings/locale/en_IN/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/en_IN/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/es_VE/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/fr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/hi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/it/LC_MESSAGES/django.po +7 -3
- wagtail/contrib/settings/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/ka/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/ko/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/sr/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/sr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/sv/LC_MESSAGES/django.po +4 -3
- wagtail/contrib/settings/locale/ta/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/zh/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/tests/generic/test_admin.py +118 -2
- wagtail/contrib/settings/tests/site_specific/test_admin.py +78 -15
- wagtail/contrib/settings/tests/site_specific/test_model.py +2 -2
- wagtail/contrib/settings/views.py +7 -0
- wagtail/contrib/simple_translation/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/bg/LC_MESSAGES/django.po +26 -0
- wagtail/contrib/simple_translation/locale/el/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/simple_translation/locale/es_419/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/it/LC_MESSAGES/django.po +5 -2
- wagtail/contrib/simple_translation/locale/mi/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/vi/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/simple_translation/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/styleguide/locale/af/LC_MESSAGES/django.mo +0 -0
- 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.mo +0 -0
- wagtail/contrib/styleguide/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/styleguide/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/bn/LC_MESSAGES/django.mo +0 -0
- 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 +1 -1
- wagtail/contrib/styleguide/locale/da/LC_MESSAGES/django.mo +0 -0
- 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 +1 -1
- wagtail/contrib/styleguide/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
- wagtail/contrib/styleguide/locale/en_IN/LC_MESSAGES/django.mo +0 -0
- 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.mo +0 -0
- wagtail/contrib/styleguide/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/es_VE/LC_MESSAGES/django.mo +0 -0
- 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.mo +0 -0
- wagtail/contrib/styleguide/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/hi/LC_MESSAGES/django.mo +0 -0
- 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.mo +0 -0
- 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.po +1 -1
- 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.mo +0 -0
- 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.mo +0 -0
- 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/tests.py +2 -0
- wagtail/contrib/styleguide/views.py +4 -5
- wagtail/contrib/table_block/locale/ar/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/ar/LC_MESSAGES/django.po +5 -1
- wagtail/contrib/table_block/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/de/LC_MESSAGES/django.po +6 -3
- wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/table_block/locale/en_IN/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/et/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/ht/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/id_ID/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.po +5 -2
- wagtail/contrib/table_block/locale/ja/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/lv/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/mi/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/my/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/ru/LC_MESSAGES/django.po +6 -3
- wagtail/contrib/table_block/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/tet/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/typed_table_block/blocks.py +37 -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.mo +0 -0
- 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.mo +0 -0
- 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.mo +0 -0
- wagtail/contrib/typed_table_block/locale/sv/LC_MESSAGES/django.po +5 -4
- wagtail/contrib/typed_table_block/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/typed_table_block/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/tr/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 +108 -0
- wagtail/coreutils.py +4 -4
- wagtail/documents/__init__.py +4 -4
- wagtail/documents/locale/af/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/az_AZ/LC_MESSAGES/django.mo +0 -0
- 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.mo +0 -0
- wagtail/documents/locale/bg/LC_MESSAGES/django.po +10 -1
- wagtail/documents/locale/bn/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ca/LC_MESSAGES/django.po +1 -1
- 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 +18 -3
- wagtail/documents/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/en/LC_MESSAGES/django.po +15 -15
- wagtail/documents/locale/es/LC_MESSAGES/django.po +1 -1
- 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.mo +0 -0
- 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.po +1 -1
- wagtail/documents/locale/fr/LC_MESSAGES/django.po +1 -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.mo +0 -0
- wagtail/documents/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/hu/LC_MESSAGES/django.po +1 -1
- 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 +33 -3
- 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.po +1 -1
- 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.po +1 -1
- wagtail/documents/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/ru/LC_MESSAGES/django.po +7 -3
- wagtail/documents/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/documents/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/sv/LC_MESSAGES/django.po +33 -5
- 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.po +1 -1
- wagtail/documents/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/documents/models.py +1 -1
- wagtail/documents/tests/test_admin_views.py +19 -4
- wagtail/documents/tests/test_search.py +0 -2
- wagtail/documents/tests/test_views.py +6 -4
- wagtail/documents/views/bulk_actions/add_tags.py +1 -1
- wagtail/embeds/finders/facebook.py +3 -3
- wagtail/embeds/finders/instagram.py +3 -3
- wagtail/embeds/finders/oembed.py +7 -2
- wagtail/embeds/locale/az_AZ/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/locale/bn/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/locale/da/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/locale/dv/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/embeds/locale/es_419/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/locale/lv/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/locale/my/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/oembed_providers.py +8 -0
- wagtail/embeds/tests/test_embeds.py +35 -0
- wagtail/images/__init__.py +4 -4
- wagtail/images/admin_urls.py +3 -3
- wagtail/images/blocks.py +1 -1
- wagtail/images/formats.py +2 -2
- wagtail/images/image_operations.py +2 -2
- wagtail/images/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/az_AZ/LC_MESSAGES/django.mo +0 -0
- 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.mo +0 -0
- wagtail/images/locale/bg/LC_MESSAGES/django.po +13 -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 +6 -3
- wagtail/images/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/cy/LC_MESSAGES/django.po +1 -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 +30 -3
- wagtail/images/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/en/LC_MESSAGES/django.po +44 -40
- 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.mo +0 -0
- wagtail/images/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/fr/LC_MESSAGES/django.po +6 -3
- wagtail/images/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/gl/LC_MESSAGES/django.po +6 -3
- 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.mo +0 -0
- wagtail/images/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/hu/LC_MESSAGES/django.po +1 -1
- 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 +54 -5
- 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.po +1 -1
- 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.po +1 -1
- wagtail/images/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/nl/LC_MESSAGES/django.po +9 -6
- wagtail/images/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/ru/LC_MESSAGES/django.po +15 -3
- wagtail/images/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/images/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/sv/LC_MESSAGES/django.po +50 -7
- 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.po +1 -1
- 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 +1 -1
- wagtail/images/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/images/models.py +11 -10
- wagtail/images/templates/wagtailimages/images/index_results.html +1 -1
- wagtail/images/templates/wagtailimages/images/url_generator.html +17 -38
- wagtail/images/templates/wagtailimages/images/url_generator_output.html +28 -0
- wagtail/images/templatetags/wagtailimages_tags.py +4 -4
- wagtail/images/tests/test_admin_views.py +432 -59
- wagtail/images/tests/test_image_operations.py +2 -2
- wagtail/images/tests/test_models.py +0 -2
- wagtail/images/views/bulk_actions/add_tags.py +1 -1
- wagtail/images/views/images.py +72 -39
- wagtail/locale/ar/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ar/LC_MESSAGES/django.po +10 -19
- wagtail/locale/az_AZ/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/az_AZ/LC_MESSAGES/django.po +4 -4
- wagtail/locale/be/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/be/LC_MESSAGES/django.po +7 -19
- wagtail/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/bg/LC_MESSAGES/django.po +35 -11
- wagtail/locale/bn/LC_MESSAGES/django.po +4 -4
- wagtail/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ca/LC_MESSAGES/django.po +24 -21
- wagtail/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/cs/LC_MESSAGES/django.po +7 -19
- wagtail/locale/cy/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/cy/LC_MESSAGES/django.po +10 -13
- wagtail/locale/da/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/da/LC_MESSAGES/django.po +7 -16
- wagtail/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/de/LC_MESSAGES/django.po +41 -21
- wagtail/locale/dv/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/dv/LC_MESSAGES/django.po +7 -19
- wagtail/locale/el/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/el/LC_MESSAGES/django.po +10 -19
- wagtail/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/en/LC_MESSAGES/django.po +103 -105
- wagtail/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/es/LC_MESSAGES/django.po +10 -19
- wagtail/locale/es_419/LC_MESSAGES/django.po +6 -6
- wagtail/locale/et/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/et/LC_MESSAGES/django.po +7 -16
- wagtail/locale/eu/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/eu/LC_MESSAGES/django.po +4 -10
- wagtail/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/fa/LC_MESSAGES/django.po +10 -16
- wagtail/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/fi/LC_MESSAGES/django.po +10 -19
- wagtail/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/fr/LC_MESSAGES/django.po +24 -21
- wagtail/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/gl/LC_MESSAGES/django.po +24 -21
- wagtail/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/he_IL/LC_MESSAGES/django.po +10 -16
- wagtail/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/hr_HR/LC_MESSAGES/django.po +7 -16
- wagtail/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/hu/LC_MESSAGES/django.po +10 -19
- wagtail/locale/id_ID/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/id_ID/LC_MESSAGES/django.po +7 -16
- wagtail/locale/is_IS/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/is_IS/LC_MESSAGES/django.po +10 -19
- wagtail/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/it/LC_MESSAGES/django.po +126 -21
- wagtail/locale/ja/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ja/LC_MESSAGES/django.po +10 -19
- wagtail/locale/ka/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ka/LC_MESSAGES/django.po +9 -6
- wagtail/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ko/LC_MESSAGES/django.po +10 -19
- wagtail/locale/lt/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/lt/LC_MESSAGES/django.po +7 -19
- wagtail/locale/lv/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/lv/LC_MESSAGES/django.po +10 -19
- wagtail/locale/mi/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/mi/LC_MESSAGES/django.po +7 -13
- wagtail/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/mn/LC_MESSAGES/django.po +7 -16
- wagtail/locale/my/LC_MESSAGES/django.po +6 -6
- wagtail/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/nb/LC_MESSAGES/django.po +10 -16
- wagtail/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/nl/LC_MESSAGES/django.po +22 -21
- wagtail/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/pl/LC_MESSAGES/django.po +10 -19
- wagtail/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/pt_BR/LC_MESSAGES/django.po +10 -19
- wagtail/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/pt_PT/LC_MESSAGES/django.po +10 -19
- wagtail/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ro/LC_MESSAGES/django.po +10 -19
- wagtail/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ru/LC_MESSAGES/django.po +32 -21
- wagtail/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/sk_SK/LC_MESSAGES/django.po +10 -16
- wagtail/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/sl/LC_MESSAGES/django.po +10 -19
- wagtail/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/sv/LC_MESSAGES/django.po +105 -31
- wagtail/locale/tet/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/tet/LC_MESSAGES/django.po +7 -16
- wagtail/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/th/LC_MESSAGES/django.po +7 -19
- wagtail/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/tr/LC_MESSAGES/django.po +10 -19
- wagtail/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/tr_TR/LC_MESSAGES/django.po +10 -16
- wagtail/locale/ug/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ug/LC_MESSAGES/django.po +10 -19
- wagtail/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/uk/LC_MESSAGES/django.po +7 -19
- wagtail/locale/vi/LC_MESSAGES/django.po +7 -7
- wagtail/locale/zh/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/zh/LC_MESSAGES/django.po +6 -15
- wagtail/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/zh_Hans/LC_MESSAGES/django.po +10 -19
- wagtail/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/zh_Hant/LC_MESSAGES/django.po +7 -19
- wagtail/locales/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/bg/LC_MESSAGES/django.po +9 -2
- wagtail/locales/locale/da/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/el/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/et/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/id_ID/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/it/LC_MESSAGES/django.po +13 -2
- wagtail/locales/locale/ja/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/lv/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/my/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/ru/LC_MESSAGES/django.po +7 -2
- wagtail/locales/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/sv/LC_MESSAGES/django.po +4 -3
- wagtail/locales/locale/tet/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/vi/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/zh/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/tests.py +5 -5
- wagtail/models/i18n.py +4 -6
- wagtail/models/media.py +18 -0
- wagtail/models/pages.py +65 -11
- wagtail/models/reference_index.py +15 -0
- wagtail/models/revisions.py +40 -12
- wagtail/models/workflows.py +0 -3
- wagtail/permission_policies/base.py +2 -2
- wagtail/permission_policies/collections.py +2 -2
- wagtail/project_template/requirements.txt +2 -2
- wagtail/search/locale/ar/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/be/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/cy/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/dv/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/search/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/et/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/fa/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/fi/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/ht/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/id_ID/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/is_IS/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/ja/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/ko/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/lt/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/mi/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/mn/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/my/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/nb/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/pl/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/tet/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/th/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/ug/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/uk/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/vi/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- wagtail/search/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/signal_handlers.py +1 -0
- wagtail/sites/locale/bn/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/da/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/dv/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/sites/locale/en_IN/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/es_419/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/es_VE/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/hi/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/ka/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/sr/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/sv/LC_MESSAGES/django.po +3 -2
- wagtail/sites/locale/ta/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/tests.py +7 -7
- wagtail/snippets/action_menu.py +2 -1
- wagtail/snippets/locale/af/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/af/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/az_AZ/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/bg/LC_MESSAGES/django.po +10 -1
- wagtail/snippets/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ca/LC_MESSAGES/django.po +11 -3
- wagtail/snippets/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/cy/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/de/LC_MESSAGES/django.po +15 -3
- wagtail/snippets/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/en/LC_MESSAGES/django.po +23 -13
- wagtail/snippets/locale/en_IN/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/en_IN/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/es_419/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/es_VE/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/et/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/eu/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/fr/LC_MESSAGES/django.po +11 -3
- wagtail/snippets/locale/gl/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/gl/LC_MESSAGES/django.po +9 -1
- wagtail/snippets/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/hi/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/hi/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ht/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ht/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/it/LC_MESSAGES/django.po +11 -1
- wagtail/snippets/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ka/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ka/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ko/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/nl/LC_MESSAGES/django.po +11 -3
- wagtail/snippets/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ru/LC_MESSAGES/django.po +11 -3
- wagtail/snippets/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/sr/LC_MESSAGES/django.mo +0 -0
- 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 +13 -2
- wagtail/snippets/locale/ta/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ta/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/ug/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/tests/test_chooser_block.py +197 -0
- wagtail/snippets/tests/test_chooser_panel.py +149 -0
- wagtail/snippets/tests/test_chooser_views.py +375 -0
- wagtail/snippets/tests/test_chooser_widget.py +22 -0
- wagtail/snippets/tests/test_compare_revisions_view.py +167 -0
- wagtail/snippets/tests/test_copy_view.py +38 -0
- wagtail/snippets/tests/test_create_view.py +1163 -0
- wagtail/snippets/tests/test_delete_view.py +225 -0
- wagtail/snippets/tests/test_edit_view.py +3243 -0
- wagtail/snippets/tests/test_history_view.py +182 -0
- wagtail/snippets/tests/test_index_view.py +105 -0
- wagtail/snippets/tests/test_list_view.py +786 -0
- wagtail/snippets/tests/test_locking.py +28 -0
- wagtail/snippets/tests/test_permissions.py +167 -0
- wagtail/snippets/tests/test_preview.py +3 -3
- wagtail/snippets/tests/test_revert_view.py +343 -0
- wagtail/snippets/tests/test_snippet_models.py +112 -0
- wagtail/snippets/tests/test_unpublish_view.py +228 -0
- wagtail/snippets/tests/test_unschedule_view.py +151 -0
- wagtail/snippets/tests/test_viewset.py +38 -5
- wagtail/snippets/views/snippets.py +76 -30
- wagtail/snippets/widgets.py +2 -2
- wagtail/templatetags/wagtailcore_tags.py +2 -2
- wagtail/test/dummy_external_storage.py +1 -1
- wagtail/test/testapp/fixtures/test.json +22 -0
- wagtail/test/testapp/fixtures/test_empty.json +2 -0
- wagtail/test/testapp/fixtures/test_explorable_pages.json +10 -0
- wagtail/test/testapp/fixtures/test_specific.json +9 -0
- wagtail/test/testapp/migrations/0059_nopromotepage.py +25 -0
- wagtail/test/testapp/models.py +7 -0
- wagtail/test/testapp/views.py +5 -0
- wagtail/test/utils/page_tests.py +7 -7
- wagtail/test/utils/wagtail_factories/builder.py +2 -2
- wagtail/tests/streamfield_migrations/test_migrations.py +35 -0
- wagtail/tests/test_blocks.py +321 -61
- wagtail/tests/test_collection_model.py +12 -0
- wagtail/tests/test_page_model.py +190 -1
- wagtail/tests/test_page_privacy.py +5 -0
- wagtail/tests/test_reference_index.py +42 -0
- wagtail/tests/test_revision_model.py +118 -1
- wagtail/tests/test_utils.py +2 -2
- wagtail/users/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/bg/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/bg/LC_MESSAGES/django.po +10 -1
- wagtail/users/locale/bn/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/cy/LC_MESSAGES/django.po +1 -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 +6 -3
- wagtail/users/locale/dv/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/en/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/en/LC_MESSAGES/django.po +14 -14
- 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.mo +0 -0
- wagtail/users/locale/eu/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/fr/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/he_IL/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/hy/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/id_ID/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/id_ID/LC_MESSAGES/django.po +7 -3
- 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 +15 -3
- 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.po +1 -1
- 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.po +1 -1
- wagtail/users/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/ru/LC_MESSAGES/django.po +4 -4
- wagtail/users/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/sv/LC_MESSAGES/django.po +49 -18
- wagtail/users/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/th/LC_MESSAGES/django.po +1 -1
- 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.po +1 -1
- wagtail/users/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
- wagtail/users/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/users/tests/test_admin_views.py +39 -25
- wagtail/users/utils.py +4 -1
- wagtail/users/views/groups.py +19 -5
- wagtail/users/wagtail_hooks.py +1 -1
- wagtail/utils/loading.py +2 -2
- wagtail-7.3.data/data/AGENTS.md +21 -0
- wagtail-7.3.data/data/CHANGELOG.txt +4957 -0
- wagtail-7.3.data/data/CODE_OF_CONDUCT.md +71 -0
- wagtail-7.3.data/data/CONTRIBUTORS.md +1001 -0
- wagtail-7.3.data/data/SPONSORS.md +55 -0
- {wagtail-7.2.2.dist-info → wagtail-7.3.dist-info}/METADATA +6 -6
- {wagtail-7.2.2.dist-info → wagtail-7.3.dist-info}/RECORD +1299 -1265
- wagtail/images/static/wagtailimages/js/image-url-generator.js +0 -1
- wagtail/snippets/tests/test_snippets.py +0 -6377
- {wagtail-7.2.2.dist-info → wagtail-7.3.dist-info}/WHEEL +0 -0
- {wagtail-7.2.2.dist-info → wagtail-7.3.dist-info}/entry_points.txt +0 -0
- {wagtail-7.2.2.dist-info → wagtail-7.3.dist-info}/licenses/LICENSE +0 -0
- {wagtail-7.2.2.dist-info → wagtail-7.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,3243 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
from django.contrib.admin.utils import quote
|
|
5
|
+
from django.contrib.auth.models import Permission
|
|
6
|
+
from django.contrib.contenttypes.models import ContentType
|
|
7
|
+
from django.core.files.base import ContentFile
|
|
8
|
+
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
9
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
10
|
+
from django.http import HttpRequest, HttpResponse, JsonResponse
|
|
11
|
+
from django.test import TestCase
|
|
12
|
+
from django.test.utils import override_settings
|
|
13
|
+
from django.urls import reverse
|
|
14
|
+
from django.utils.timezone import now
|
|
15
|
+
from freezegun import freeze_time
|
|
16
|
+
from taggit.models import Tag
|
|
17
|
+
|
|
18
|
+
from wagtail.admin.admin_url_finder import AdminURLFinder
|
|
19
|
+
from wagtail.models import Locale, ModelLogEntry, Revision
|
|
20
|
+
from wagtail.signals import published
|
|
21
|
+
from wagtail.snippets.action_menu import (
|
|
22
|
+
ActionMenuItem,
|
|
23
|
+
get_base_snippet_action_menu_items,
|
|
24
|
+
)
|
|
25
|
+
from wagtail.test.snippets.models import (
|
|
26
|
+
FileUploadSnippet,
|
|
27
|
+
StandardSnippetWithCustomPrimaryKey,
|
|
28
|
+
TranslatableSnippet,
|
|
29
|
+
)
|
|
30
|
+
from wagtail.test.testapp.models import (
|
|
31
|
+
Advert,
|
|
32
|
+
AdvertWithTabbedInterface,
|
|
33
|
+
CustomPreviewSizesModel,
|
|
34
|
+
DraftStateCustomPrimaryKeyModel,
|
|
35
|
+
DraftStateModel,
|
|
36
|
+
FullFeaturedSnippet,
|
|
37
|
+
PreviewableModel,
|
|
38
|
+
RevisableModel,
|
|
39
|
+
)
|
|
40
|
+
from wagtail.test.utils import WagtailTestUtils
|
|
41
|
+
from wagtail.test.utils.timestamps import submittable_timestamp
|
|
42
|
+
from wagtail.utils.timestamps import render_timestamp
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BaseTestSnippetEditView(WagtailTestUtils, TestCase):
|
|
46
|
+
def get_edit_url(self):
|
|
47
|
+
snippet = self.test_snippet
|
|
48
|
+
args = [quote(snippet.pk)]
|
|
49
|
+
return reverse(snippet.snippet_viewset.get_url_name("edit"), args=args)
|
|
50
|
+
|
|
51
|
+
def get(self, params=None, headers=None):
|
|
52
|
+
return self.client.get(self.get_edit_url(), params, headers=headers)
|
|
53
|
+
|
|
54
|
+
def post(self, post_data=None, headers=None):
|
|
55
|
+
return self.client.post(self.get_edit_url(), post_data, headers=headers)
|
|
56
|
+
|
|
57
|
+
def setUp(self):
|
|
58
|
+
self.user = self.login()
|
|
59
|
+
|
|
60
|
+
def assertSchedulingDialogRendered(self, response, label="Edit schedule"):
|
|
61
|
+
# Should show the "Edit schedule" button
|
|
62
|
+
html = response.content.decode()
|
|
63
|
+
self.assertTagInHTML(
|
|
64
|
+
f'<button type="button" data-a11y-dialog-show="schedule-publishing-dialog">{label}</button>',
|
|
65
|
+
html,
|
|
66
|
+
count=1,
|
|
67
|
+
allow_extra_attrs=True,
|
|
68
|
+
)
|
|
69
|
+
# Should show the dialog template pointing to the [data-edit-form] selector as the root
|
|
70
|
+
soup = self.get_soup(html)
|
|
71
|
+
dialog = soup.select_one(
|
|
72
|
+
"""
|
|
73
|
+
template[data-controller="w-teleport"][data-w-teleport-target-value="[data-edit-form]"]
|
|
74
|
+
#schedule-publishing-dialog
|
|
75
|
+
"""
|
|
76
|
+
)
|
|
77
|
+
self.assertIsNotNone(dialog)
|
|
78
|
+
# Should render the main form with data-edit-form attribute
|
|
79
|
+
self.assertTagInHTML(
|
|
80
|
+
f'<form action="{self.get_edit_url()}" method="POST" data-edit-form>',
|
|
81
|
+
html,
|
|
82
|
+
count=1,
|
|
83
|
+
allow_extra_attrs=True,
|
|
84
|
+
)
|
|
85
|
+
self.assertTagInHTML(
|
|
86
|
+
'<div id="schedule-publishing-dialog" class="w-dialog publishing" data-controller="w-dialog">',
|
|
87
|
+
html,
|
|
88
|
+
count=1,
|
|
89
|
+
allow_extra_attrs=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TestSnippetEditView(BaseTestSnippetEditView):
|
|
94
|
+
fixtures = ["test.json"]
|
|
95
|
+
|
|
96
|
+
def setUp(self):
|
|
97
|
+
super().setUp()
|
|
98
|
+
self.test_snippet = Advert.objects.get(pk=1)
|
|
99
|
+
ModelLogEntry.objects.create(
|
|
100
|
+
content_type=ContentType.objects.get_for_model(Advert),
|
|
101
|
+
label="Test Advert",
|
|
102
|
+
action="wagtail.create",
|
|
103
|
+
timestamp=now() - datetime.timedelta(weeks=3),
|
|
104
|
+
user=self.user,
|
|
105
|
+
object_id="1",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def test_get_with_limited_permissions(self):
|
|
109
|
+
self.user.is_superuser = False
|
|
110
|
+
self.user.user_permissions.add(
|
|
111
|
+
Permission.objects.get(
|
|
112
|
+
content_type__app_label="wagtailadmin", codename="access_admin"
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
self.user.save()
|
|
116
|
+
|
|
117
|
+
response = self.get()
|
|
118
|
+
self.assertEqual(response.status_code, 302)
|
|
119
|
+
|
|
120
|
+
def test_simple(self):
|
|
121
|
+
response = self.get()
|
|
122
|
+
html = response.content.decode()
|
|
123
|
+
self.assertEqual(response.status_code, 200)
|
|
124
|
+
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
|
|
125
|
+
self.assertNotContains(response, 'role="tablist"')
|
|
126
|
+
|
|
127
|
+
# Without DraftStateMixin, there should be no "No publishing schedule set" info
|
|
128
|
+
self.assertNotContains(response, "No publishing schedule set")
|
|
129
|
+
|
|
130
|
+
history_url = reverse(
|
|
131
|
+
"wagtailsnippets_tests_advert:history", args=[quote(self.test_snippet.pk)]
|
|
132
|
+
)
|
|
133
|
+
# History link should be present, one in the header and one in the status side panel
|
|
134
|
+
self.assertContains(response, history_url, count=2)
|
|
135
|
+
|
|
136
|
+
usage_url = reverse(
|
|
137
|
+
"wagtailsnippets_tests_advert:usage", args=[quote(self.test_snippet.pk)]
|
|
138
|
+
)
|
|
139
|
+
# Usage link should be present in the status side panel
|
|
140
|
+
self.assertContains(response, usage_url)
|
|
141
|
+
|
|
142
|
+
# Live status and last updated info should be shown, with a link to the history page
|
|
143
|
+
self.assertContains(response, "3\xa0weeks ago")
|
|
144
|
+
self.assertTagInHTML(
|
|
145
|
+
f'<a href="{history_url}" aria-describedby="status-sidebar-live">View history</a>',
|
|
146
|
+
html,
|
|
147
|
+
allow_extra_attrs=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
soup = self.get_soup(response.content)
|
|
151
|
+
|
|
152
|
+
# Should have the unsaved controller set up
|
|
153
|
+
editor_form = soup.select_one("#w-editor-form")
|
|
154
|
+
self.assertIsNotNone(editor_form)
|
|
155
|
+
self.assertIn("w-unsaved", editor_form.attrs.get("data-controller").split())
|
|
156
|
+
self.assertTrue(
|
|
157
|
+
{
|
|
158
|
+
"w-unsaved#submit",
|
|
159
|
+
"beforeunload@window->w-unsaved#confirm",
|
|
160
|
+
}.issubset(editor_form.attrs.get("data-action").split())
|
|
161
|
+
)
|
|
162
|
+
self.assertEqual(
|
|
163
|
+
editor_form.attrs.get("data-w-unsaved-confirmation-value"),
|
|
164
|
+
"true",
|
|
165
|
+
)
|
|
166
|
+
self.assertEqual(
|
|
167
|
+
editor_form.attrs.get("data-w-unsaved-force-value"),
|
|
168
|
+
"false",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self.assertIsNone(editor_form.select_one("input[name='loaded_revision_id']"))
|
|
172
|
+
self.assertIsNone(
|
|
173
|
+
editor_form.select_one("input[name='loaded_revision_created_at']")
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self.assertIsNotNone(editor_form)
|
|
177
|
+
self.assertNotIn("w-autosave", editor_form["data-controller"].split())
|
|
178
|
+
self.assertNotIn("w-autosave", editor_form["data-action"])
|
|
179
|
+
self.assertIsNone(editor_form.attrs.get("data-w-autosave-interval-value"))
|
|
180
|
+
|
|
181
|
+
url_finder = AdminURLFinder(self.user)
|
|
182
|
+
expected_url = "/admin/snippets/tests/advert/edit/%d/" % self.test_snippet.pk
|
|
183
|
+
self.assertEqual(url_finder.get_edit_url(self.test_snippet), expected_url)
|
|
184
|
+
|
|
185
|
+
def test_get_hydrate_create_view(self):
|
|
186
|
+
response = self.get(params={"_w_hydrate_create_view": "1"})
|
|
187
|
+
self.assertEqual(response.status_code, 200)
|
|
188
|
+
self.assertTemplateUsed(response, "wagtailadmin/generic/edit_partials.html")
|
|
189
|
+
soup = self.get_soup(response.content)
|
|
190
|
+
|
|
191
|
+
# Should reload only the status side panel
|
|
192
|
+
side_panels = soup.select(
|
|
193
|
+
"template[data-controller='w-teleport']"
|
|
194
|
+
"[data-w-teleport-target-value^='[data-side-panel=']"
|
|
195
|
+
"[data-w-teleport-mode-value='innerHTML']"
|
|
196
|
+
)
|
|
197
|
+
self.assertEqual(len(side_panels), 1)
|
|
198
|
+
status_side_panel = side_panels[0]
|
|
199
|
+
self.assertEqual(
|
|
200
|
+
status_side_panel["data-w-teleport-target-value"],
|
|
201
|
+
"[data-side-panel='status']",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Workflow and privacy features are not available
|
|
205
|
+
workflow_status_dialog = soup.find("div", id="workflow-status-dialog")
|
|
206
|
+
self.assertIsNone(workflow_status_dialog)
|
|
207
|
+
set_privacy_dialog = soup.find("div", id="set-privacy")
|
|
208
|
+
self.assertIsNone(set_privacy_dialog)
|
|
209
|
+
|
|
210
|
+
breadcrumbs = soup.find(
|
|
211
|
+
"template",
|
|
212
|
+
{
|
|
213
|
+
"data-controller": "w-teleport",
|
|
214
|
+
"data-w-teleport-target-value": "header [data-w-breadcrumbs]",
|
|
215
|
+
"data-w-teleport-mode-value": "outerHTML",
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
self.assertIsNotNone(breadcrumbs)
|
|
219
|
+
# Should include header buttons as they were not rendered in the create view
|
|
220
|
+
self.assertIsNotNone(breadcrumbs.select_one("#w-slim-header-buttons"))
|
|
221
|
+
|
|
222
|
+
# Should render the history link button as it wasn't rendered in the create view
|
|
223
|
+
history_link = soup.find(
|
|
224
|
+
"template",
|
|
225
|
+
{
|
|
226
|
+
"data-controller": "w-teleport",
|
|
227
|
+
"data-w-teleport-target-value": "[data-side-panel-toggle]:last-of-type",
|
|
228
|
+
"data-w-teleport-mode-value": "afterend",
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
history_url = reverse(
|
|
232
|
+
self.test_snippet.snippet_viewset.get_url_name("history"),
|
|
233
|
+
args=(quote(self.test_snippet.pk),),
|
|
234
|
+
)
|
|
235
|
+
self.assertIsNotNone(history_link)
|
|
236
|
+
self.assertIsNotNone(history_link.select_one(f"a[href='{history_url}']"))
|
|
237
|
+
|
|
238
|
+
form_title_heading = soup.find(
|
|
239
|
+
"template",
|
|
240
|
+
{
|
|
241
|
+
"data-controller": "w-teleport",
|
|
242
|
+
"data-w-teleport-target-value": "#header-title span",
|
|
243
|
+
"data-w-teleport-mode-value": "textContent",
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
self.assertIsNotNone(form_title_heading)
|
|
247
|
+
self.assertEqual(form_title_heading.text.strip(), str(self.test_snippet))
|
|
248
|
+
header_title = soup.find(
|
|
249
|
+
"template",
|
|
250
|
+
{
|
|
251
|
+
"data-controller": "w-teleport",
|
|
252
|
+
"data-w-teleport-target-value": "head title",
|
|
253
|
+
"data-w-teleport-mode-value": "textContent",
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
self.assertIsNotNone(header_title)
|
|
257
|
+
self.assertEqual(header_title.text.strip(), f"Editing: {self.test_snippet}")
|
|
258
|
+
|
|
259
|
+
# Should not include any updates to the form as we don't have revisions
|
|
260
|
+
# enabled and thus don't need to add loaded revision info
|
|
261
|
+
form_adds = soup.find(
|
|
262
|
+
"template",
|
|
263
|
+
{
|
|
264
|
+
"data-controller": "w-teleport",
|
|
265
|
+
"data-w-teleport-target-value": "form[data-edit-form]",
|
|
266
|
+
"data-w-teleport-mode-value": "afterbegin",
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
self.assertIsNone(form_adds)
|
|
270
|
+
|
|
271
|
+
# Should load the editing sessions module as it was not in the create view
|
|
272
|
+
editing_sessions = soup.find(
|
|
273
|
+
"template",
|
|
274
|
+
{
|
|
275
|
+
"data-controller": "w-teleport",
|
|
276
|
+
"data-w-teleport-target-value": "#w-autosave-indicator",
|
|
277
|
+
"data-w-teleport-mode-value": "afterend",
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
self.assertIsNotNone(editing_sessions)
|
|
281
|
+
# without the revision info
|
|
282
|
+
self.assertIsNone(editing_sessions.select_one("input[name='revision_id']"))
|
|
283
|
+
self.assertIsNone(
|
|
284
|
+
editing_sessions.select_one("input[name='revision_created_at']")
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def test_non_existent_model(self):
|
|
288
|
+
response = self.client.get(
|
|
289
|
+
f"/admin/snippets/tests/foo/edit/{quote(self.test_snippet.pk)}/"
|
|
290
|
+
)
|
|
291
|
+
self.assertEqual(response.status_code, 404)
|
|
292
|
+
|
|
293
|
+
def test_nonexistent_id(self):
|
|
294
|
+
response = self.client.get(
|
|
295
|
+
reverse("wagtailsnippets_tests_advert:edit", args=[999999])
|
|
296
|
+
)
|
|
297
|
+
self.assertEqual(response.status_code, 404)
|
|
298
|
+
|
|
299
|
+
def test_edit_with_limited_permissions(self):
|
|
300
|
+
self.user.is_superuser = False
|
|
301
|
+
self.user.user_permissions.add(
|
|
302
|
+
Permission.objects.get(
|
|
303
|
+
content_type__app_label="wagtailadmin", codename="access_admin"
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
self.user.save()
|
|
307
|
+
|
|
308
|
+
response = self.post(
|
|
309
|
+
post_data={"text": "test text", "url": "http://www.example.com/"}
|
|
310
|
+
)
|
|
311
|
+
self.assertEqual(response.status_code, 302)
|
|
312
|
+
|
|
313
|
+
url_finder = AdminURLFinder(self.user)
|
|
314
|
+
self.assertIsNone(url_finder.get_edit_url(self.test_snippet))
|
|
315
|
+
|
|
316
|
+
def test_edit_invalid(self):
|
|
317
|
+
response = self.post(post_data={"foo": "bar"})
|
|
318
|
+
soup = self.get_soup(response.content)
|
|
319
|
+
|
|
320
|
+
header_messages = soup.css.select(".messages[role='status'] ul > li")
|
|
321
|
+
|
|
322
|
+
# the top level message should indicate that the page could not be saved
|
|
323
|
+
self.assertEqual(len(header_messages), 1)
|
|
324
|
+
message = header_messages[0]
|
|
325
|
+
self.assertIn(
|
|
326
|
+
"The advert could not be saved due to errors.", message.get_text()
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# the top level message should provide a go to error button
|
|
330
|
+
buttons = message.find_all("button")
|
|
331
|
+
self.assertEqual(len(buttons), 1)
|
|
332
|
+
self.assertEqual(buttons[0].attrs["data-controller"], "w-count w-focus")
|
|
333
|
+
self.assertEqual(
|
|
334
|
+
set(buttons[0].attrs["data-action"].split()),
|
|
335
|
+
{"click->w-focus#focus", "wagtail:panel-init@document->w-count#count"},
|
|
336
|
+
)
|
|
337
|
+
self.assertIn("Go to the first error", buttons[0].get_text())
|
|
338
|
+
|
|
339
|
+
# the error should only appear once: against the field, not in the header message
|
|
340
|
+
error_messages = soup.css.select(".error-message")
|
|
341
|
+
self.assertEqual(len(error_messages), 1)
|
|
342
|
+
error_message = error_messages[0]
|
|
343
|
+
self.assertEqual(error_message.parent["id"], "panel-child-text-errors")
|
|
344
|
+
self.assertIn("This field is required", error_message.get_text())
|
|
345
|
+
|
|
346
|
+
# Should have the unsaved controller set up
|
|
347
|
+
editor_form = soup.select_one("#w-editor-form")
|
|
348
|
+
self.assertIsNotNone(editor_form)
|
|
349
|
+
self.assertIn("w-unsaved", editor_form.attrs.get("data-controller").split())
|
|
350
|
+
self.assertTrue(
|
|
351
|
+
{
|
|
352
|
+
"w-unsaved#submit",
|
|
353
|
+
"beforeunload@window->w-unsaved#confirm",
|
|
354
|
+
}.issubset(editor_form.attrs.get("data-action").split())
|
|
355
|
+
)
|
|
356
|
+
self.assertEqual(
|
|
357
|
+
editor_form.attrs.get("data-w-unsaved-confirmation-value"),
|
|
358
|
+
"true",
|
|
359
|
+
)
|
|
360
|
+
self.assertEqual(
|
|
361
|
+
editor_form.attrs.get("data-w-unsaved-force-value"),
|
|
362
|
+
# The form is invalid, we want to force it to be "dirty" on initial load
|
|
363
|
+
"true",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def test_edit_invalid_with_json_response(self):
|
|
367
|
+
response = self.post(
|
|
368
|
+
post_data={"foo": "bar"},
|
|
369
|
+
headers={"Accept": "application/json"},
|
|
370
|
+
)
|
|
371
|
+
self.assertEqual(response.status_code, 400)
|
|
372
|
+
self.assertEqual(response["Content-Type"], "application/json")
|
|
373
|
+
self.assertEqual(
|
|
374
|
+
response.json(),
|
|
375
|
+
{
|
|
376
|
+
"success": False,
|
|
377
|
+
"error_code": "validation_error",
|
|
378
|
+
"error_message": "There are validation errors, click save to highlight them.",
|
|
379
|
+
},
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def test_edit(self):
|
|
383
|
+
response = self.post(
|
|
384
|
+
post_data={
|
|
385
|
+
"text": "edited_test_advert",
|
|
386
|
+
"url": "http://www.example.com/edited",
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
self.assertRedirects(response, reverse("wagtailsnippets_tests_advert:list"))
|
|
390
|
+
|
|
391
|
+
snippets = Advert.objects.filter(text="edited_test_advert")
|
|
392
|
+
self.assertEqual(snippets.count(), 1)
|
|
393
|
+
self.assertEqual(snippets.first().url, "http://www.example.com/edited")
|
|
394
|
+
|
|
395
|
+
def test_edit_with_json_response(self):
|
|
396
|
+
response = self.post(
|
|
397
|
+
post_data={
|
|
398
|
+
"text": "edited_test_advert",
|
|
399
|
+
"url": "http://www.example.com/edited",
|
|
400
|
+
},
|
|
401
|
+
headers={"Accept": "application/json"},
|
|
402
|
+
)
|
|
403
|
+
self.assertEqual(response.status_code, 200)
|
|
404
|
+
self.assertEqual(response["Content-Type"], "application/json")
|
|
405
|
+
|
|
406
|
+
snippets = Advert.objects.filter(text="edited_test_advert")
|
|
407
|
+
self.assertEqual(snippets.count(), 1)
|
|
408
|
+
snippet = snippets.first()
|
|
409
|
+
self.assertEqual(snippet.url, "http://www.example.com/edited")
|
|
410
|
+
|
|
411
|
+
response_json = response.json()
|
|
412
|
+
self.assertEqual(response_json["success"], True)
|
|
413
|
+
self.assertEqual(response_json["pk"], snippet.pk)
|
|
414
|
+
self.assertEqual(response_json["field_updates"], {})
|
|
415
|
+
|
|
416
|
+
def test_edit_with_tags(self):
|
|
417
|
+
tags = ["hello", "world"]
|
|
418
|
+
response = self.post(
|
|
419
|
+
post_data={
|
|
420
|
+
"text": "edited_test_advert",
|
|
421
|
+
"url": "http://www.example.com/edited",
|
|
422
|
+
"tags": ", ".join(tags),
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
self.assertRedirects(response, reverse("wagtailsnippets_tests_advert:list"))
|
|
427
|
+
|
|
428
|
+
snippet = Advert.objects.get(text="edited_test_advert")
|
|
429
|
+
|
|
430
|
+
expected_tags = list(Tag.objects.order_by("name").filter(name__in=tags))
|
|
431
|
+
self.assertEqual(len(expected_tags), 2)
|
|
432
|
+
self.assertEqual(list(snippet.tags.order_by("name")), expected_tags)
|
|
433
|
+
|
|
434
|
+
def test_before_edit_snippet_hook_get(self):
|
|
435
|
+
def hook_func(request, instance):
|
|
436
|
+
self.assertIsInstance(request, HttpRequest)
|
|
437
|
+
self.assertEqual(instance.text, "test_advert")
|
|
438
|
+
self.assertEqual(instance.url, "http://www.example.com")
|
|
439
|
+
return HttpResponse("Overridden!")
|
|
440
|
+
|
|
441
|
+
with self.register_hook("before_edit_snippet", hook_func):
|
|
442
|
+
response = self.get()
|
|
443
|
+
|
|
444
|
+
self.assertEqual(response.status_code, 200)
|
|
445
|
+
self.assertEqual(response.content, b"Overridden!")
|
|
446
|
+
|
|
447
|
+
def test_before_edit_snippet_hook_get_with_json_response(self):
|
|
448
|
+
def non_json_hook_func(request, instance):
|
|
449
|
+
self.assertIsInstance(request, HttpRequest)
|
|
450
|
+
self.assertEqual(instance.text, "test_advert")
|
|
451
|
+
self.assertEqual(instance.url, "http://www.example.com")
|
|
452
|
+
|
|
453
|
+
return HttpResponse("Overridden!")
|
|
454
|
+
|
|
455
|
+
def json_hook_func(request, instance):
|
|
456
|
+
self.assertIsInstance(request, HttpRequest)
|
|
457
|
+
self.assertEqual(instance.text, "test_advert")
|
|
458
|
+
self.assertEqual(instance.url, "http://www.example.com")
|
|
459
|
+
|
|
460
|
+
return JsonResponse({"status": "purple"})
|
|
461
|
+
|
|
462
|
+
with self.register_hook("before_edit_snippet", non_json_hook_func):
|
|
463
|
+
response = self.get(
|
|
464
|
+
headers={"Accept": "application/json"},
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
self.assertEqual(response.status_code, 400)
|
|
468
|
+
self.assertEqual(
|
|
469
|
+
response.json(),
|
|
470
|
+
{
|
|
471
|
+
"success": False,
|
|
472
|
+
"error_code": "blocked_by_hook",
|
|
473
|
+
"error_message": "Request to edit advert was blocked by hook.",
|
|
474
|
+
},
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
with self.register_hook("before_edit_snippet", json_hook_func):
|
|
478
|
+
response = self.get(
|
|
479
|
+
headers={"Accept": "application/json"},
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
self.assertEqual(response.status_code, 200)
|
|
483
|
+
self.assertEqual(response.json(), {"status": "purple"})
|
|
484
|
+
|
|
485
|
+
def test_before_edit_snippet_hook_post(self):
|
|
486
|
+
def hook_func(request, instance):
|
|
487
|
+
self.assertIsInstance(request, HttpRequest)
|
|
488
|
+
self.assertEqual(instance.text, "test_advert")
|
|
489
|
+
self.assertEqual(instance.url, "http://www.example.com")
|
|
490
|
+
return HttpResponse("Overridden!")
|
|
491
|
+
|
|
492
|
+
with self.register_hook("before_edit_snippet", hook_func):
|
|
493
|
+
response = self.post(
|
|
494
|
+
post_data={
|
|
495
|
+
"text": "Edited and runs hook",
|
|
496
|
+
"url": "http://www.example.com/hook-enabled-edited",
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
self.assertEqual(response.status_code, 200)
|
|
501
|
+
self.assertEqual(response.content, b"Overridden!")
|
|
502
|
+
|
|
503
|
+
# Request intercepted before advert was updated
|
|
504
|
+
self.assertEqual(Advert.objects.get().text, "test_advert")
|
|
505
|
+
|
|
506
|
+
def test_before_edit_snippet_hook_post_with_json_response(self):
|
|
507
|
+
def non_json_hook_func(request, instance):
|
|
508
|
+
self.assertIsInstance(request, HttpRequest)
|
|
509
|
+
self.assertEqual(instance.text, "test_advert")
|
|
510
|
+
self.assertEqual(instance.url, "http://www.example.com")
|
|
511
|
+
|
|
512
|
+
return HttpResponse("Overridden!")
|
|
513
|
+
|
|
514
|
+
def json_hook_func(request, instance):
|
|
515
|
+
self.assertIsInstance(request, HttpRequest)
|
|
516
|
+
self.assertEqual(instance.text, "test_advert")
|
|
517
|
+
self.assertEqual(instance.url, "http://www.example.com")
|
|
518
|
+
|
|
519
|
+
return JsonResponse({"status": "purple"})
|
|
520
|
+
|
|
521
|
+
with self.register_hook("before_edit_snippet", non_json_hook_func):
|
|
522
|
+
post_data = {
|
|
523
|
+
"text": "Edited and runs hook",
|
|
524
|
+
"url": "http://www.example.com/hook-enabled-edited",
|
|
525
|
+
}
|
|
526
|
+
response = self.post(
|
|
527
|
+
post_data,
|
|
528
|
+
headers={"Accept": "application/json"},
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
self.assertEqual(response.status_code, 400)
|
|
532
|
+
self.assertEqual(
|
|
533
|
+
response.json(),
|
|
534
|
+
{
|
|
535
|
+
"success": False,
|
|
536
|
+
"error_code": "blocked_by_hook",
|
|
537
|
+
"error_message": "Request to edit advert was blocked by hook.",
|
|
538
|
+
},
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Request intercepted before advert was updated
|
|
542
|
+
self.assertEqual(Advert.objects.get().text, "test_advert")
|
|
543
|
+
|
|
544
|
+
with self.register_hook("before_edit_snippet", json_hook_func):
|
|
545
|
+
post_data = {
|
|
546
|
+
"text": "Edited and runs hook",
|
|
547
|
+
"url": "http://www.example.com/hook-enabled-edited",
|
|
548
|
+
}
|
|
549
|
+
response = self.post(
|
|
550
|
+
post_data,
|
|
551
|
+
headers={"Accept": "application/json"},
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
self.assertEqual(response.status_code, 200)
|
|
555
|
+
self.assertEqual(response.json(), {"status": "purple"})
|
|
556
|
+
|
|
557
|
+
# Request intercepted before advert was updated
|
|
558
|
+
self.assertEqual(Advert.objects.get().text, "test_advert")
|
|
559
|
+
|
|
560
|
+
def test_after_edit_snippet_hook(self):
|
|
561
|
+
def hook_func(request, instance):
|
|
562
|
+
self.assertIsInstance(request, HttpRequest)
|
|
563
|
+
self.assertEqual(instance.text, "Edited and runs hook")
|
|
564
|
+
self.assertEqual(instance.url, "http://www.example.com/hook-enabled-edited")
|
|
565
|
+
return HttpResponse("Overridden!")
|
|
566
|
+
|
|
567
|
+
with self.register_hook("after_edit_snippet", hook_func):
|
|
568
|
+
response = self.post(
|
|
569
|
+
post_data={
|
|
570
|
+
"text": "Edited and runs hook",
|
|
571
|
+
"url": "http://www.example.com/hook-enabled-edited",
|
|
572
|
+
}
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
self.assertEqual(response.status_code, 200)
|
|
576
|
+
self.assertEqual(response.content, b"Overridden!")
|
|
577
|
+
|
|
578
|
+
# Request intercepted after advert was updated
|
|
579
|
+
self.assertEqual(Advert.objects.get().text, "Edited and runs hook")
|
|
580
|
+
|
|
581
|
+
def test_after_edit_snippet_hook_with_json_response(self):
|
|
582
|
+
def non_json_hook_func(request, instance):
|
|
583
|
+
self.assertIsInstance(request, HttpRequest)
|
|
584
|
+
self.assertEqual(instance.text, "Edited and runs hook")
|
|
585
|
+
self.assertEqual(instance.url, "http://www.example.com/hook-enabled-edited")
|
|
586
|
+
|
|
587
|
+
return HttpResponse("Overridden!")
|
|
588
|
+
|
|
589
|
+
def json_hook_func(request, instance):
|
|
590
|
+
self.assertIsInstance(request, HttpRequest)
|
|
591
|
+
self.assertEqual(instance.text, "Edited and runs hook x2")
|
|
592
|
+
self.assertEqual(instance.url, "http://www.example.com/hook-enabled-edited")
|
|
593
|
+
|
|
594
|
+
return JsonResponse({"status": "purple"})
|
|
595
|
+
|
|
596
|
+
with self.register_hook("after_edit_snippet", non_json_hook_func):
|
|
597
|
+
post_data = {
|
|
598
|
+
"text": "Edited and runs hook",
|
|
599
|
+
"url": "http://www.example.com/hook-enabled-edited",
|
|
600
|
+
}
|
|
601
|
+
response = self.post(
|
|
602
|
+
post_data,
|
|
603
|
+
headers={"Accept": "application/json"},
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
self.assertEqual(response.status_code, 200)
|
|
607
|
+
# hook response is ignored, since it's not a JSON response
|
|
608
|
+
self.assertEqual(response.json()["success"], True)
|
|
609
|
+
|
|
610
|
+
# Request intercepted after advert was updated
|
|
611
|
+
self.assertEqual(Advert.objects.get().text, "Edited and runs hook")
|
|
612
|
+
|
|
613
|
+
with self.register_hook("after_edit_snippet", json_hook_func):
|
|
614
|
+
post_data = {
|
|
615
|
+
"text": "Edited and runs hook x2",
|
|
616
|
+
"url": "http://www.example.com/hook-enabled-edited",
|
|
617
|
+
}
|
|
618
|
+
response = self.post(
|
|
619
|
+
post_data,
|
|
620
|
+
headers={"Accept": "application/json"},
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
self.assertEqual(response.status_code, 200)
|
|
624
|
+
self.assertEqual(response.json(), {"status": "purple"})
|
|
625
|
+
|
|
626
|
+
# Request intercepted after advert was updated
|
|
627
|
+
self.assertEqual(Advert.objects.get().text, "Edited and runs hook x2")
|
|
628
|
+
|
|
629
|
+
def test_register_snippet_action_menu_item(self):
|
|
630
|
+
class TestSnippetActionMenuItem(ActionMenuItem):
|
|
631
|
+
label = "Test"
|
|
632
|
+
name = "test"
|
|
633
|
+
icon_name = "check"
|
|
634
|
+
classname = "custom-class"
|
|
635
|
+
|
|
636
|
+
def is_shown(self, context):
|
|
637
|
+
return True
|
|
638
|
+
|
|
639
|
+
def hook_func(model):
|
|
640
|
+
return TestSnippetActionMenuItem(order=0)
|
|
641
|
+
|
|
642
|
+
with self.register_hook("register_snippet_action_menu_item", hook_func):
|
|
643
|
+
get_base_snippet_action_menu_items.cache_clear()
|
|
644
|
+
|
|
645
|
+
response = self.get()
|
|
646
|
+
|
|
647
|
+
get_base_snippet_action_menu_items.cache_clear()
|
|
648
|
+
|
|
649
|
+
self.assertContains(
|
|
650
|
+
response,
|
|
651
|
+
'<button type="submit" name="test" value="Test" class="button custom-class"><svg class="icon icon-check icon" aria-hidden="true"><use href="#icon-check"></use></svg>Test</button>',
|
|
652
|
+
html=True,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
def test_construct_snippet_action_menu(self):
|
|
656
|
+
def hook_func(menu_items, request, context):
|
|
657
|
+
self.assertIsInstance(menu_items, list)
|
|
658
|
+
self.assertIsInstance(request, WSGIRequest)
|
|
659
|
+
self.assertEqual(context["view"], "edit")
|
|
660
|
+
self.assertEqual(context["instance"], self.test_snippet)
|
|
661
|
+
self.assertEqual(context["model"], Advert)
|
|
662
|
+
|
|
663
|
+
# Remove the save item
|
|
664
|
+
del menu_items[0]
|
|
665
|
+
|
|
666
|
+
with self.register_hook("construct_snippet_action_menu", hook_func):
|
|
667
|
+
response = self.get()
|
|
668
|
+
|
|
669
|
+
self.assertNotContains(response, "<em>Save</em>")
|
|
670
|
+
|
|
671
|
+
def test_previewable_snippet(self):
|
|
672
|
+
self.test_snippet = PreviewableModel.objects.create(
|
|
673
|
+
text="Preview-enabled snippet"
|
|
674
|
+
)
|
|
675
|
+
response = self.get()
|
|
676
|
+
self.assertEqual(response.status_code, 200)
|
|
677
|
+
soup = self.get_soup(response.content)
|
|
678
|
+
|
|
679
|
+
radios = soup.select('input[type="radio"][name="preview-size"]')
|
|
680
|
+
self.assertEqual(len(radios), 3)
|
|
681
|
+
|
|
682
|
+
self.assertEqual(
|
|
683
|
+
[
|
|
684
|
+
"Preview in mobile size",
|
|
685
|
+
"Preview in tablet size",
|
|
686
|
+
"Preview in desktop size",
|
|
687
|
+
],
|
|
688
|
+
[radio["aria-label"] for radio in radios],
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
self.assertEqual("375", radios[0]["data-device-width"])
|
|
692
|
+
self.assertTrue(radios[0].has_attr("checked"))
|
|
693
|
+
|
|
694
|
+
def test_custom_preview_sizes(self):
|
|
695
|
+
self.test_snippet = CustomPreviewSizesModel.objects.create(
|
|
696
|
+
text="Preview-enabled with custom sizes",
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
response = self.get()
|
|
700
|
+
self.assertEqual(response.status_code, 200)
|
|
701
|
+
soup = self.get_soup(response.content)
|
|
702
|
+
|
|
703
|
+
radios = soup.select('input[type="radio"][name="preview-size"]')
|
|
704
|
+
self.assertEqual(len(radios), 2)
|
|
705
|
+
|
|
706
|
+
self.assertEqual("412", radios[0]["data-device-width"])
|
|
707
|
+
self.assertEqual("Custom mobile preview", radios[0]["aria-label"])
|
|
708
|
+
self.assertFalse(radios[0].has_attr("checked"))
|
|
709
|
+
|
|
710
|
+
self.assertEqual("1280", radios[1]["data-device-width"])
|
|
711
|
+
self.assertEqual("Original desktop", radios[1]["aria-label"])
|
|
712
|
+
self.assertTrue(radios[1].has_attr("checked"))
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class TestEditTabbedSnippet(BaseTestSnippetEditView):
|
|
716
|
+
def setUp(self):
|
|
717
|
+
super().setUp()
|
|
718
|
+
self.test_snippet = AdvertWithTabbedInterface.objects.create(
|
|
719
|
+
text="test_advert",
|
|
720
|
+
url="http://www.example.com",
|
|
721
|
+
something_else="Model with tabbed interface",
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
def test_snippet_with_tabbed_interface(self):
|
|
725
|
+
response = self.get()
|
|
726
|
+
|
|
727
|
+
self.assertEqual(response.status_code, 200)
|
|
728
|
+
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
|
|
729
|
+
self.assertContains(response, 'role="tablist"')
|
|
730
|
+
|
|
731
|
+
self.assertContains(
|
|
732
|
+
response,
|
|
733
|
+
'<a id="tab-label-advert" href="#tab-advert" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1" data-action="w-tabs#select:prevent" data-w-tabs-target="trigger">',
|
|
734
|
+
)
|
|
735
|
+
self.assertContains(
|
|
736
|
+
response,
|
|
737
|
+
'<a id="tab-label-other" href="#tab-other" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1" data-action="w-tabs#select:prevent" data-w-tabs-target="trigger">',
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class TestEditFileUploadSnippet(BaseTestSnippetEditView):
|
|
742
|
+
def setUp(self):
|
|
743
|
+
super().setUp()
|
|
744
|
+
self.test_snippet = FileUploadSnippet.objects.create(
|
|
745
|
+
file=ContentFile(b"Simple text document", "test.txt")
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
def test_edit_file_upload_multipart(self):
|
|
749
|
+
response = self.get()
|
|
750
|
+
self.assertContains(response, 'enctype="multipart/form-data"')
|
|
751
|
+
|
|
752
|
+
response = self.post(
|
|
753
|
+
post_data={
|
|
754
|
+
"file": SimpleUploadedFile("replacement.txt", b"Replacement document")
|
|
755
|
+
}
|
|
756
|
+
)
|
|
757
|
+
self.assertRedirects(
|
|
758
|
+
response,
|
|
759
|
+
reverse("wagtailsnippets_snippetstests_fileuploadsnippet:list"),
|
|
760
|
+
)
|
|
761
|
+
snippet = FileUploadSnippet.objects.get()
|
|
762
|
+
self.assertEqual(snippet.file.read(), b"Replacement document")
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
@override_settings(WAGTAIL_I18N_ENABLED=True)
|
|
766
|
+
class TestLocaleSelectorOnEdit(BaseTestSnippetEditView):
|
|
767
|
+
fixtures = ["test.json"]
|
|
768
|
+
|
|
769
|
+
LOCALE_SELECTOR_LABEL = "Switch locales"
|
|
770
|
+
LOCALE_INDICATOR_HTML = '<h3 id="status-sidebar-english"'
|
|
771
|
+
|
|
772
|
+
def setUp(self):
|
|
773
|
+
super().setUp()
|
|
774
|
+
self.test_snippet = TranslatableSnippet.objects.create(text="This is a test")
|
|
775
|
+
self.fr_locale = Locale.objects.create(language_code="fr")
|
|
776
|
+
self.test_snippet_fr = self.test_snippet.copy_for_translation(self.fr_locale)
|
|
777
|
+
self.test_snippet_fr.save()
|
|
778
|
+
|
|
779
|
+
def test_locale_selector(self):
|
|
780
|
+
response = self.get()
|
|
781
|
+
self.assertContains(response, self.LOCALE_SELECTOR_LABEL)
|
|
782
|
+
self.assertContains(response, self.LOCALE_INDICATOR_HTML)
|
|
783
|
+
|
|
784
|
+
def test_locale_selector_without_translation(self):
|
|
785
|
+
self.test_snippet_fr.delete()
|
|
786
|
+
response = self.get()
|
|
787
|
+
# The "Switch locale" button should not be shown
|
|
788
|
+
self.assertNotContains(response, self.LOCALE_SELECTOR_LABEL)
|
|
789
|
+
# Locale status still available and says "No other translations"
|
|
790
|
+
self.assertContains(response, self.LOCALE_INDICATOR_HTML)
|
|
791
|
+
self.assertContains(response, "No other translations")
|
|
792
|
+
|
|
793
|
+
@override_settings(WAGTAIL_I18N_ENABLED=False)
|
|
794
|
+
def test_locale_selector_not_present_when_i18n_disabled(self):
|
|
795
|
+
response = self.get()
|
|
796
|
+
self.assertNotContains(response, self.LOCALE_SELECTOR_LABEL)
|
|
797
|
+
self.assertNotContains(response, self.LOCALE_INDICATOR_HTML)
|
|
798
|
+
|
|
799
|
+
def test_locale_selector_not_present_on_non_translatable_snippet(self):
|
|
800
|
+
self.test_snippet = Advert.objects.get(pk=1)
|
|
801
|
+
response = self.get()
|
|
802
|
+
self.assertNotContains(response, self.LOCALE_SELECTOR_LABEL)
|
|
803
|
+
self.assertNotContains(response, self.LOCALE_INDICATOR_HTML)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
class TestEditRevisionSnippet(BaseTestSnippetEditView):
|
|
807
|
+
def setUp(self):
|
|
808
|
+
super().setUp()
|
|
809
|
+
self.test_snippet = RevisableModel.objects.create(text="foo")
|
|
810
|
+
|
|
811
|
+
def test_get_hydrate_create_view(self):
|
|
812
|
+
latest_revision = self.test_snippet.save_revision(user=self.user)
|
|
813
|
+
response = self.get(params={"_w_hydrate_create_view": "1"})
|
|
814
|
+
self.assertEqual(response.status_code, 200)
|
|
815
|
+
self.assertTemplateUsed(response, "wagtailadmin/generic/edit_partials.html")
|
|
816
|
+
soup = self.get_soup(response.content)
|
|
817
|
+
|
|
818
|
+
# Should reload only the status side panel
|
|
819
|
+
side_panels = soup.select(
|
|
820
|
+
"template[data-controller='w-teleport']"
|
|
821
|
+
"[data-w-teleport-target-value^='[data-side-panel=']"
|
|
822
|
+
"[data-w-teleport-mode-value='innerHTML']"
|
|
823
|
+
)
|
|
824
|
+
self.assertEqual(len(side_panels), 1)
|
|
825
|
+
status_side_panel = side_panels[0]
|
|
826
|
+
self.assertEqual(
|
|
827
|
+
status_side_panel["data-w-teleport-target-value"],
|
|
828
|
+
"[data-side-panel='status']",
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# Workflow and privacy features are not available
|
|
832
|
+
workflow_status_dialog = soup.find("div", id="workflow-status-dialog")
|
|
833
|
+
self.assertIsNone(workflow_status_dialog)
|
|
834
|
+
set_privacy_dialog = soup.find("div", id="set-privacy")
|
|
835
|
+
self.assertIsNone(set_privacy_dialog)
|
|
836
|
+
|
|
837
|
+
breadcrumbs = soup.find(
|
|
838
|
+
"template",
|
|
839
|
+
{
|
|
840
|
+
"data-controller": "w-teleport",
|
|
841
|
+
"data-w-teleport-target-value": "header [data-w-breadcrumbs]",
|
|
842
|
+
"data-w-teleport-mode-value": "outerHTML",
|
|
843
|
+
},
|
|
844
|
+
)
|
|
845
|
+
self.assertIsNotNone(breadcrumbs)
|
|
846
|
+
# Should include header buttons as they were not rendered in the create view
|
|
847
|
+
self.assertIsNotNone(breadcrumbs.select_one("#w-slim-header-buttons"))
|
|
848
|
+
|
|
849
|
+
# Should render the history link button as it wasn't rendered in the create view
|
|
850
|
+
history_link = soup.find(
|
|
851
|
+
"template",
|
|
852
|
+
{
|
|
853
|
+
"data-controller": "w-teleport",
|
|
854
|
+
"data-w-teleport-target-value": "[data-side-panel-toggle]:last-of-type",
|
|
855
|
+
"data-w-teleport-mode-value": "afterend",
|
|
856
|
+
},
|
|
857
|
+
)
|
|
858
|
+
history_url = reverse(
|
|
859
|
+
self.test_snippet.snippet_viewset.get_url_name("history"),
|
|
860
|
+
args=(quote(self.test_snippet.pk),),
|
|
861
|
+
)
|
|
862
|
+
self.assertIsNotNone(history_link)
|
|
863
|
+
self.assertIsNotNone(history_link.select_one(f"a[href='{history_url}']"))
|
|
864
|
+
|
|
865
|
+
form_title_heading = soup.find(
|
|
866
|
+
"template",
|
|
867
|
+
{
|
|
868
|
+
"data-controller": "w-teleport",
|
|
869
|
+
"data-w-teleport-target-value": "#header-title span",
|
|
870
|
+
"data-w-teleport-mode-value": "textContent",
|
|
871
|
+
},
|
|
872
|
+
)
|
|
873
|
+
self.assertIsNotNone(form_title_heading)
|
|
874
|
+
self.assertEqual(form_title_heading.text.strip(), str(self.test_snippet))
|
|
875
|
+
header_title = soup.find(
|
|
876
|
+
"template",
|
|
877
|
+
{
|
|
878
|
+
"data-controller": "w-teleport",
|
|
879
|
+
"data-w-teleport-target-value": "head title",
|
|
880
|
+
"data-w-teleport-mode-value": "textContent",
|
|
881
|
+
},
|
|
882
|
+
)
|
|
883
|
+
self.assertIsNotNone(header_title)
|
|
884
|
+
self.assertEqual(header_title.text.strip(), f"Editing: {self.test_snippet}")
|
|
885
|
+
|
|
886
|
+
# Should include loaded revision ID and timestamp in the form for
|
|
887
|
+
# subsequent autosave requests
|
|
888
|
+
form_adds = soup.find(
|
|
889
|
+
"template",
|
|
890
|
+
{
|
|
891
|
+
"data-controller": "w-teleport",
|
|
892
|
+
"data-w-teleport-target-value": "form[data-edit-form]",
|
|
893
|
+
"data-w-teleport-mode-value": "afterbegin",
|
|
894
|
+
},
|
|
895
|
+
)
|
|
896
|
+
self.assertIsNotNone(form_adds)
|
|
897
|
+
self.assertEqual(
|
|
898
|
+
form_adds.select_one("input[name='loaded_revision_id']")["value"],
|
|
899
|
+
str(latest_revision.pk),
|
|
900
|
+
)
|
|
901
|
+
self.assertEqual(
|
|
902
|
+
form_adds.select_one("input[name='loaded_revision_created_at']")["value"],
|
|
903
|
+
latest_revision.created_at.isoformat(),
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Should load the editing sessions module as it was not in the create view
|
|
907
|
+
editing_sessions = soup.find(
|
|
908
|
+
"template",
|
|
909
|
+
{
|
|
910
|
+
"data-controller": "w-teleport",
|
|
911
|
+
"data-w-teleport-target-value": "#w-autosave-indicator",
|
|
912
|
+
"data-w-teleport-mode-value": "afterend",
|
|
913
|
+
},
|
|
914
|
+
)
|
|
915
|
+
self.assertIsNotNone(editing_sessions)
|
|
916
|
+
# with the revision info
|
|
917
|
+
self.assertEqual(
|
|
918
|
+
editing_sessions.select_one("input[name='revision_id']")["value"],
|
|
919
|
+
str(latest_revision.pk),
|
|
920
|
+
)
|
|
921
|
+
self.assertEqual(
|
|
922
|
+
editing_sessions.select_one("input[name='revision_created_at']")["value"],
|
|
923
|
+
latest_revision.created_at.isoformat(),
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
def test_edit_snippet_with_revision(self):
|
|
927
|
+
response = self.post(post_data={"text": "bar"})
|
|
928
|
+
self.assertRedirects(
|
|
929
|
+
response, reverse("wagtailsnippets_tests_revisablemodel:list")
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
# The instance should be updated
|
|
933
|
+
snippets = RevisableModel.objects.filter(text="bar")
|
|
934
|
+
self.assertEqual(snippets.count(), 1)
|
|
935
|
+
|
|
936
|
+
# The revision should be created
|
|
937
|
+
revisions = self.test_snippet.revisions
|
|
938
|
+
revision = revisions.first()
|
|
939
|
+
self.assertEqual(revisions.count(), 1)
|
|
940
|
+
self.assertEqual(revision.content["text"], "bar")
|
|
941
|
+
|
|
942
|
+
# The log entry should have the revision attached
|
|
943
|
+
log_entries = ModelLogEntry.objects.for_instance(self.test_snippet).filter(
|
|
944
|
+
action="wagtail.edit"
|
|
945
|
+
)
|
|
946
|
+
self.assertEqual(log_entries.count(), 1)
|
|
947
|
+
self.assertEqual(log_entries.first().revision, revision)
|
|
948
|
+
|
|
949
|
+
def test_edit_snippet_with_revision_and_json_response(self):
|
|
950
|
+
initial_revision = self.test_snippet.save_revision(user=self.user)
|
|
951
|
+
self.assertEqual(self.test_snippet.revisions.count(), 1)
|
|
952
|
+
response = self.post(
|
|
953
|
+
post_data={
|
|
954
|
+
"text": "bar",
|
|
955
|
+
"loaded_revision_id": initial_revision.pk,
|
|
956
|
+
"loaded_revision_created_at": initial_revision.created_at.isoformat(),
|
|
957
|
+
},
|
|
958
|
+
headers={"Accept": "application/json"},
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
# Should be a 200 OK JSON response
|
|
962
|
+
self.assertEqual(response.status_code, 200)
|
|
963
|
+
self.assertEqual(response["Content-Type"], "application/json")
|
|
964
|
+
response_json = response.json()
|
|
965
|
+
self.assertIs(response_json["success"], True)
|
|
966
|
+
self.assertEqual(response_json["pk"], self.test_snippet.pk)
|
|
967
|
+
|
|
968
|
+
# Should create a new revision to be overwritten later
|
|
969
|
+
self.assertEqual(self.test_snippet.revisions.count(), 2)
|
|
970
|
+
self.assertNotEqual(response_json["revision_id"], initial_revision.pk)
|
|
971
|
+
revision = self.test_snippet.revisions.get(pk=response_json["revision_id"])
|
|
972
|
+
self.assertEqual(
|
|
973
|
+
response_json["revision_created_at"],
|
|
974
|
+
revision.created_at.isoformat(),
|
|
975
|
+
)
|
|
976
|
+
self.assertEqual(revision.content["text"], "bar")
|
|
977
|
+
|
|
978
|
+
# The instance should be updated
|
|
979
|
+
snippets = RevisableModel.objects.filter(text="bar")
|
|
980
|
+
self.assertEqual(snippets.count(), 1)
|
|
981
|
+
|
|
982
|
+
# The log entry should have the revision attached
|
|
983
|
+
log_entries = ModelLogEntry.objects.for_instance(self.test_snippet).filter(
|
|
984
|
+
action="wagtail.edit"
|
|
985
|
+
)
|
|
986
|
+
self.assertEqual(log_entries.count(), 1)
|
|
987
|
+
self.assertEqual(log_entries.first().revision, revision)
|
|
988
|
+
|
|
989
|
+
def test_save_outdated_revision_with_json_response(self):
|
|
990
|
+
self.test_snippet.text = "Initial revision"
|
|
991
|
+
revision = self.test_snippet.save_revision(user=self.user)
|
|
992
|
+
self.test_snippet.text = "Latest revision"
|
|
993
|
+
self.test_snippet.save_revision()
|
|
994
|
+
self.assertEqual(self.test_snippet.revisions.count(), 2)
|
|
995
|
+
response = self.post(
|
|
996
|
+
post_data={
|
|
997
|
+
"text": "Updated revision",
|
|
998
|
+
"loaded_revision_id": revision.pk,
|
|
999
|
+
},
|
|
1000
|
+
headers={"Accept": "application/json"},
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
# Instead of creating a new revision for autosave (which means the user
|
|
1004
|
+
# would unknowingly replace a newer revision), we return an error
|
|
1005
|
+
# response that should be a 400 response
|
|
1006
|
+
self.assertEqual(response.status_code, 400)
|
|
1007
|
+
self.assertEqual(response["Content-Type"], "application/json")
|
|
1008
|
+
self.assertEqual(
|
|
1009
|
+
response.json(),
|
|
1010
|
+
{
|
|
1011
|
+
"success": False,
|
|
1012
|
+
"error_code": "invalid_revision",
|
|
1013
|
+
"error_message": "Saving will overwrite a newer version.",
|
|
1014
|
+
},
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
self.assertEqual(self.test_snippet.revisions.count(), 2)
|
|
1018
|
+
revision.refresh_from_db()
|
|
1019
|
+
self.assertEqual(revision.content["text"], "Initial revision")
|
|
1020
|
+
|
|
1021
|
+
def test_save_outdated_revision_timestampwith_json_response(self):
|
|
1022
|
+
self.test_snippet.text = "Initial revision"
|
|
1023
|
+
revision = self.test_snippet.save_revision(user=self.user)
|
|
1024
|
+
loaded_revision_created_at = revision.created_at.isoformat()
|
|
1025
|
+
self.test_snippet.text = "Latest revision"
|
|
1026
|
+
self.test_snippet.save_revision(user=self.user, overwrite_revision=revision)
|
|
1027
|
+
self.assertEqual(self.test_snippet.revisions.count(), 1)
|
|
1028
|
+
response = self.post(
|
|
1029
|
+
post_data={
|
|
1030
|
+
"text": "Updated revision",
|
|
1031
|
+
"loaded_revision_id": revision.pk,
|
|
1032
|
+
"loaded_revision_created_at": loaded_revision_created_at,
|
|
1033
|
+
},
|
|
1034
|
+
headers={"Accept": "application/json"},
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
# Instead of creating a new revision for autosave (which means the user
|
|
1038
|
+
# would unknowingly replace a newer revision), we return an error
|
|
1039
|
+
# response that should be a 400 response
|
|
1040
|
+
self.assertEqual(response.status_code, 400)
|
|
1041
|
+
self.assertEqual(response["Content-Type"], "application/json")
|
|
1042
|
+
self.assertEqual(
|
|
1043
|
+
response.json(),
|
|
1044
|
+
{
|
|
1045
|
+
"success": False,
|
|
1046
|
+
"error_code": "invalid_revision",
|
|
1047
|
+
"error_message": "Saving will overwrite a newer version.",
|
|
1048
|
+
},
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
self.assertEqual(self.test_snippet.revisions.count(), 1)
|
|
1052
|
+
revision.refresh_from_db()
|
|
1053
|
+
self.assertEqual(revision.content["text"], "Latest revision")
|
|
1054
|
+
|
|
1055
|
+
def test_overwrite_revision_with_json_response(self):
|
|
1056
|
+
self.test_snippet.text = "Initial revision"
|
|
1057
|
+
initial_revision = self.test_snippet.save_revision()
|
|
1058
|
+
self.test_snippet.text = "Changed via a previous autosave"
|
|
1059
|
+
revision = self.test_snippet.save_revision(user=self.user)
|
|
1060
|
+
self.assertEqual(self.test_snippet.revisions.count(), 2)
|
|
1061
|
+
response = self.post(
|
|
1062
|
+
post_data={
|
|
1063
|
+
"text": "Updated revision",
|
|
1064
|
+
# The page was originally loaded with initial_revision, but
|
|
1065
|
+
# a successful autosave created a new revision which we now
|
|
1066
|
+
# want to overwrite with a new autosave request
|
|
1067
|
+
"loaded_revision_id": initial_revision.pk,
|
|
1068
|
+
"overwrite_revision_id": revision.pk,
|
|
1069
|
+
},
|
|
1070
|
+
headers={"Accept": "application/json"},
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
# Should be a 200 OK JSON response
|
|
1074
|
+
self.assertEqual(response.status_code, 200)
|
|
1075
|
+
self.assertEqual(response["Content-Type"], "application/json")
|
|
1076
|
+
revision.refresh_from_db()
|
|
1077
|
+
response_json = response.json()
|
|
1078
|
+
self.assertIs(response_json["success"], True)
|
|
1079
|
+
self.assertEqual(response_json["pk"], self.test_snippet.pk)
|
|
1080
|
+
self.assertEqual(response_json["revision_id"], revision.pk)
|
|
1081
|
+
self.assertEqual(
|
|
1082
|
+
response_json["revision_created_at"],
|
|
1083
|
+
revision.created_at.isoformat(),
|
|
1084
|
+
)
|
|
1085
|
+
self.assertEqual(response_json["field_updates"], {})
|
|
1086
|
+
soup = self.get_soup(response_json["html"])
|
|
1087
|
+
status_side_panel = soup.find(
|
|
1088
|
+
"template",
|
|
1089
|
+
{
|
|
1090
|
+
"data-controller": "w-teleport",
|
|
1091
|
+
"data-w-teleport-target-value": "[data-side-panel='status']",
|
|
1092
|
+
"data-w-teleport-mode-value": "innerHTML",
|
|
1093
|
+
},
|
|
1094
|
+
)
|
|
1095
|
+
self.assertIsNotNone(status_side_panel)
|
|
1096
|
+
breadcrumbs = soup.find(
|
|
1097
|
+
"template",
|
|
1098
|
+
{
|
|
1099
|
+
"data-controller": "w-teleport",
|
|
1100
|
+
"data-w-teleport-target-value": "header [data-w-breadcrumbs]",
|
|
1101
|
+
"data-w-teleport-mode-value": "outerHTML",
|
|
1102
|
+
},
|
|
1103
|
+
)
|
|
1104
|
+
self.assertIsNotNone(breadcrumbs)
|
|
1105
|
+
form_title_heading = soup.find(
|
|
1106
|
+
"template",
|
|
1107
|
+
{
|
|
1108
|
+
"data-controller": "w-teleport",
|
|
1109
|
+
"data-w-teleport-target-value": "#header-title span",
|
|
1110
|
+
"data-w-teleport-mode-value": "textContent",
|
|
1111
|
+
},
|
|
1112
|
+
)
|
|
1113
|
+
self.assertIsNotNone(form_title_heading)
|
|
1114
|
+
self.assertEqual(form_title_heading.text.strip(), "Updated revision")
|
|
1115
|
+
header_title = soup.find(
|
|
1116
|
+
"template",
|
|
1117
|
+
{
|
|
1118
|
+
"data-controller": "w-teleport",
|
|
1119
|
+
"data-w-teleport-target-value": "head title",
|
|
1120
|
+
"data-w-teleport-mode-value": "textContent",
|
|
1121
|
+
},
|
|
1122
|
+
)
|
|
1123
|
+
self.assertIsNotNone(header_title)
|
|
1124
|
+
self.assertEqual(header_title.text.strip(), "Editing: Updated revision")
|
|
1125
|
+
|
|
1126
|
+
self.assertEqual(self.test_snippet.revisions.count(), 2)
|
|
1127
|
+
revision.refresh_from_db()
|
|
1128
|
+
self.assertEqual(revision.content["text"], "Updated revision")
|
|
1129
|
+
|
|
1130
|
+
def test_overwrite_non_latest_revision(self):
|
|
1131
|
+
self.test_snippet.text = "Initial revision"
|
|
1132
|
+
initial_revision = self.test_snippet.save_revision(user=self.user)
|
|
1133
|
+
self.test_snippet.text = "First update via autosave"
|
|
1134
|
+
user_revision = self.test_snippet.save_revision(user=self.user)
|
|
1135
|
+
self.test_snippet.text = "Someone else's changed text"
|
|
1136
|
+
later_revision = self.test_snippet.save_revision()
|
|
1137
|
+
self.assertEqual(self.test_snippet.revisions.count(), 3)
|
|
1138
|
+
|
|
1139
|
+
post_data = {
|
|
1140
|
+
"text": "Updated revision",
|
|
1141
|
+
"loaded_revision_id": initial_revision.id,
|
|
1142
|
+
"overwrite_revision_id": user_revision.id,
|
|
1143
|
+
}
|
|
1144
|
+
response = self.post(
|
|
1145
|
+
post_data=post_data,
|
|
1146
|
+
headers={"Accept": "application/json"},
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
# Should be a 400 response
|
|
1150
|
+
self.assertEqual(response.status_code, 400)
|
|
1151
|
+
self.assertEqual(response["Content-Type"], "application/json")
|
|
1152
|
+
self.assertEqual(
|
|
1153
|
+
response.json(),
|
|
1154
|
+
{
|
|
1155
|
+
"success": False,
|
|
1156
|
+
"error_code": "invalid_revision",
|
|
1157
|
+
"error_message": "Saving will overwrite a newer version.",
|
|
1158
|
+
},
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
# Live DB record should be unchanged
|
|
1162
|
+
# (neither save_revision nor the failed form post should have updated it)
|
|
1163
|
+
self.test_snippet.refresh_from_db()
|
|
1164
|
+
self.assertEqual(self.test_snippet.text, "foo")
|
|
1165
|
+
|
|
1166
|
+
# The passed revision for overwriting, and the actual latest revision, should both be unchanged
|
|
1167
|
+
self.assertEqual(self.test_snippet.revisions.count(), 3)
|
|
1168
|
+
user_revision.refresh_from_db()
|
|
1169
|
+
self.assertEqual(user_revision.content["text"], "First update via autosave")
|
|
1170
|
+
later_revision.refresh_from_db()
|
|
1171
|
+
self.assertEqual(later_revision.content["text"], "Someone else's changed text")
|
|
1172
|
+
self.assertEqual(self.test_snippet.get_latest_revision().id, later_revision.id)
|
|
1173
|
+
|
|
1174
|
+
def test_overwrite_nonexistent_revision(self):
|
|
1175
|
+
self.test_snippet.text = "Initial revision"
|
|
1176
|
+
user_revision = self.test_snippet.save_revision(user=self.user)
|
|
1177
|
+
self.assertEqual(self.test_snippet.revisions.count(), 1)
|
|
1178
|
+
|
|
1179
|
+
post_data = {
|
|
1180
|
+
"text": "Updated revision",
|
|
1181
|
+
"overwrite_revision_id": 999999,
|
|
1182
|
+
}
|
|
1183
|
+
response = self.post(
|
|
1184
|
+
post_data=post_data,
|
|
1185
|
+
headers={"Accept": "application/json"},
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
# Should be a 400 response
|
|
1189
|
+
self.assertEqual(response.status_code, 400)
|
|
1190
|
+
self.assertEqual(response["Content-Type"], "application/json")
|
|
1191
|
+
self.assertEqual(
|
|
1192
|
+
response.json(),
|
|
1193
|
+
{
|
|
1194
|
+
"success": False,
|
|
1195
|
+
"error_code": "invalid_revision",
|
|
1196
|
+
# We only naively check whether overwrite_revision_id matches
|
|
1197
|
+
# the latest revision ID, and if it doesn't, we assume there's
|
|
1198
|
+
# a newer revision.
|
|
1199
|
+
"error_message": "Saving will overwrite a newer version.",
|
|
1200
|
+
},
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
# Live DB record should be unchanged
|
|
1204
|
+
# (neither save_revision nor the failed form post should have updated it)
|
|
1205
|
+
self.test_snippet.refresh_from_db()
|
|
1206
|
+
self.assertEqual(self.test_snippet.text, "foo")
|
|
1207
|
+
|
|
1208
|
+
# The latest revision should be unchanged
|
|
1209
|
+
self.assertEqual(self.test_snippet.revisions.count(), 1)
|
|
1210
|
+
latest_revision = self.test_snippet.get_latest_revision()
|
|
1211
|
+
self.assertEqual(latest_revision.id, user_revision.id)
|
|
1212
|
+
self.assertEqual(latest_revision.content["text"], "Initial revision")
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|
1216
|
+
STATUS_TOGGLE_BADGE_REGEX = (
|
|
1217
|
+
r'data-side-panel-toggle="status"[^<]+<svg[^<]+<use[^<]+</use[^<]+</svg[^<]+'
|
|
1218
|
+
r"<div data-side-panel-toggle-counter[^>]+w-bg-critical-200[^>]+>\s*%(num_errors)s\s*</div>"
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
def setUp(self):
|
|
1222
|
+
super().setUp()
|
|
1223
|
+
self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.create(
|
|
1224
|
+
custom_id="custom/1", text="Draft-enabled Foo", live=False
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
def test_get(self):
|
|
1228
|
+
revision = self.test_snippet.save_revision()
|
|
1229
|
+
response = self.get()
|
|
1230
|
+
|
|
1231
|
+
self.assertEqual(response.status_code, 200)
|
|
1232
|
+
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
|
|
1233
|
+
|
|
1234
|
+
# The save button should be labelled "Save draft"
|
|
1235
|
+
self.assertContains(response, "Save draft")
|
|
1236
|
+
# The publish button should exist
|
|
1237
|
+
self.assertContains(response, "Publish")
|
|
1238
|
+
# The publish button should have name="action-publish"
|
|
1239
|
+
self.assertContains(
|
|
1240
|
+
response,
|
|
1241
|
+
'<button\n type="submit"\n name="action-publish"\n value="action-publish"\n class="button action-save button-longrunning"\n data-controller="w-progress"\n data-action="w-progress#activate"\n',
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
# The status side panel should show "No publishing schedule set" info
|
|
1245
|
+
self.assertContains(response, "No publishing schedule set")
|
|
1246
|
+
|
|
1247
|
+
# Should show the "Set schedule" button
|
|
1248
|
+
self.assertSchedulingDialogRendered(response, label="Set schedule")
|
|
1249
|
+
|
|
1250
|
+
# Should show the correct subtitle in the dialog
|
|
1251
|
+
self.assertContains(
|
|
1252
|
+
response,
|
|
1253
|
+
"Choose when this draft state custom primary key model should go live and/or expire",
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
# Should not show the Unpublish action menu item
|
|
1257
|
+
unpublish_url = reverse(
|
|
1258
|
+
"wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish",
|
|
1259
|
+
args=(quote(self.test_snippet.pk),),
|
|
1260
|
+
)
|
|
1261
|
+
self.assertNotContains(
|
|
1262
|
+
response,
|
|
1263
|
+
f'<a class="button" href="{unpublish_url}">',
|
|
1264
|
+
)
|
|
1265
|
+
self.assertNotContains(response, "Unpublish")
|
|
1266
|
+
|
|
1267
|
+
soup = self.get_soup(response.content)
|
|
1268
|
+
form = soup.select_one("form[data-edit-form]")
|
|
1269
|
+
self.assertIsNotNone(form)
|
|
1270
|
+
loaded_revision = form.select_one("input[name='loaded_revision_id']")
|
|
1271
|
+
self.assertIsNotNone(loaded_revision)
|
|
1272
|
+
self.assertEqual(int(loaded_revision["value"]), revision.pk)
|
|
1273
|
+
loaded_timestamp = form.select_one("input[name='loaded_revision_created_at']")
|
|
1274
|
+
self.assertIsNotNone(loaded_timestamp)
|
|
1275
|
+
self.assertEqual(loaded_timestamp["value"], revision.created_at.isoformat())
|
|
1276
|
+
|
|
1277
|
+
# Autosave defaults to enabled with 500ms interval
|
|
1278
|
+
soup = self.get_soup(response.content)
|
|
1279
|
+
form = soup.select_one("form[data-edit-form]")
|
|
1280
|
+
self.assertIsNotNone(form)
|
|
1281
|
+
self.assertIn("w-autosave", form["data-controller"].split())
|
|
1282
|
+
self.assertTrue(
|
|
1283
|
+
{
|
|
1284
|
+
"w-unsaved:add->w-autosave#save:prevent",
|
|
1285
|
+
"w-autosave:success->w-unsaved#clear",
|
|
1286
|
+
}.issubset(form["data-action"].split())
|
|
1287
|
+
)
|
|
1288
|
+
self.assertEqual(form.attrs.get("data-w-autosave-interval-value"), "500")
|
|
1289
|
+
|
|
1290
|
+
def test_get_hydrate_create_view(self):
|
|
1291
|
+
# Use FullFeaturedSnippet to test the UI hydration of all features
|
|
1292
|
+
snippet = FullFeaturedSnippet.objects.create(
|
|
1293
|
+
text="Hello world",
|
|
1294
|
+
country_code="UK",
|
|
1295
|
+
some_number=42,
|
|
1296
|
+
)
|
|
1297
|
+
latest_revision = snippet.save_revision(user=self.user)
|
|
1298
|
+
edit_url = reverse(
|
|
1299
|
+
snippet.snippet_viewset.get_url_name("edit"),
|
|
1300
|
+
args=(quote(snippet.pk),),
|
|
1301
|
+
)
|
|
1302
|
+
response = self.client.get(edit_url, {"_w_hydrate_create_view": "1"})
|
|
1303
|
+
self.assertEqual(response.status_code, 200)
|
|
1304
|
+
self.assertTemplateUsed(response, "wagtailadmin/generic/edit_partials.html")
|
|
1305
|
+
soup = self.get_soup(response.content)
|
|
1306
|
+
|
|
1307
|
+
# Should reload the status and preview side panels only
|
|
1308
|
+
side_panels = soup.select(
|
|
1309
|
+
"template[data-controller='w-teleport']"
|
|
1310
|
+
"[data-w-teleport-target-value^='[data-side-panel=']"
|
|
1311
|
+
"[data-w-teleport-mode-value='innerHTML']"
|
|
1312
|
+
)
|
|
1313
|
+
self.assertEqual(len(side_panels), 2)
|
|
1314
|
+
status_side_panel = side_panels[0]
|
|
1315
|
+
self.assertEqual(
|
|
1316
|
+
status_side_panel["data-w-teleport-target-value"],
|
|
1317
|
+
"[data-side-panel='status']",
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
# Under normal circumstances, a newly-created snippet would never
|
|
1321
|
+
# immediately enter a workflow without a full-page reload, so don't
|
|
1322
|
+
# bother rendering the workflow status dialog when hydrating a create view
|
|
1323
|
+
workflow_status_dialog = soup.find("div", id="workflow-status-dialog")
|
|
1324
|
+
self.assertIsNone(workflow_status_dialog)
|
|
1325
|
+
# Privacy features are not available for snippets
|
|
1326
|
+
set_privacy_dialog = soup.find("div", id="set-privacy")
|
|
1327
|
+
self.assertIsNone(set_privacy_dialog)
|
|
1328
|
+
|
|
1329
|
+
# We need to change the preview URL to use the one for editing, but there is
|
|
1330
|
+
# no way to declaratively change attributes via partial rendering yet, and we
|
|
1331
|
+
# need to restart the controller anyway, so just re-render the whole panel
|
|
1332
|
+
preview_side_panel = side_panels[1]
|
|
1333
|
+
self.assertEqual(
|
|
1334
|
+
preview_side_panel["data-w-teleport-target-value"],
|
|
1335
|
+
"[data-side-panel='preview']",
|
|
1336
|
+
)
|
|
1337
|
+
preview_url = reverse(
|
|
1338
|
+
snippet.snippet_viewset.get_url_name("preview_on_edit"),
|
|
1339
|
+
args=(quote(snippet.pk),),
|
|
1340
|
+
)
|
|
1341
|
+
self.assertIsNotNone(
|
|
1342
|
+
preview_side_panel.select_one(f"[data-w-preview-url-value='{preview_url}']")
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
breadcrumbs = soup.find(
|
|
1346
|
+
"template",
|
|
1347
|
+
{
|
|
1348
|
+
"data-controller": "w-teleport",
|
|
1349
|
+
"data-w-teleport-target-value": "header [data-w-breadcrumbs]",
|
|
1350
|
+
"data-w-teleport-mode-value": "outerHTML",
|
|
1351
|
+
},
|
|
1352
|
+
)
|
|
1353
|
+
self.assertIsNotNone(breadcrumbs)
|
|
1354
|
+
# Should include header buttons as they were not rendered in the create view
|
|
1355
|
+
self.assertIsNotNone(breadcrumbs.select_one("#w-slim-header-buttons"))
|
|
1356
|
+
|
|
1357
|
+
# Should render the history link button as it wasn't rendered in the create view
|
|
1358
|
+
history_link = soup.find(
|
|
1359
|
+
"template",
|
|
1360
|
+
{
|
|
1361
|
+
"data-controller": "w-teleport",
|
|
1362
|
+
"data-w-teleport-target-value": "[data-side-panel-toggle]:last-of-type",
|
|
1363
|
+
"data-w-teleport-mode-value": "afterend",
|
|
1364
|
+
},
|
|
1365
|
+
)
|
|
1366
|
+
history_url = reverse(
|
|
1367
|
+
snippet.snippet_viewset.get_url_name("history"),
|
|
1368
|
+
args=(quote(snippet.pk),),
|
|
1369
|
+
)
|
|
1370
|
+
self.assertIsNotNone(history_link)
|
|
1371
|
+
self.assertIsNotNone(history_link.select_one(f"a[href='{history_url}']"))
|
|
1372
|
+
|
|
1373
|
+
form_title_heading = soup.find(
|
|
1374
|
+
"template",
|
|
1375
|
+
{
|
|
1376
|
+
"data-controller": "w-teleport",
|
|
1377
|
+
"data-w-teleport-target-value": "#header-title span",
|
|
1378
|
+
"data-w-teleport-mode-value": "textContent",
|
|
1379
|
+
},
|
|
1380
|
+
)
|
|
1381
|
+
self.assertIsNotNone(form_title_heading)
|
|
1382
|
+
self.assertEqual(form_title_heading.text.strip(), str(snippet))
|
|
1383
|
+
header_title = soup.find(
|
|
1384
|
+
"template",
|
|
1385
|
+
{
|
|
1386
|
+
"data-controller": "w-teleport",
|
|
1387
|
+
"data-w-teleport-target-value": "head title",
|
|
1388
|
+
"data-w-teleport-mode-value": "textContent",
|
|
1389
|
+
},
|
|
1390
|
+
)
|
|
1391
|
+
self.assertIsNotNone(header_title)
|
|
1392
|
+
self.assertEqual(header_title.text.strip(), f"Editing: {snippet}")
|
|
1393
|
+
|
|
1394
|
+
# Should include loaded revision ID and timestamp in the form for
|
|
1395
|
+
# subsequent autosave requests
|
|
1396
|
+
form_adds = soup.find(
|
|
1397
|
+
"template",
|
|
1398
|
+
{
|
|
1399
|
+
"data-controller": "w-teleport",
|
|
1400
|
+
"data-w-teleport-target-value": "form[data-edit-form]",
|
|
1401
|
+
"data-w-teleport-mode-value": "afterbegin",
|
|
1402
|
+
},
|
|
1403
|
+
)
|
|
1404
|
+
self.assertIsNotNone(form_adds)
|
|
1405
|
+
self.assertEqual(
|
|
1406
|
+
form_adds.select_one("input[name='loaded_revision_id']")["value"],
|
|
1407
|
+
str(latest_revision.pk),
|
|
1408
|
+
)
|
|
1409
|
+
self.assertEqual(
|
|
1410
|
+
form_adds.select_one("input[name='loaded_revision_created_at']")["value"],
|
|
1411
|
+
latest_revision.created_at.isoformat(),
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
# Should load the editing sessions module as it was not in the create view
|
|
1415
|
+
editing_sessions = soup.find(
|
|
1416
|
+
"template",
|
|
1417
|
+
{
|
|
1418
|
+
"data-controller": "w-teleport",
|
|
1419
|
+
"data-w-teleport-target-value": "#w-autosave-indicator",
|
|
1420
|
+
"data-w-teleport-mode-value": "afterend",
|
|
1421
|
+
},
|
|
1422
|
+
)
|
|
1423
|
+
self.assertIsNotNone(editing_sessions)
|
|
1424
|
+
# with the revision info
|
|
1425
|
+
self.assertEqual(
|
|
1426
|
+
editing_sessions.select_one("input[name='revision_id']")["value"],
|
|
1427
|
+
str(latest_revision.pk),
|
|
1428
|
+
)
|
|
1429
|
+
self.assertEqual(
|
|
1430
|
+
editing_sessions.select_one("input[name='revision_created_at']")["value"],
|
|
1431
|
+
latest_revision.created_at.isoformat(),
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
@override_settings(WAGTAIL_AUTOSAVE_INTERVAL=0)
|
|
1435
|
+
def test_autosave_disabled(self):
|
|
1436
|
+
response = self.get()
|
|
1437
|
+
self.assertEqual(response.status_code, 200)
|
|
1438
|
+
soup = self.get_soup(response.content)
|
|
1439
|
+
form = soup.select_one("form[data-edit-form]")
|
|
1440
|
+
self.assertIsNotNone(form)
|
|
1441
|
+
self.assertNotIn("w-autosave", form["data-controller"].split())
|
|
1442
|
+
self.assertNotIn("w-autosave", form["data-action"])
|
|
1443
|
+
self.assertIsNone(form.attrs.get("data-w-autosave-interval-value"))
|
|
1444
|
+
|
|
1445
|
+
@override_settings(WAGTAIL_AUTOSAVE_INTERVAL=2000)
|
|
1446
|
+
def test_autosave_custom_interval(self):
|
|
1447
|
+
response = self.get()
|
|
1448
|
+
self.assertEqual(response.status_code, 200)
|
|
1449
|
+
soup = self.get_soup(response.content)
|
|
1450
|
+
form = soup.select_one("form[data-edit-form]")
|
|
1451
|
+
self.assertIsNotNone(form)
|
|
1452
|
+
self.assertIn("w-autosave", form["data-controller"].split())
|
|
1453
|
+
self.assertTrue(
|
|
1454
|
+
{
|
|
1455
|
+
"w-unsaved:add->w-autosave#save:prevent",
|
|
1456
|
+
"w-autosave:success->w-unsaved#clear",
|
|
1457
|
+
}.issubset(form["data-action"].split())
|
|
1458
|
+
)
|
|
1459
|
+
self.assertEqual(form.attrs.get("data-w-autosave-interval-value"), "2000")
|
|
1460
|
+
|
|
1461
|
+
def test_save_draft(self):
|
|
1462
|
+
response = self.post(post_data={"text": "Draft-enabled Bar"})
|
|
1463
|
+
self.test_snippet.refresh_from_db()
|
|
1464
|
+
revisions = Revision.objects.for_instance(self.test_snippet)
|
|
1465
|
+
latest_revision = self.test_snippet.latest_revision
|
|
1466
|
+
|
|
1467
|
+
self.assertRedirects(response, self.get_edit_url())
|
|
1468
|
+
|
|
1469
|
+
# The instance should be updated, since it is still a draft
|
|
1470
|
+
self.assertEqual(self.test_snippet.text, "Draft-enabled Bar")
|
|
1471
|
+
|
|
1472
|
+
# The instance should be a draft
|
|
1473
|
+
self.assertFalse(self.test_snippet.live)
|
|
1474
|
+
self.assertTrue(self.test_snippet.has_unpublished_changes)
|
|
1475
|
+
self.assertIsNone(self.test_snippet.first_published_at)
|
|
1476
|
+
self.assertIsNone(self.test_snippet.last_published_at)
|
|
1477
|
+
self.assertIsNone(self.test_snippet.live_revision)
|
|
1478
|
+
|
|
1479
|
+
# The revision should be created and set as latest_revision
|
|
1480
|
+
self.assertEqual(revisions.count(), 1)
|
|
1481
|
+
self.assertEqual(latest_revision, revisions.first())
|
|
1482
|
+
|
|
1483
|
+
# The revision content should contain the new data
|
|
1484
|
+
self.assertEqual(latest_revision.content["text"], "Draft-enabled Bar")
|
|
1485
|
+
|
|
1486
|
+
# A log entry should be created
|
|
1487
|
+
log_entry = (
|
|
1488
|
+
ModelLogEntry.objects.for_instance(self.test_snippet)
|
|
1489
|
+
.filter(action="wagtail.edit")
|
|
1490
|
+
.order_by("-timestamp")
|
|
1491
|
+
.first()
|
|
1492
|
+
)
|
|
1493
|
+
self.assertEqual(log_entry.revision, self.test_snippet.latest_revision)
|
|
1494
|
+
self.assertEqual(log_entry.label, "Draft-enabled Bar")
|
|
1495
|
+
|
|
1496
|
+
def test_skip_validation_on_save_draft(self):
|
|
1497
|
+
response = self.post(post_data={"text": ""})
|
|
1498
|
+
self.test_snippet.refresh_from_db()
|
|
1499
|
+
revisions = Revision.objects.for_instance(self.test_snippet)
|
|
1500
|
+
latest_revision = self.test_snippet.latest_revision
|
|
1501
|
+
|
|
1502
|
+
self.assertRedirects(response, self.get_edit_url())
|
|
1503
|
+
|
|
1504
|
+
# The instance should be updated, since it is still a draft
|
|
1505
|
+
self.assertEqual(self.test_snippet.text, "")
|
|
1506
|
+
|
|
1507
|
+
# The instance should be a draft
|
|
1508
|
+
self.assertFalse(self.test_snippet.live)
|
|
1509
|
+
self.assertTrue(self.test_snippet.has_unpublished_changes)
|
|
1510
|
+
self.assertIsNone(self.test_snippet.first_published_at)
|
|
1511
|
+
self.assertIsNone(self.test_snippet.last_published_at)
|
|
1512
|
+
self.assertIsNone(self.test_snippet.live_revision)
|
|
1513
|
+
|
|
1514
|
+
# The revision should be created and set as latest_revision
|
|
1515
|
+
self.assertEqual(revisions.count(), 1)
|
|
1516
|
+
self.assertEqual(latest_revision, revisions.first())
|
|
1517
|
+
|
|
1518
|
+
# The revision content should contain the new data
|
|
1519
|
+
self.assertEqual(latest_revision.content["text"], "")
|
|
1520
|
+
|
|
1521
|
+
# A log entry should be created (with a fallback label)
|
|
1522
|
+
log_entry = (
|
|
1523
|
+
ModelLogEntry.objects.for_instance(self.test_snippet)
|
|
1524
|
+
.filter(action="wagtail.edit")
|
|
1525
|
+
.order_by("-timestamp")
|
|
1526
|
+
.first()
|
|
1527
|
+
)
|
|
1528
|
+
self.assertEqual(log_entry.revision, self.test_snippet.latest_revision)
|
|
1529
|
+
self.assertEqual(
|
|
1530
|
+
log_entry.label,
|
|
1531
|
+
f"DraftStateCustomPrimaryKeyModel object ({self.test_snippet.pk})",
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
def test_required_asterisk_on_reshowing_form(self):
|
|
1535
|
+
"""
|
|
1536
|
+
If a form is reshown due to a validation error elsewhere, fields whose validation
|
|
1537
|
+
was deferred should still show the required asterisk.
|
|
1538
|
+
"""
|
|
1539
|
+
snippet = FullFeaturedSnippet.objects.create(
|
|
1540
|
+
text="Hello world",
|
|
1541
|
+
country_code="UK",
|
|
1542
|
+
some_number=42,
|
|
1543
|
+
)
|
|
1544
|
+
response = self.client.post(
|
|
1545
|
+
reverse("some_namespace:edit", args=[snippet.pk]),
|
|
1546
|
+
{"text": "", "country_code": "UK", "some_number": "meef"},
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
self.assertEqual(response.status_code, 200)
|
|
1550
|
+
|
|
1551
|
+
# The empty text should not cause a validation error, but the invalid number should
|
|
1552
|
+
self.assertNotContains(response, "This field is required.")
|
|
1553
|
+
self.assertContains(response, "Enter a whole number.", count=1)
|
|
1554
|
+
|
|
1555
|
+
soup = self.get_soup(response.content)
|
|
1556
|
+
self.assertTrue(soup.select_one('label[for="id_text"] > span.w-required-mark'))
|
|
1557
|
+
|
|
1558
|
+
def test_cannot_publish_invalid(self):
|
|
1559
|
+
# Connect a mock signal handler to published signal
|
|
1560
|
+
mock_handler = mock.MagicMock()
|
|
1561
|
+
published.connect(mock_handler)
|
|
1562
|
+
|
|
1563
|
+
try:
|
|
1564
|
+
response = self.post(
|
|
1565
|
+
post_data={
|
|
1566
|
+
"text": "",
|
|
1567
|
+
"action-publish": "action-publish",
|
|
1568
|
+
}
|
|
1569
|
+
)
|
|
1570
|
+
|
|
1571
|
+
self.test_snippet.refresh_from_db()
|
|
1572
|
+
|
|
1573
|
+
self.assertEqual(response.status_code, 200)
|
|
1574
|
+
self.assertContains(
|
|
1575
|
+
response,
|
|
1576
|
+
"The draft state custom primary key model could not be saved due to errors.",
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
# The instance should be unchanged
|
|
1580
|
+
self.assertEqual(self.test_snippet.text, "Draft-enabled Foo")
|
|
1581
|
+
self.assertFalse(self.test_snippet.live)
|
|
1582
|
+
|
|
1583
|
+
# The published signal should not have been fired
|
|
1584
|
+
self.assertEqual(mock_handler.call_count, 0)
|
|
1585
|
+
finally:
|
|
1586
|
+
published.disconnect(mock_handler)
|
|
1587
|
+
|
|
1588
|
+
def test_publish(self):
|
|
1589
|
+
# Connect a mock signal handler to published signal
|
|
1590
|
+
mock_handler = mock.MagicMock()
|
|
1591
|
+
published.connect(mock_handler)
|
|
1592
|
+
|
|
1593
|
+
try:
|
|
1594
|
+
timestamp = now()
|
|
1595
|
+
with freeze_time(timestamp):
|
|
1596
|
+
response = self.post(
|
|
1597
|
+
post_data={
|
|
1598
|
+
"text": "Draft-enabled Bar, Published",
|
|
1599
|
+
"action-publish": "action-publish",
|
|
1600
|
+
}
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
self.test_snippet.refresh_from_db()
|
|
1604
|
+
revisions = Revision.objects.for_instance(self.test_snippet)
|
|
1605
|
+
latest_revision = self.test_snippet.latest_revision
|
|
1606
|
+
|
|
1607
|
+
log_entries = ModelLogEntry.objects.filter(
|
|
1608
|
+
content_type=ContentType.objects.get_for_model(
|
|
1609
|
+
DraftStateCustomPrimaryKeyModel
|
|
1610
|
+
),
|
|
1611
|
+
action="wagtail.publish",
|
|
1612
|
+
object_id=self.test_snippet.pk,
|
|
1613
|
+
)
|
|
1614
|
+
log_entry = log_entries.first()
|
|
1615
|
+
|
|
1616
|
+
self.assertRedirects(
|
|
1617
|
+
response,
|
|
1618
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
# The instance should be updated
|
|
1622
|
+
self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Published")
|
|
1623
|
+
|
|
1624
|
+
# The instance should be live
|
|
1625
|
+
self.assertTrue(self.test_snippet.live)
|
|
1626
|
+
self.assertFalse(self.test_snippet.has_unpublished_changes)
|
|
1627
|
+
self.assertEqual(self.test_snippet.first_published_at, timestamp)
|
|
1628
|
+
self.assertEqual(self.test_snippet.last_published_at, timestamp)
|
|
1629
|
+
self.assertEqual(self.test_snippet.live_revision, latest_revision)
|
|
1630
|
+
|
|
1631
|
+
# The revision should be created and set as latest_revision
|
|
1632
|
+
self.assertEqual(revisions.count(), 1)
|
|
1633
|
+
self.assertEqual(latest_revision, revisions.first())
|
|
1634
|
+
|
|
1635
|
+
# The revision content should contain the new data
|
|
1636
|
+
self.assertEqual(
|
|
1637
|
+
latest_revision.content["text"],
|
|
1638
|
+
"Draft-enabled Bar, Published",
|
|
1639
|
+
)
|
|
1640
|
+
|
|
1641
|
+
# A log entry with wagtail.publish action should be created
|
|
1642
|
+
self.assertEqual(log_entries.count(), 1)
|
|
1643
|
+
self.assertEqual(log_entry.timestamp, timestamp)
|
|
1644
|
+
|
|
1645
|
+
# Check that the published signal was fired
|
|
1646
|
+
self.assertEqual(mock_handler.call_count, 1)
|
|
1647
|
+
mock_call = mock_handler.mock_calls[0][2]
|
|
1648
|
+
|
|
1649
|
+
self.assertEqual(mock_call["sender"], DraftStateCustomPrimaryKeyModel)
|
|
1650
|
+
self.assertEqual(mock_call["instance"], self.test_snippet)
|
|
1651
|
+
self.assertIsInstance(
|
|
1652
|
+
mock_call["instance"], DraftStateCustomPrimaryKeyModel
|
|
1653
|
+
)
|
|
1654
|
+
finally:
|
|
1655
|
+
published.disconnect(mock_handler)
|
|
1656
|
+
|
|
1657
|
+
def test_publish_bad_permissions(self):
|
|
1658
|
+
# Only add edit permission
|
|
1659
|
+
self.user.is_superuser = False
|
|
1660
|
+
edit_permission = Permission.objects.get(
|
|
1661
|
+
content_type__app_label="tests",
|
|
1662
|
+
codename="change_draftstatecustomprimarykeymodel",
|
|
1663
|
+
)
|
|
1664
|
+
admin_permission = Permission.objects.get(
|
|
1665
|
+
content_type__app_label="wagtailadmin",
|
|
1666
|
+
codename="access_admin",
|
|
1667
|
+
)
|
|
1668
|
+
self.user.user_permissions.add(edit_permission, admin_permission)
|
|
1669
|
+
self.user.save()
|
|
1670
|
+
|
|
1671
|
+
# Connect a mock signal handler to published signal
|
|
1672
|
+
mock_handler = mock.MagicMock()
|
|
1673
|
+
published.connect(mock_handler)
|
|
1674
|
+
|
|
1675
|
+
try:
|
|
1676
|
+
response = self.post(
|
|
1677
|
+
post_data={
|
|
1678
|
+
"text": "Edited draft Foo",
|
|
1679
|
+
"action-publish": "action-publish",
|
|
1680
|
+
}
|
|
1681
|
+
)
|
|
1682
|
+
self.test_snippet.refresh_from_db()
|
|
1683
|
+
|
|
1684
|
+
# Should remain on the edit page
|
|
1685
|
+
self.assertRedirects(response, self.get_edit_url())
|
|
1686
|
+
|
|
1687
|
+
# The instance should be edited, since it is still a draft
|
|
1688
|
+
self.assertEqual(self.test_snippet.text, "Edited draft Foo")
|
|
1689
|
+
|
|
1690
|
+
# The instance should not be live
|
|
1691
|
+
self.assertFalse(self.test_snippet.live)
|
|
1692
|
+
self.assertTrue(self.test_snippet.has_unpublished_changes)
|
|
1693
|
+
|
|
1694
|
+
# A revision should be created and set as latest_revision, but not live_revision
|
|
1695
|
+
self.assertIsNotNone(self.test_snippet.latest_revision)
|
|
1696
|
+
self.assertIsNone(self.test_snippet.live_revision)
|
|
1697
|
+
|
|
1698
|
+
# The revision content should contain the data
|
|
1699
|
+
self.assertEqual(
|
|
1700
|
+
self.test_snippet.latest_revision.content["text"],
|
|
1701
|
+
"Edited draft Foo",
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
# Check that the published signal was not fired
|
|
1705
|
+
self.assertEqual(mock_handler.call_count, 0)
|
|
1706
|
+
finally:
|
|
1707
|
+
published.disconnect(mock_handler)
|
|
1708
|
+
|
|
1709
|
+
def test_publish_with_publish_permission(self):
|
|
1710
|
+
# Only add edit and publish permissions
|
|
1711
|
+
self.user.is_superuser = False
|
|
1712
|
+
edit_permission = Permission.objects.get(
|
|
1713
|
+
content_type__app_label="tests",
|
|
1714
|
+
codename="change_draftstatecustomprimarykeymodel",
|
|
1715
|
+
)
|
|
1716
|
+
publish_permission = Permission.objects.get(
|
|
1717
|
+
content_type__app_label="tests",
|
|
1718
|
+
codename="publish_draftstatecustomprimarykeymodel",
|
|
1719
|
+
)
|
|
1720
|
+
admin_permission = Permission.objects.get(
|
|
1721
|
+
content_type__app_label="wagtailadmin", codename="access_admin"
|
|
1722
|
+
)
|
|
1723
|
+
self.user.user_permissions.add(
|
|
1724
|
+
edit_permission,
|
|
1725
|
+
publish_permission,
|
|
1726
|
+
admin_permission,
|
|
1727
|
+
)
|
|
1728
|
+
self.user.save()
|
|
1729
|
+
|
|
1730
|
+
# Connect a mock signal handler to published signal
|
|
1731
|
+
mock_handler = mock.MagicMock()
|
|
1732
|
+
published.connect(mock_handler)
|
|
1733
|
+
|
|
1734
|
+
try:
|
|
1735
|
+
timestamp = now()
|
|
1736
|
+
with freeze_time(timestamp):
|
|
1737
|
+
response = self.post(
|
|
1738
|
+
post_data={
|
|
1739
|
+
"text": "Draft-enabled Bar, Published",
|
|
1740
|
+
"action-publish": "action-publish",
|
|
1741
|
+
}
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
self.test_snippet.refresh_from_db()
|
|
1745
|
+
revisions = Revision.objects.for_instance(self.test_snippet)
|
|
1746
|
+
latest_revision = self.test_snippet.latest_revision
|
|
1747
|
+
|
|
1748
|
+
log_entries = ModelLogEntry.objects.filter(
|
|
1749
|
+
content_type=ContentType.objects.get_for_model(
|
|
1750
|
+
DraftStateCustomPrimaryKeyModel
|
|
1751
|
+
),
|
|
1752
|
+
action="wagtail.publish",
|
|
1753
|
+
object_id=self.test_snippet.pk,
|
|
1754
|
+
)
|
|
1755
|
+
log_entry = log_entries.first()
|
|
1756
|
+
|
|
1757
|
+
self.assertRedirects(
|
|
1758
|
+
response,
|
|
1759
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
# The instance should be updated
|
|
1763
|
+
self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Published")
|
|
1764
|
+
|
|
1765
|
+
# The instance should be live
|
|
1766
|
+
self.assertTrue(self.test_snippet.live)
|
|
1767
|
+
self.assertFalse(self.test_snippet.has_unpublished_changes)
|
|
1768
|
+
self.assertEqual(self.test_snippet.first_published_at, timestamp)
|
|
1769
|
+
self.assertEqual(self.test_snippet.last_published_at, timestamp)
|
|
1770
|
+
self.assertEqual(self.test_snippet.live_revision, latest_revision)
|
|
1771
|
+
|
|
1772
|
+
# The revision should be created and set as latest_revision
|
|
1773
|
+
self.assertEqual(revisions.count(), 1)
|
|
1774
|
+
self.assertEqual(latest_revision, revisions.first())
|
|
1775
|
+
|
|
1776
|
+
# The revision content should contain the new data
|
|
1777
|
+
self.assertEqual(
|
|
1778
|
+
latest_revision.content["text"],
|
|
1779
|
+
"Draft-enabled Bar, Published",
|
|
1780
|
+
)
|
|
1781
|
+
|
|
1782
|
+
# A log entry with wagtail.publish action should be created
|
|
1783
|
+
self.assertEqual(log_entries.count(), 1)
|
|
1784
|
+
self.assertEqual(log_entry.timestamp, timestamp)
|
|
1785
|
+
|
|
1786
|
+
# Check that the published signal was fired
|
|
1787
|
+
self.assertEqual(mock_handler.call_count, 1)
|
|
1788
|
+
mock_call = mock_handler.mock_calls[0][2]
|
|
1789
|
+
|
|
1790
|
+
self.assertEqual(mock_call["sender"], DraftStateCustomPrimaryKeyModel)
|
|
1791
|
+
self.assertEqual(mock_call["instance"], self.test_snippet)
|
|
1792
|
+
self.assertIsInstance(
|
|
1793
|
+
mock_call["instance"], DraftStateCustomPrimaryKeyModel
|
|
1794
|
+
)
|
|
1795
|
+
finally:
|
|
1796
|
+
published.disconnect(mock_handler)
|
|
1797
|
+
|
|
1798
|
+
def test_save_draft_then_publish(self):
|
|
1799
|
+
save_timestamp = now()
|
|
1800
|
+
with freeze_time(save_timestamp):
|
|
1801
|
+
self.test_snippet.text = "Draft-enabled Bar, In Draft"
|
|
1802
|
+
self.test_snippet.save_revision()
|
|
1803
|
+
|
|
1804
|
+
publish_timestamp = now()
|
|
1805
|
+
with freeze_time(publish_timestamp):
|
|
1806
|
+
response = self.post(
|
|
1807
|
+
post_data={
|
|
1808
|
+
"text": "Draft-enabled Bar, Now Published",
|
|
1809
|
+
"action-publish": "action-publish",
|
|
1810
|
+
}
|
|
1811
|
+
)
|
|
1812
|
+
|
|
1813
|
+
self.test_snippet.refresh_from_db()
|
|
1814
|
+
revisions = Revision.objects.for_instance(self.test_snippet).order_by("pk")
|
|
1815
|
+
latest_revision = self.test_snippet.latest_revision
|
|
1816
|
+
|
|
1817
|
+
self.assertRedirects(
|
|
1818
|
+
response,
|
|
1819
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
1820
|
+
)
|
|
1821
|
+
|
|
1822
|
+
# The instance should be updated
|
|
1823
|
+
self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Now Published")
|
|
1824
|
+
|
|
1825
|
+
# The instance should be live
|
|
1826
|
+
self.assertTrue(self.test_snippet.live)
|
|
1827
|
+
self.assertFalse(self.test_snippet.has_unpublished_changes)
|
|
1828
|
+
self.assertEqual(self.test_snippet.first_published_at, publish_timestamp)
|
|
1829
|
+
self.assertEqual(self.test_snippet.last_published_at, publish_timestamp)
|
|
1830
|
+
self.assertEqual(self.test_snippet.live_revision, latest_revision)
|
|
1831
|
+
|
|
1832
|
+
# The revision should be created and set as latest_revision
|
|
1833
|
+
self.assertEqual(revisions.count(), 2)
|
|
1834
|
+
self.assertEqual(latest_revision, revisions.last())
|
|
1835
|
+
|
|
1836
|
+
# The revision content should contain the new data
|
|
1837
|
+
self.assertEqual(
|
|
1838
|
+
latest_revision.content["text"],
|
|
1839
|
+
"Draft-enabled Bar, Now Published",
|
|
1840
|
+
)
|
|
1841
|
+
|
|
1842
|
+
def test_publish_then_save_draft(self):
|
|
1843
|
+
publish_timestamp = now()
|
|
1844
|
+
with freeze_time(publish_timestamp):
|
|
1845
|
+
self.test_snippet.text = "Draft-enabled Bar, Published"
|
|
1846
|
+
self.test_snippet.save_revision().publish()
|
|
1847
|
+
|
|
1848
|
+
save_timestamp = now()
|
|
1849
|
+
with freeze_time(save_timestamp):
|
|
1850
|
+
response = self.post(
|
|
1851
|
+
post_data={"text": "Draft-enabled Bar, Published and In Draft"}
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
self.test_snippet.refresh_from_db()
|
|
1855
|
+
revisions = Revision.objects.for_instance(self.test_snippet).order_by("pk")
|
|
1856
|
+
latest_revision = self.test_snippet.latest_revision
|
|
1857
|
+
|
|
1858
|
+
self.assertRedirects(response, self.get_edit_url())
|
|
1859
|
+
|
|
1860
|
+
# The instance should be updated with the last published changes
|
|
1861
|
+
self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Published")
|
|
1862
|
+
|
|
1863
|
+
# The instance should be live
|
|
1864
|
+
self.assertTrue(self.test_snippet.live)
|
|
1865
|
+
# The instance should have unpublished changes
|
|
1866
|
+
self.assertTrue(self.test_snippet.has_unpublished_changes)
|
|
1867
|
+
|
|
1868
|
+
self.assertEqual(self.test_snippet.first_published_at, publish_timestamp)
|
|
1869
|
+
self.assertEqual(self.test_snippet.last_published_at, publish_timestamp)
|
|
1870
|
+
|
|
1871
|
+
# The live revision should be the first revision
|
|
1872
|
+
self.assertEqual(self.test_snippet.live_revision, revisions.first())
|
|
1873
|
+
|
|
1874
|
+
# The second revision should be created and set as latest_revision
|
|
1875
|
+
self.assertEqual(revisions.count(), 2)
|
|
1876
|
+
self.assertEqual(latest_revision, revisions.last())
|
|
1877
|
+
|
|
1878
|
+
# The revision content should contain the new data
|
|
1879
|
+
self.assertEqual(
|
|
1880
|
+
latest_revision.content["text"],
|
|
1881
|
+
"Draft-enabled Bar, Published and In Draft",
|
|
1882
|
+
)
|
|
1883
|
+
|
|
1884
|
+
def test_publish_twice(self):
|
|
1885
|
+
first_timestamp = now()
|
|
1886
|
+
with freeze_time(first_timestamp):
|
|
1887
|
+
self.test_snippet.text = "Draft-enabled Bar, Published Once"
|
|
1888
|
+
self.test_snippet.save_revision().publish()
|
|
1889
|
+
|
|
1890
|
+
second_timestamp = now() + datetime.timedelta(days=1)
|
|
1891
|
+
with freeze_time(second_timestamp):
|
|
1892
|
+
response = self.post(
|
|
1893
|
+
post_data={
|
|
1894
|
+
"text": "Draft-enabled Bar, Published Twice",
|
|
1895
|
+
"action-publish": "action-publish",
|
|
1896
|
+
}
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
self.test_snippet.refresh_from_db()
|
|
1900
|
+
|
|
1901
|
+
revisions = Revision.objects.for_instance(self.test_snippet).order_by("pk")
|
|
1902
|
+
latest_revision = self.test_snippet.latest_revision
|
|
1903
|
+
|
|
1904
|
+
self.assertRedirects(
|
|
1905
|
+
response,
|
|
1906
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
# The instance should be updated with the last published changes
|
|
1910
|
+
self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Published Twice")
|
|
1911
|
+
|
|
1912
|
+
# The instance should be live
|
|
1913
|
+
self.assertTrue(self.test_snippet.live)
|
|
1914
|
+
self.assertFalse(self.test_snippet.has_unpublished_changes)
|
|
1915
|
+
|
|
1916
|
+
# The first_published_at and last_published_at should be set correctly
|
|
1917
|
+
self.assertEqual(self.test_snippet.first_published_at, first_timestamp)
|
|
1918
|
+
self.assertEqual(self.test_snippet.last_published_at, second_timestamp)
|
|
1919
|
+
|
|
1920
|
+
# The live revision should be the second revision
|
|
1921
|
+
self.assertEqual(self.test_snippet.live_revision, revisions.last())
|
|
1922
|
+
|
|
1923
|
+
# The second revision should be created and set as latest_revision
|
|
1924
|
+
self.assertEqual(revisions.count(), 2)
|
|
1925
|
+
self.assertEqual(latest_revision, revisions.last())
|
|
1926
|
+
|
|
1927
|
+
# The revision content should contain the new data
|
|
1928
|
+
self.assertEqual(
|
|
1929
|
+
latest_revision.content["text"],
|
|
1930
|
+
"Draft-enabled Bar, Published Twice",
|
|
1931
|
+
)
|
|
1932
|
+
|
|
1933
|
+
def test_get_after_save_draft(self):
|
|
1934
|
+
self.post(post_data={"text": "Draft-enabled Bar"})
|
|
1935
|
+
response = self.get()
|
|
1936
|
+
|
|
1937
|
+
self.assertEqual(response.status_code, 200)
|
|
1938
|
+
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
|
|
1939
|
+
|
|
1940
|
+
# Should not show the Live status
|
|
1941
|
+
self.assertNotContains(
|
|
1942
|
+
response,
|
|
1943
|
+
'<h3 id="status-sidebar-live" class="w-label-1 !w-mt-0 w-mb-1"><span class="w-sr-only">Status: </span>Live</h3>',
|
|
1944
|
+
html=True,
|
|
1945
|
+
)
|
|
1946
|
+
# Should show the Draft status
|
|
1947
|
+
self.assertContains(
|
|
1948
|
+
response,
|
|
1949
|
+
'<h3 id="status-sidebar-draft" class="w-label-1 !w-mt-0 w-mb-1"><span class="w-sr-only">Status: </span>Draft</h3>',
|
|
1950
|
+
html=True,
|
|
1951
|
+
)
|
|
1952
|
+
|
|
1953
|
+
# Should not show the Unpublish action menu item
|
|
1954
|
+
unpublish_url = reverse(
|
|
1955
|
+
"wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish",
|
|
1956
|
+
args=(quote(self.test_snippet.pk),),
|
|
1957
|
+
)
|
|
1958
|
+
self.assertNotContains(
|
|
1959
|
+
response,
|
|
1960
|
+
f'<a class="button" href="{unpublish_url}">',
|
|
1961
|
+
)
|
|
1962
|
+
self.assertNotContains(response, "Unpublish")
|
|
1963
|
+
|
|
1964
|
+
def test_get_after_publish(self):
|
|
1965
|
+
self.post(
|
|
1966
|
+
post_data={
|
|
1967
|
+
"text": "Draft-enabled Bar, Published",
|
|
1968
|
+
"action-publish": "action-publish",
|
|
1969
|
+
}
|
|
1970
|
+
)
|
|
1971
|
+
response = self.get()
|
|
1972
|
+
|
|
1973
|
+
self.assertEqual(response.status_code, 200)
|
|
1974
|
+
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
|
|
1975
|
+
|
|
1976
|
+
# Should show the Live status
|
|
1977
|
+
self.assertContains(
|
|
1978
|
+
response,
|
|
1979
|
+
'<h3 id="status-sidebar-live" class="w-label-1 !w-mt-0 w-mb-1"><span class="w-sr-only">Status: </span>Live</h3>',
|
|
1980
|
+
html=True,
|
|
1981
|
+
)
|
|
1982
|
+
# Should not show the Draft status
|
|
1983
|
+
self.assertNotContains(
|
|
1984
|
+
response,
|
|
1985
|
+
'<h3 id="status-sidebar-draft" class="w-label-1 !w-mt-0 w-mb-1"><span class="w-sr-only">Status: </span>Draft</h3>',
|
|
1986
|
+
html=True,
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
# Should show the Unpublish action menu item
|
|
1990
|
+
unpublish_url = reverse(
|
|
1991
|
+
"wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish",
|
|
1992
|
+
args=(quote(self.test_snippet.pk),),
|
|
1993
|
+
)
|
|
1994
|
+
self.assertContains(
|
|
1995
|
+
response,
|
|
1996
|
+
f'<a class="button" href="{unpublish_url}">',
|
|
1997
|
+
)
|
|
1998
|
+
self.assertContains(response, "Unpublish")
|
|
1999
|
+
|
|
2000
|
+
def test_get_after_publish_and_save_draft(self):
|
|
2001
|
+
self.post(
|
|
2002
|
+
post_data={
|
|
2003
|
+
"text": "Draft-enabled Bar, Published",
|
|
2004
|
+
"action-publish": "action-publish",
|
|
2005
|
+
}
|
|
2006
|
+
)
|
|
2007
|
+
self.post(post_data={"text": "Draft-enabled Bar, In Draft"})
|
|
2008
|
+
response = self.get()
|
|
2009
|
+
html = response.content.decode()
|
|
2010
|
+
|
|
2011
|
+
self.assertEqual(response.status_code, 200)
|
|
2012
|
+
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
|
|
2013
|
+
|
|
2014
|
+
# Should show the Live status
|
|
2015
|
+
self.assertContains(
|
|
2016
|
+
response,
|
|
2017
|
+
'<h3 id="status-sidebar-live" class="w-label-1 !w-mt-0 w-mb-1"><span class="w-sr-only">Status: </span>Live</h3>',
|
|
2018
|
+
html=True,
|
|
2019
|
+
)
|
|
2020
|
+
# Should show the Draft status
|
|
2021
|
+
self.assertContains(
|
|
2022
|
+
response,
|
|
2023
|
+
'<h3 id="status-sidebar-draft" class="w-label-1 !w-mt-0 w-mb-1"><span class="w-sr-only">Status: </span>Draft</h3>',
|
|
2024
|
+
html=True,
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
# Should show the Unpublish action menu item
|
|
2028
|
+
unpublish_url = reverse(
|
|
2029
|
+
"wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish",
|
|
2030
|
+
args=(quote(self.test_snippet.pk),),
|
|
2031
|
+
)
|
|
2032
|
+
self.assertContains(
|
|
2033
|
+
response,
|
|
2034
|
+
f'<a class="button" href="{unpublish_url}">',
|
|
2035
|
+
)
|
|
2036
|
+
self.assertContains(response, "Unpublish")
|
|
2037
|
+
|
|
2038
|
+
soup = self.get_soup(response.content)
|
|
2039
|
+
h2 = soup.select_one("#header-title")
|
|
2040
|
+
self.assertIsNotNone(h2)
|
|
2041
|
+
icon = h2.select_one("svg use")
|
|
2042
|
+
self.assertIsNotNone(icon)
|
|
2043
|
+
self.assertEqual(icon["href"], "#icon-snippet")
|
|
2044
|
+
self.assertEqual(h2.text.strip(), "Draft-enabled Bar, In Draft")
|
|
2045
|
+
|
|
2046
|
+
# Should use the latest draft content for the form
|
|
2047
|
+
self.assertTagInHTML(
|
|
2048
|
+
'<textarea name="text">Draft-enabled Bar, In Draft</textarea>',
|
|
2049
|
+
html,
|
|
2050
|
+
allow_extra_attrs=True,
|
|
2051
|
+
)
|
|
2052
|
+
|
|
2053
|
+
def test_edit_post_scheduled(self):
|
|
2054
|
+
self.test_snippet.save_revision().publish()
|
|
2055
|
+
|
|
2056
|
+
# put go_live_at and expire_at several days away from the current date, to avoid
|
|
2057
|
+
# false matches in content__ tests
|
|
2058
|
+
go_live_at = now() + datetime.timedelta(days=10)
|
|
2059
|
+
expire_at = now() + datetime.timedelta(days=20)
|
|
2060
|
+
response = self.post(
|
|
2061
|
+
post_data={
|
|
2062
|
+
"text": "Some content",
|
|
2063
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2064
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2065
|
+
}
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
# Should be redirected to the edit page
|
|
2069
|
+
self.assertRedirects(
|
|
2070
|
+
response,
|
|
2071
|
+
reverse(
|
|
2072
|
+
"wagtailsnippets_tests_draftstatecustomprimarykeymodel:edit",
|
|
2073
|
+
args=[quote(self.test_snippet.pk)],
|
|
2074
|
+
),
|
|
2075
|
+
)
|
|
2076
|
+
|
|
2077
|
+
self.test_snippet.refresh_from_db()
|
|
2078
|
+
|
|
2079
|
+
# The object will still be live
|
|
2080
|
+
self.assertTrue(self.test_snippet.live)
|
|
2081
|
+
|
|
2082
|
+
# A revision with approved_go_live_at should not exist
|
|
2083
|
+
self.assertFalse(
|
|
2084
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2085
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2086
|
+
.exists()
|
|
2087
|
+
)
|
|
2088
|
+
|
|
2089
|
+
# But a revision with go_live_at and expire_at in their content json *should* exist
|
|
2090
|
+
self.assertTrue(
|
|
2091
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2092
|
+
.filter(
|
|
2093
|
+
content__go_live_at__startswith=str(go_live_at.date()),
|
|
2094
|
+
)
|
|
2095
|
+
.exists()
|
|
2096
|
+
)
|
|
2097
|
+
self.assertTrue(
|
|
2098
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2099
|
+
.filter(
|
|
2100
|
+
content__expire_at__startswith=str(expire_at.date()),
|
|
2101
|
+
)
|
|
2102
|
+
.exists()
|
|
2103
|
+
)
|
|
2104
|
+
|
|
2105
|
+
# Get the edit page again
|
|
2106
|
+
response = self.get()
|
|
2107
|
+
|
|
2108
|
+
# Should show the draft go_live_at and expire_at under the "Once scheduled" label
|
|
2109
|
+
self.assertContains(
|
|
2110
|
+
response,
|
|
2111
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2112
|
+
html=True,
|
|
2113
|
+
count=1,
|
|
2114
|
+
)
|
|
2115
|
+
self.assertContains(
|
|
2116
|
+
response,
|
|
2117
|
+
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
|
2118
|
+
html=True,
|
|
2119
|
+
count=1,
|
|
2120
|
+
)
|
|
2121
|
+
self.assertContains(
|
|
2122
|
+
response,
|
|
2123
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
|
2124
|
+
html=True,
|
|
2125
|
+
count=1,
|
|
2126
|
+
)
|
|
2127
|
+
self.assertSchedulingDialogRendered(response)
|
|
2128
|
+
|
|
2129
|
+
def test_edit_scheduled_go_live_before_expiry(self):
|
|
2130
|
+
response = self.post(
|
|
2131
|
+
post_data={
|
|
2132
|
+
"text": "Some content",
|
|
2133
|
+
"go_live_at": submittable_timestamp(now() + datetime.timedelta(days=2)),
|
|
2134
|
+
"expire_at": submittable_timestamp(now() + datetime.timedelta(days=1)),
|
|
2135
|
+
}
|
|
2136
|
+
)
|
|
2137
|
+
|
|
2138
|
+
self.assertEqual(response.status_code, 200)
|
|
2139
|
+
|
|
2140
|
+
# Check that a form error was raised
|
|
2141
|
+
self.assertFormError(
|
|
2142
|
+
response.context["form"],
|
|
2143
|
+
"go_live_at",
|
|
2144
|
+
"Go live date/time must be before expiry date/time",
|
|
2145
|
+
)
|
|
2146
|
+
self.assertFormError(
|
|
2147
|
+
response.context["form"],
|
|
2148
|
+
"expire_at",
|
|
2149
|
+
"Go live date/time must be before expiry date/time",
|
|
2150
|
+
)
|
|
2151
|
+
|
|
2152
|
+
self.assertContains(
|
|
2153
|
+
response,
|
|
2154
|
+
'<div class="w-label-3 w-text-primary">Invalid schedule</div>',
|
|
2155
|
+
html=True,
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
num_errors = 2
|
|
2159
|
+
|
|
2160
|
+
# Should show the correct number on the badge of the toggle button
|
|
2161
|
+
self.assertRegex(
|
|
2162
|
+
response.content.decode(),
|
|
2163
|
+
self.STATUS_TOGGLE_BADGE_REGEX % {"num_errors": num_errors},
|
|
2164
|
+
)
|
|
2165
|
+
|
|
2166
|
+
def test_edit_scheduled_expire_in_the_past(self):
|
|
2167
|
+
response = self.post(
|
|
2168
|
+
post_data={
|
|
2169
|
+
"text": "Some content",
|
|
2170
|
+
"expire_at": submittable_timestamp(now() + datetime.timedelta(days=-1)),
|
|
2171
|
+
}
|
|
2172
|
+
)
|
|
2173
|
+
|
|
2174
|
+
self.assertEqual(response.status_code, 200)
|
|
2175
|
+
|
|
2176
|
+
# Check that a form error was raised
|
|
2177
|
+
self.assertFormError(
|
|
2178
|
+
response.context["form"],
|
|
2179
|
+
"expire_at",
|
|
2180
|
+
"Expiry date/time must be in the future.",
|
|
2181
|
+
)
|
|
2182
|
+
|
|
2183
|
+
self.assertContains(
|
|
2184
|
+
response,
|
|
2185
|
+
'<div class="w-label-3 w-text-primary">Invalid schedule</div>',
|
|
2186
|
+
html=True,
|
|
2187
|
+
)
|
|
2188
|
+
|
|
2189
|
+
num_errors = 1
|
|
2190
|
+
|
|
2191
|
+
# Should show the correct number on the badge of the toggle button
|
|
2192
|
+
self.assertRegex(
|
|
2193
|
+
response.content.decode(),
|
|
2194
|
+
self.STATUS_TOGGLE_BADGE_REGEX % {"num_errors": num_errors},
|
|
2195
|
+
)
|
|
2196
|
+
|
|
2197
|
+
def test_edit_post_invalid_schedule_with_existing_draft_schedule(self):
|
|
2198
|
+
self.test_snippet.go_live_at = now() + datetime.timedelta(days=1)
|
|
2199
|
+
self.test_snippet.expire_at = now() + datetime.timedelta(days=2)
|
|
2200
|
+
latest_revision = self.test_snippet.save_revision()
|
|
2201
|
+
|
|
2202
|
+
go_live_at = now() + datetime.timedelta(days=10)
|
|
2203
|
+
expire_at = now() + datetime.timedelta(days=-20)
|
|
2204
|
+
response = self.post(
|
|
2205
|
+
post_data={
|
|
2206
|
+
"text": "Some edited content",
|
|
2207
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2208
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2209
|
+
}
|
|
2210
|
+
)
|
|
2211
|
+
|
|
2212
|
+
# Should render the edit page with errors instead of redirecting
|
|
2213
|
+
self.assertEqual(response.status_code, 200)
|
|
2214
|
+
|
|
2215
|
+
self.test_snippet.refresh_from_db()
|
|
2216
|
+
|
|
2217
|
+
# The snippet will not be live
|
|
2218
|
+
self.assertFalse(self.test_snippet.live)
|
|
2219
|
+
|
|
2220
|
+
# No new revision should have been created
|
|
2221
|
+
self.assertEqual(self.test_snippet.latest_revision_id, latest_revision.pk)
|
|
2222
|
+
|
|
2223
|
+
# Should not show the draft go_live_at and expire_at under the "Once scheduled" label
|
|
2224
|
+
self.assertNotContains(
|
|
2225
|
+
response,
|
|
2226
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2227
|
+
html=True,
|
|
2228
|
+
)
|
|
2229
|
+
self.assertNotContains(
|
|
2230
|
+
response,
|
|
2231
|
+
'<span class="w-text-grey-600">Go-live:</span>',
|
|
2232
|
+
html=True,
|
|
2233
|
+
)
|
|
2234
|
+
self.assertNotContains(
|
|
2235
|
+
response,
|
|
2236
|
+
'<span class="w-text-grey-600">Expiry:</span>',
|
|
2237
|
+
html=True,
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
# Should show the "Edit schedule" button
|
|
2241
|
+
html = response.content.decode()
|
|
2242
|
+
self.assertTagInHTML(
|
|
2243
|
+
'<button type="button" data-a11y-dialog-show="schedule-publishing-dialog">Edit schedule</button>',
|
|
2244
|
+
html,
|
|
2245
|
+
count=1,
|
|
2246
|
+
allow_extra_attrs=True,
|
|
2247
|
+
)
|
|
2248
|
+
|
|
2249
|
+
self.assertContains(
|
|
2250
|
+
response,
|
|
2251
|
+
'<div class="w-label-3 w-text-primary">Invalid schedule</div>',
|
|
2252
|
+
html=True,
|
|
2253
|
+
)
|
|
2254
|
+
|
|
2255
|
+
num_errors = 2
|
|
2256
|
+
|
|
2257
|
+
# Should show the correct number on the badge of the toggle button
|
|
2258
|
+
self.assertRegex(
|
|
2259
|
+
response.content.decode(),
|
|
2260
|
+
self.STATUS_TOGGLE_BADGE_REGEX % {"num_errors": num_errors},
|
|
2261
|
+
)
|
|
2262
|
+
|
|
2263
|
+
def test_first_published_at_editable(self):
|
|
2264
|
+
"""Test that we can update the first_published_at via the edit form,
|
|
2265
|
+
for models that expose it."""
|
|
2266
|
+
|
|
2267
|
+
self.test_snippet.save_revision().publish()
|
|
2268
|
+
self.test_snippet.refresh_from_db()
|
|
2269
|
+
|
|
2270
|
+
initial_delta = self.test_snippet.first_published_at - now()
|
|
2271
|
+
|
|
2272
|
+
first_published_at = now() - datetime.timedelta(days=2)
|
|
2273
|
+
|
|
2274
|
+
self.post(
|
|
2275
|
+
post_data={
|
|
2276
|
+
"text": "I've been edited!",
|
|
2277
|
+
"action-publish": "action-publish",
|
|
2278
|
+
"first_published_at": submittable_timestamp(first_published_at),
|
|
2279
|
+
}
|
|
2280
|
+
)
|
|
2281
|
+
|
|
2282
|
+
self.test_snippet.refresh_from_db()
|
|
2283
|
+
|
|
2284
|
+
# first_published_at should have changed.
|
|
2285
|
+
new_delta = self.test_snippet.first_published_at - now()
|
|
2286
|
+
self.assertNotEqual(new_delta.days, initial_delta.days)
|
|
2287
|
+
# first_published_at should be 3 days ago.
|
|
2288
|
+
self.assertEqual(new_delta.days, -3)
|
|
2289
|
+
|
|
2290
|
+
def test_edit_post_publish_scheduled_unpublished(self):
|
|
2291
|
+
go_live_at = now() + datetime.timedelta(days=1)
|
|
2292
|
+
expire_at = now() + datetime.timedelta(days=2)
|
|
2293
|
+
response = self.post(
|
|
2294
|
+
post_data={
|
|
2295
|
+
"text": "Some content",
|
|
2296
|
+
"action-publish": "Publish",
|
|
2297
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2298
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2299
|
+
}
|
|
2300
|
+
)
|
|
2301
|
+
|
|
2302
|
+
# Should be redirected to the listing page
|
|
2303
|
+
self.assertRedirects(
|
|
2304
|
+
response,
|
|
2305
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2306
|
+
)
|
|
2307
|
+
|
|
2308
|
+
self.test_snippet.refresh_from_db()
|
|
2309
|
+
|
|
2310
|
+
# The object should not be live
|
|
2311
|
+
self.assertFalse(self.test_snippet.live)
|
|
2312
|
+
|
|
2313
|
+
# Instead a revision with approved_go_live_at should now exist
|
|
2314
|
+
self.assertTrue(
|
|
2315
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2316
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2317
|
+
.exists()
|
|
2318
|
+
)
|
|
2319
|
+
|
|
2320
|
+
# The object SHOULD have the "has_unpublished_changes" flag set,
|
|
2321
|
+
# because the changes are not visible as a live object yet
|
|
2322
|
+
self.assertTrue(
|
|
2323
|
+
self.test_snippet.has_unpublished_changes,
|
|
2324
|
+
msg="An object scheduled for future publishing should have has_unpublished_changes=True",
|
|
2325
|
+
)
|
|
2326
|
+
|
|
2327
|
+
self.assertEqual(self.test_snippet.status_string, "scheduled")
|
|
2328
|
+
|
|
2329
|
+
response = self.get()
|
|
2330
|
+
|
|
2331
|
+
# Should show the go_live_at and expire_at without the "Once scheduled" label
|
|
2332
|
+
self.assertNotContains(
|
|
2333
|
+
response,
|
|
2334
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2335
|
+
html=True,
|
|
2336
|
+
)
|
|
2337
|
+
self.assertContains(
|
|
2338
|
+
response,
|
|
2339
|
+
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
|
2340
|
+
html=True,
|
|
2341
|
+
count=1,
|
|
2342
|
+
)
|
|
2343
|
+
self.assertContains(
|
|
2344
|
+
response,
|
|
2345
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
|
2346
|
+
html=True,
|
|
2347
|
+
count=1,
|
|
2348
|
+
)
|
|
2349
|
+
self.assertSchedulingDialogRendered(response)
|
|
2350
|
+
|
|
2351
|
+
def test_edit_post_publish_now_an_already_scheduled_unpublished(self):
|
|
2352
|
+
# First let's publish an object with a go_live_at in the future
|
|
2353
|
+
go_live_at = now() + datetime.timedelta(days=1)
|
|
2354
|
+
expire_at = now() + datetime.timedelta(days=2)
|
|
2355
|
+
response = self.post(
|
|
2356
|
+
post_data={
|
|
2357
|
+
"text": "Some content",
|
|
2358
|
+
"action-publish": "Publish",
|
|
2359
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2360
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2361
|
+
}
|
|
2362
|
+
)
|
|
2363
|
+
|
|
2364
|
+
# Should be redirected to the listing page
|
|
2365
|
+
self.assertRedirects(
|
|
2366
|
+
response,
|
|
2367
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2368
|
+
)
|
|
2369
|
+
|
|
2370
|
+
self.test_snippet.refresh_from_db()
|
|
2371
|
+
|
|
2372
|
+
# The object should not be live
|
|
2373
|
+
self.assertFalse(self.test_snippet.live)
|
|
2374
|
+
|
|
2375
|
+
self.assertEqual(self.test_snippet.status_string, "scheduled")
|
|
2376
|
+
|
|
2377
|
+
# Instead a revision with approved_go_live_at should now exist
|
|
2378
|
+
self.assertTrue(
|
|
2379
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2380
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2381
|
+
.exists()
|
|
2382
|
+
)
|
|
2383
|
+
|
|
2384
|
+
# Now, let's edit it and publish it right now
|
|
2385
|
+
response = self.post(
|
|
2386
|
+
post_data={
|
|
2387
|
+
"text": "Some content",
|
|
2388
|
+
"action-publish": "Publish",
|
|
2389
|
+
"go_live_at": "",
|
|
2390
|
+
}
|
|
2391
|
+
)
|
|
2392
|
+
|
|
2393
|
+
# Should be redirected to the listing page
|
|
2394
|
+
self.assertRedirects(
|
|
2395
|
+
response,
|
|
2396
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2397
|
+
)
|
|
2398
|
+
|
|
2399
|
+
self.test_snippet.refresh_from_db()
|
|
2400
|
+
|
|
2401
|
+
# The object should be live
|
|
2402
|
+
self.assertTrue(self.test_snippet.live)
|
|
2403
|
+
|
|
2404
|
+
# The revision with approved_go_live_at should no longer exist
|
|
2405
|
+
self.assertFalse(
|
|
2406
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2407
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2408
|
+
.exists()
|
|
2409
|
+
)
|
|
2410
|
+
|
|
2411
|
+
response = self.get()
|
|
2412
|
+
self.assertSchedulingDialogRendered(response)
|
|
2413
|
+
|
|
2414
|
+
def test_edit_post_publish_scheduled_published(self):
|
|
2415
|
+
self.test_snippet.save_revision().publish()
|
|
2416
|
+
self.test_snippet.refresh_from_db()
|
|
2417
|
+
|
|
2418
|
+
live_revision = self.test_snippet.live_revision
|
|
2419
|
+
|
|
2420
|
+
go_live_at = now() + datetime.timedelta(days=1)
|
|
2421
|
+
expire_at = now() + datetime.timedelta(days=2)
|
|
2422
|
+
response = self.post(
|
|
2423
|
+
post_data={
|
|
2424
|
+
"text": "I've been edited!",
|
|
2425
|
+
"action-publish": "Publish",
|
|
2426
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2427
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2428
|
+
}
|
|
2429
|
+
)
|
|
2430
|
+
|
|
2431
|
+
# Should be redirected to the listing page
|
|
2432
|
+
self.assertRedirects(
|
|
2433
|
+
response,
|
|
2434
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2435
|
+
)
|
|
2436
|
+
|
|
2437
|
+
self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.get(
|
|
2438
|
+
pk=self.test_snippet.pk
|
|
2439
|
+
)
|
|
2440
|
+
|
|
2441
|
+
# The object should still be live
|
|
2442
|
+
self.assertTrue(self.test_snippet.live)
|
|
2443
|
+
|
|
2444
|
+
self.assertEqual(self.test_snippet.status_string, "live + scheduled")
|
|
2445
|
+
|
|
2446
|
+
# A revision with approved_go_live_at should now exist
|
|
2447
|
+
self.assertTrue(
|
|
2448
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2449
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2450
|
+
.exists()
|
|
2451
|
+
)
|
|
2452
|
+
|
|
2453
|
+
# The object SHOULD have the "has_unpublished_changes" flag set,
|
|
2454
|
+
# because the changes are not visible as a live object yet
|
|
2455
|
+
self.assertTrue(
|
|
2456
|
+
self.test_snippet.has_unpublished_changes,
|
|
2457
|
+
msg="An object scheduled for future publishing should have has_unpublished_changes=True",
|
|
2458
|
+
)
|
|
2459
|
+
|
|
2460
|
+
self.assertNotEqual(
|
|
2461
|
+
self.test_snippet.get_latest_revision(),
|
|
2462
|
+
live_revision,
|
|
2463
|
+
"An object scheduled for future publishing should have a new revision, that is not the live revision",
|
|
2464
|
+
)
|
|
2465
|
+
|
|
2466
|
+
self.assertEqual(
|
|
2467
|
+
self.test_snippet.text,
|
|
2468
|
+
"Draft-enabled Foo",
|
|
2469
|
+
"A live object with a scheduled revision should still have the original content",
|
|
2470
|
+
)
|
|
2471
|
+
|
|
2472
|
+
response = self.get()
|
|
2473
|
+
|
|
2474
|
+
# Should show the go_live_at and expire_at without the "Once scheduled" label
|
|
2475
|
+
self.assertNotContains(
|
|
2476
|
+
response,
|
|
2477
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2478
|
+
html=True,
|
|
2479
|
+
)
|
|
2480
|
+
self.assertContains(
|
|
2481
|
+
response,
|
|
2482
|
+
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
|
2483
|
+
html=True,
|
|
2484
|
+
count=1,
|
|
2485
|
+
)
|
|
2486
|
+
self.assertContains(
|
|
2487
|
+
response,
|
|
2488
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
|
2489
|
+
html=True,
|
|
2490
|
+
count=1,
|
|
2491
|
+
)
|
|
2492
|
+
self.assertSchedulingDialogRendered(response)
|
|
2493
|
+
|
|
2494
|
+
def test_edit_post_publish_now_an_already_scheduled_published(self):
|
|
2495
|
+
self.test_snippet.save_revision().publish()
|
|
2496
|
+
|
|
2497
|
+
# First let's publish an object with a go_live_at in the future
|
|
2498
|
+
go_live_at = now() + datetime.timedelta(days=1)
|
|
2499
|
+
expire_at = now() + datetime.timedelta(days=2)
|
|
2500
|
+
response = self.post(
|
|
2501
|
+
post_data={
|
|
2502
|
+
"text": "Some content",
|
|
2503
|
+
"action-publish": "Publish",
|
|
2504
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2505
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2506
|
+
}
|
|
2507
|
+
)
|
|
2508
|
+
|
|
2509
|
+
# Should be redirected to the listing page
|
|
2510
|
+
self.assertRedirects(
|
|
2511
|
+
response,
|
|
2512
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2513
|
+
)
|
|
2514
|
+
|
|
2515
|
+
self.test_snippet.refresh_from_db()
|
|
2516
|
+
|
|
2517
|
+
# The object should still be live
|
|
2518
|
+
self.assertTrue(self.test_snippet.live)
|
|
2519
|
+
|
|
2520
|
+
# A revision with approved_go_live_at should now exist
|
|
2521
|
+
self.assertTrue(
|
|
2522
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2523
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2524
|
+
.exists()
|
|
2525
|
+
)
|
|
2526
|
+
|
|
2527
|
+
self.assertEqual(
|
|
2528
|
+
self.test_snippet.text,
|
|
2529
|
+
"Draft-enabled Foo",
|
|
2530
|
+
"A live object with scheduled revisions should still have original content",
|
|
2531
|
+
)
|
|
2532
|
+
|
|
2533
|
+
# Now, let's edit it and publish it right now
|
|
2534
|
+
response = self.post(
|
|
2535
|
+
post_data={
|
|
2536
|
+
"text": "I've been updated!",
|
|
2537
|
+
"action-publish": "Publish",
|
|
2538
|
+
"go_live_at": "",
|
|
2539
|
+
}
|
|
2540
|
+
)
|
|
2541
|
+
|
|
2542
|
+
# Should be redirected to the listing page
|
|
2543
|
+
self.assertRedirects(
|
|
2544
|
+
response,
|
|
2545
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2546
|
+
)
|
|
2547
|
+
|
|
2548
|
+
self.test_snippet.refresh_from_db()
|
|
2549
|
+
|
|
2550
|
+
# The object should be live
|
|
2551
|
+
self.assertTrue(self.test_snippet.live)
|
|
2552
|
+
|
|
2553
|
+
# The scheduled revision should no longer exist
|
|
2554
|
+
self.assertFalse(
|
|
2555
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2556
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2557
|
+
.exists()
|
|
2558
|
+
)
|
|
2559
|
+
|
|
2560
|
+
# The content should be updated
|
|
2561
|
+
self.assertEqual(self.test_snippet.text, "I've been updated!")
|
|
2562
|
+
|
|
2563
|
+
def test_edit_post_save_schedule_before_a_scheduled_expire(self):
|
|
2564
|
+
# First let's publish an object with *just* an expire_at in the future
|
|
2565
|
+
expire_at = now() + datetime.timedelta(days=20)
|
|
2566
|
+
response = self.post(
|
|
2567
|
+
post_data={
|
|
2568
|
+
"text": "Some content",
|
|
2569
|
+
"action-publish": "Publish",
|
|
2570
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2571
|
+
}
|
|
2572
|
+
)
|
|
2573
|
+
|
|
2574
|
+
# Should be redirected to the listing page
|
|
2575
|
+
self.assertRedirects(
|
|
2576
|
+
response,
|
|
2577
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2578
|
+
)
|
|
2579
|
+
|
|
2580
|
+
self.test_snippet.refresh_from_db()
|
|
2581
|
+
|
|
2582
|
+
# The object should still be live
|
|
2583
|
+
self.assertTrue(self.test_snippet.live)
|
|
2584
|
+
|
|
2585
|
+
self.assertEqual(self.test_snippet.status_string, "live")
|
|
2586
|
+
|
|
2587
|
+
# The live object should have the expire_at field set
|
|
2588
|
+
self.assertEqual(
|
|
2589
|
+
self.test_snippet.expire_at,
|
|
2590
|
+
expire_at.replace(second=0, microsecond=0),
|
|
2591
|
+
)
|
|
2592
|
+
|
|
2593
|
+
# Now, let's save an object with a go_live_at in the future,
|
|
2594
|
+
# but before the existing expire_at
|
|
2595
|
+
go_live_at = now() + datetime.timedelta(days=10)
|
|
2596
|
+
new_expire_at = now() + datetime.timedelta(days=15)
|
|
2597
|
+
response = self.post(
|
|
2598
|
+
post_data={
|
|
2599
|
+
"text": "Some content",
|
|
2600
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2601
|
+
"expire_at": submittable_timestamp(new_expire_at),
|
|
2602
|
+
}
|
|
2603
|
+
)
|
|
2604
|
+
|
|
2605
|
+
# Should be redirected to the edit page
|
|
2606
|
+
self.assertRedirects(
|
|
2607
|
+
response,
|
|
2608
|
+
reverse(
|
|
2609
|
+
"wagtailsnippets_tests_draftstatecustomprimarykeymodel:edit",
|
|
2610
|
+
args=[quote(self.test_snippet.pk)],
|
|
2611
|
+
),
|
|
2612
|
+
)
|
|
2613
|
+
|
|
2614
|
+
self.test_snippet.refresh_from_db()
|
|
2615
|
+
|
|
2616
|
+
# The object will still be live
|
|
2617
|
+
self.assertTrue(self.test_snippet.live)
|
|
2618
|
+
|
|
2619
|
+
# A revision with approved_go_live_at should not exist
|
|
2620
|
+
self.assertFalse(
|
|
2621
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2622
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2623
|
+
.exists()
|
|
2624
|
+
)
|
|
2625
|
+
|
|
2626
|
+
# But a revision with go_live_at and expire_at in their content json *should* exist
|
|
2627
|
+
self.assertTrue(
|
|
2628
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2629
|
+
.filter(content__go_live_at__startswith=str(go_live_at.date()))
|
|
2630
|
+
.exists()
|
|
2631
|
+
)
|
|
2632
|
+
self.assertTrue(
|
|
2633
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2634
|
+
.filter(content__expire_at__startswith=str(expire_at.date()))
|
|
2635
|
+
.exists()
|
|
2636
|
+
)
|
|
2637
|
+
|
|
2638
|
+
response = self.get()
|
|
2639
|
+
|
|
2640
|
+
# Should still show the active expire_at in the live object
|
|
2641
|
+
self.assertContains(
|
|
2642
|
+
response,
|
|
2643
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
|
2644
|
+
html=True,
|
|
2645
|
+
count=1,
|
|
2646
|
+
)
|
|
2647
|
+
|
|
2648
|
+
# Should also show the draft go_live_at and expire_at under the "Once scheduled" label
|
|
2649
|
+
self.assertContains(
|
|
2650
|
+
response,
|
|
2651
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2652
|
+
html=True,
|
|
2653
|
+
count=1,
|
|
2654
|
+
)
|
|
2655
|
+
self.assertContains(
|
|
2656
|
+
response,
|
|
2657
|
+
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
|
2658
|
+
html=True,
|
|
2659
|
+
count=1,
|
|
2660
|
+
)
|
|
2661
|
+
self.assertContains(
|
|
2662
|
+
response,
|
|
2663
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
|
2664
|
+
html=True,
|
|
2665
|
+
count=1,
|
|
2666
|
+
)
|
|
2667
|
+
self.assertSchedulingDialogRendered(response)
|
|
2668
|
+
|
|
2669
|
+
def test_edit_post_publish_schedule_before_a_scheduled_expire(self):
|
|
2670
|
+
# First let's publish an object with *just* an expire_at in the future
|
|
2671
|
+
expire_at = now() + datetime.timedelta(days=20)
|
|
2672
|
+
response = self.post(
|
|
2673
|
+
post_data={
|
|
2674
|
+
"text": "Some content",
|
|
2675
|
+
"action-publish": "Publish",
|
|
2676
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2677
|
+
}
|
|
2678
|
+
)
|
|
2679
|
+
|
|
2680
|
+
# Should be redirected to the listing page
|
|
2681
|
+
self.assertRedirects(
|
|
2682
|
+
response,
|
|
2683
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2684
|
+
)
|
|
2685
|
+
|
|
2686
|
+
self.test_snippet.refresh_from_db()
|
|
2687
|
+
|
|
2688
|
+
# The object should still be live
|
|
2689
|
+
self.assertTrue(self.test_snippet.live)
|
|
2690
|
+
|
|
2691
|
+
self.assertEqual(self.test_snippet.status_string, "live")
|
|
2692
|
+
|
|
2693
|
+
# The live object should have the expire_at field set
|
|
2694
|
+
self.assertEqual(
|
|
2695
|
+
self.test_snippet.expire_at,
|
|
2696
|
+
expire_at.replace(second=0, microsecond=0),
|
|
2697
|
+
)
|
|
2698
|
+
|
|
2699
|
+
# Now, let's publish an object with a go_live_at in the future,
|
|
2700
|
+
# but before the existing expire_at
|
|
2701
|
+
go_live_at = now() + datetime.timedelta(days=10)
|
|
2702
|
+
new_expire_at = now() + datetime.timedelta(days=15)
|
|
2703
|
+
response = self.post(
|
|
2704
|
+
post_data={
|
|
2705
|
+
"text": "Some content",
|
|
2706
|
+
"action-publish": "Publish",
|
|
2707
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2708
|
+
"expire_at": submittable_timestamp(new_expire_at),
|
|
2709
|
+
}
|
|
2710
|
+
)
|
|
2711
|
+
|
|
2712
|
+
# Should be redirected to the listing page
|
|
2713
|
+
self.assertRedirects(
|
|
2714
|
+
response,
|
|
2715
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2716
|
+
)
|
|
2717
|
+
|
|
2718
|
+
self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.get(
|
|
2719
|
+
pk=self.test_snippet.pk
|
|
2720
|
+
)
|
|
2721
|
+
|
|
2722
|
+
# The object should still be live
|
|
2723
|
+
self.assertTrue(self.test_snippet.live)
|
|
2724
|
+
|
|
2725
|
+
self.assertEqual(self.test_snippet.status_string, "live + scheduled")
|
|
2726
|
+
|
|
2727
|
+
# A revision with approved_go_live_at should now exist
|
|
2728
|
+
self.assertTrue(
|
|
2729
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2730
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2731
|
+
.exists()
|
|
2732
|
+
)
|
|
2733
|
+
|
|
2734
|
+
response = self.get()
|
|
2735
|
+
|
|
2736
|
+
# Should not show the active expire_at in the live object because the
|
|
2737
|
+
# scheduled revision is before the existing expire_at, which means it will
|
|
2738
|
+
# override the existing expire_at when it goes live
|
|
2739
|
+
self.assertNotContains(
|
|
2740
|
+
response,
|
|
2741
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
|
2742
|
+
html=True,
|
|
2743
|
+
)
|
|
2744
|
+
|
|
2745
|
+
# Should show the go_live_at and expire_at without the "Once scheduled" label
|
|
2746
|
+
self.assertNotContains(
|
|
2747
|
+
response,
|
|
2748
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2749
|
+
html=True,
|
|
2750
|
+
)
|
|
2751
|
+
self.assertContains(
|
|
2752
|
+
response,
|
|
2753
|
+
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
|
2754
|
+
html=True,
|
|
2755
|
+
count=1,
|
|
2756
|
+
)
|
|
2757
|
+
self.assertContains(
|
|
2758
|
+
response,
|
|
2759
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
|
2760
|
+
html=True,
|
|
2761
|
+
count=1,
|
|
2762
|
+
)
|
|
2763
|
+
self.assertSchedulingDialogRendered(response)
|
|
2764
|
+
|
|
2765
|
+
def test_edit_post_publish_schedule_after_a_scheduled_expire(self):
|
|
2766
|
+
# First let's publish an object with *just* an expire_at in the future
|
|
2767
|
+
expire_at = now() + datetime.timedelta(days=20)
|
|
2768
|
+
response = self.post(
|
|
2769
|
+
post_data={
|
|
2770
|
+
"text": "Some content",
|
|
2771
|
+
"action-publish": "Publish",
|
|
2772
|
+
"expire_at": submittable_timestamp(expire_at),
|
|
2773
|
+
}
|
|
2774
|
+
)
|
|
2775
|
+
|
|
2776
|
+
# Should be redirected to the listing page
|
|
2777
|
+
self.assertRedirects(
|
|
2778
|
+
response,
|
|
2779
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2780
|
+
)
|
|
2781
|
+
|
|
2782
|
+
self.test_snippet.refresh_from_db()
|
|
2783
|
+
|
|
2784
|
+
# The object should still be live
|
|
2785
|
+
self.assertTrue(self.test_snippet.live)
|
|
2786
|
+
|
|
2787
|
+
self.assertEqual(self.test_snippet.status_string, "live")
|
|
2788
|
+
|
|
2789
|
+
# The live object should have the expire_at field set
|
|
2790
|
+
self.assertEqual(
|
|
2791
|
+
self.test_snippet.expire_at,
|
|
2792
|
+
expire_at.replace(second=0, microsecond=0),
|
|
2793
|
+
)
|
|
2794
|
+
|
|
2795
|
+
# Now, let's publish an object with a go_live_at in the future,
|
|
2796
|
+
# but after the existing expire_at
|
|
2797
|
+
go_live_at = now() + datetime.timedelta(days=23)
|
|
2798
|
+
new_expire_at = now() + datetime.timedelta(days=25)
|
|
2799
|
+
response = self.post(
|
|
2800
|
+
post_data={
|
|
2801
|
+
"text": "Some content",
|
|
2802
|
+
"action-publish": "Publish",
|
|
2803
|
+
"go_live_at": submittable_timestamp(go_live_at),
|
|
2804
|
+
"expire_at": submittable_timestamp(new_expire_at),
|
|
2805
|
+
}
|
|
2806
|
+
)
|
|
2807
|
+
|
|
2808
|
+
# Should be redirected to the listing page
|
|
2809
|
+
self.assertRedirects(
|
|
2810
|
+
response,
|
|
2811
|
+
reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"),
|
|
2812
|
+
)
|
|
2813
|
+
|
|
2814
|
+
self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.get(
|
|
2815
|
+
pk=self.test_snippet.pk
|
|
2816
|
+
)
|
|
2817
|
+
|
|
2818
|
+
# The object should still be live
|
|
2819
|
+
self.assertTrue(self.test_snippet.live)
|
|
2820
|
+
|
|
2821
|
+
self.assertEqual(self.test_snippet.status_string, "live + scheduled")
|
|
2822
|
+
|
|
2823
|
+
# Instead a revision with approved_go_live_at should now exist
|
|
2824
|
+
self.assertTrue(
|
|
2825
|
+
Revision.objects.for_instance(self.test_snippet)
|
|
2826
|
+
.exclude(approved_go_live_at__isnull=True)
|
|
2827
|
+
.exists()
|
|
2828
|
+
)
|
|
2829
|
+
|
|
2830
|
+
response = self.get()
|
|
2831
|
+
|
|
2832
|
+
# Should still show the active expire_at in the live object because the
|
|
2833
|
+
# scheduled revision is after the existing expire_at, which means the
|
|
2834
|
+
# new expire_at won't take effect until the revision goes live.
|
|
2835
|
+
# This means the object will be:
|
|
2836
|
+
# unpublished (expired) -> published (scheduled) -> unpublished (expired again)
|
|
2837
|
+
self.assertContains(
|
|
2838
|
+
response,
|
|
2839
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
|
2840
|
+
html=True,
|
|
2841
|
+
count=1,
|
|
2842
|
+
)
|
|
2843
|
+
|
|
2844
|
+
# Should show the go_live_at and expire_at without the "Once scheduled" label
|
|
2845
|
+
self.assertNotContains(
|
|
2846
|
+
response,
|
|
2847
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2848
|
+
html=True,
|
|
2849
|
+
)
|
|
2850
|
+
self.assertContains(
|
|
2851
|
+
response,
|
|
2852
|
+
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
|
2853
|
+
html=True,
|
|
2854
|
+
count=1,
|
|
2855
|
+
)
|
|
2856
|
+
self.assertContains(
|
|
2857
|
+
response,
|
|
2858
|
+
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
|
2859
|
+
html=True,
|
|
2860
|
+
count=1,
|
|
2861
|
+
)
|
|
2862
|
+
self.assertSchedulingDialogRendered(response)
|
|
2863
|
+
|
|
2864
|
+
def test_use_fallback_for_blank_string_representation(self):
|
|
2865
|
+
self.snippet = DraftStateModel.objects.create(text="", live=False)
|
|
2866
|
+
|
|
2867
|
+
response = self.client.get(
|
|
2868
|
+
reverse(
|
|
2869
|
+
"wagtailsnippets_tests_draftstatemodel:edit",
|
|
2870
|
+
args=[quote(self.snippet.pk)],
|
|
2871
|
+
),
|
|
2872
|
+
)
|
|
2873
|
+
|
|
2874
|
+
title = f"DraftStateModel object ({self.snippet.pk})"
|
|
2875
|
+
|
|
2876
|
+
soup = self.get_soup(response.content)
|
|
2877
|
+
h2 = soup.select_one("#header-title")
|
|
2878
|
+
self.assertEqual(h2.text.strip(), title)
|
|
2879
|
+
|
|
2880
|
+
sublabel = soup.select_one(".w-breadcrumbs li:last-of-type")
|
|
2881
|
+
self.assertEqual(sublabel.get_text(strip=True), title)
|
|
2882
|
+
|
|
2883
|
+
|
|
2884
|
+
class TestScheduledForPublishLock(BaseTestSnippetEditView):
|
|
2885
|
+
def setUp(self):
|
|
2886
|
+
super().setUp()
|
|
2887
|
+
self.test_snippet = DraftStateModel.objects.create(
|
|
2888
|
+
text="Draft-enabled Foo", live=False
|
|
2889
|
+
)
|
|
2890
|
+
self.go_live_at = now() + datetime.timedelta(days=1)
|
|
2891
|
+
self.test_snippet.text = "I've been edited!"
|
|
2892
|
+
self.test_snippet.go_live_at = self.go_live_at
|
|
2893
|
+
self.latest_revision = self.test_snippet.save_revision()
|
|
2894
|
+
self.latest_revision.publish()
|
|
2895
|
+
self.test_snippet.refresh_from_db()
|
|
2896
|
+
|
|
2897
|
+
def test_edit_get_scheduled_for_publishing_with_publish_permission(self):
|
|
2898
|
+
self.user.is_superuser = False
|
|
2899
|
+
|
|
2900
|
+
edit_permission = Permission.objects.get(
|
|
2901
|
+
content_type__app_label="tests", codename="change_draftstatemodel"
|
|
2902
|
+
)
|
|
2903
|
+
publish_permission = Permission.objects.get(
|
|
2904
|
+
content_type__app_label="tests", codename="publish_draftstatemodel"
|
|
2905
|
+
)
|
|
2906
|
+
admin_permission = Permission.objects.get(
|
|
2907
|
+
content_type__app_label="wagtailadmin", codename="access_admin"
|
|
2908
|
+
)
|
|
2909
|
+
|
|
2910
|
+
self.user.user_permissions.add(
|
|
2911
|
+
edit_permission,
|
|
2912
|
+
publish_permission,
|
|
2913
|
+
admin_permission,
|
|
2914
|
+
)
|
|
2915
|
+
self.user.save()
|
|
2916
|
+
|
|
2917
|
+
response = self.get()
|
|
2918
|
+
|
|
2919
|
+
# Should show the go_live_at without the "Once scheduled" label
|
|
2920
|
+
self.assertNotContains(
|
|
2921
|
+
response,
|
|
2922
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2923
|
+
html=True,
|
|
2924
|
+
)
|
|
2925
|
+
|
|
2926
|
+
self.assertContains(
|
|
2927
|
+
response,
|
|
2928
|
+
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(self.go_live_at)}',
|
|
2929
|
+
html=True,
|
|
2930
|
+
count=1,
|
|
2931
|
+
)
|
|
2932
|
+
|
|
2933
|
+
# Should show the lock message
|
|
2934
|
+
self.assertContains(
|
|
2935
|
+
response,
|
|
2936
|
+
"Draft state model 'I've been edited!' is locked and has been scheduled to go live at",
|
|
2937
|
+
count=1,
|
|
2938
|
+
)
|
|
2939
|
+
|
|
2940
|
+
# Should show the lock information in the status side panel
|
|
2941
|
+
self.assertContains(response, "Locked by schedule")
|
|
2942
|
+
self.assertContains(
|
|
2943
|
+
response,
|
|
2944
|
+
'<div class="w-help-text">Currently locked and will go live on the scheduled date</div>',
|
|
2945
|
+
html=True,
|
|
2946
|
+
count=1,
|
|
2947
|
+
)
|
|
2948
|
+
|
|
2949
|
+
html = response.content.decode()
|
|
2950
|
+
|
|
2951
|
+
# Should not show the "Edit schedule" button
|
|
2952
|
+
self.assertTagInHTML(
|
|
2953
|
+
'<button type="button" data-a11y-dialog-show="schedule-publishing-dialog">Edit schedule</button>',
|
|
2954
|
+
html,
|
|
2955
|
+
count=0,
|
|
2956
|
+
allow_extra_attrs=True,
|
|
2957
|
+
)
|
|
2958
|
+
|
|
2959
|
+
# Should show button to cancel scheduled publishing
|
|
2960
|
+
unschedule_url = reverse(
|
|
2961
|
+
"wagtailsnippets_tests_draftstatemodel:revisions_unschedule",
|
|
2962
|
+
args=[self.test_snippet.pk, self.latest_revision.pk],
|
|
2963
|
+
)
|
|
2964
|
+
self.assertTagInHTML(
|
|
2965
|
+
f'<button data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{unschedule_url}">Cancel scheduled publish</button>',
|
|
2966
|
+
html,
|
|
2967
|
+
count=1,
|
|
2968
|
+
allow_extra_attrs=True,
|
|
2969
|
+
)
|
|
2970
|
+
|
|
2971
|
+
def test_edit_get_scheduled_for_publishing_without_publish_permission(self):
|
|
2972
|
+
self.user.is_superuser = False
|
|
2973
|
+
|
|
2974
|
+
edit_permission = Permission.objects.get(
|
|
2975
|
+
content_type__app_label="tests", codename="change_draftstatemodel"
|
|
2976
|
+
)
|
|
2977
|
+
admin_permission = Permission.objects.get(
|
|
2978
|
+
content_type__app_label="wagtailadmin", codename="access_admin"
|
|
2979
|
+
)
|
|
2980
|
+
|
|
2981
|
+
self.user.user_permissions.add(edit_permission, admin_permission)
|
|
2982
|
+
self.user.save()
|
|
2983
|
+
|
|
2984
|
+
response = self.get()
|
|
2985
|
+
|
|
2986
|
+
# Should show the go_live_at without the "Once scheduled" label
|
|
2987
|
+
self.assertNotContains(
|
|
2988
|
+
response,
|
|
2989
|
+
'<div class="w-label-3 w-text-primary">Once scheduled:</div>',
|
|
2990
|
+
html=True,
|
|
2991
|
+
)
|
|
2992
|
+
|
|
2993
|
+
self.assertContains(
|
|
2994
|
+
response,
|
|
2995
|
+
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(self.go_live_at)}',
|
|
2996
|
+
html=True,
|
|
2997
|
+
count=1,
|
|
2998
|
+
)
|
|
2999
|
+
|
|
3000
|
+
# Should show the lock message
|
|
3001
|
+
self.assertContains(
|
|
3002
|
+
response,
|
|
3003
|
+
"Draft state model 'I've been edited!' is locked and has been scheduled to go live at",
|
|
3004
|
+
count=1,
|
|
3005
|
+
)
|
|
3006
|
+
|
|
3007
|
+
# Should show the lock information in the status side panel
|
|
3008
|
+
self.assertContains(response, "Locked by schedule")
|
|
3009
|
+
self.assertContains(
|
|
3010
|
+
response,
|
|
3011
|
+
'<div class="w-help-text">Currently locked and will go live on the scheduled date</div>',
|
|
3012
|
+
html=True,
|
|
3013
|
+
count=1,
|
|
3014
|
+
)
|
|
3015
|
+
|
|
3016
|
+
html = response.content.decode()
|
|
3017
|
+
|
|
3018
|
+
# Should not show the "Edit schedule" button
|
|
3019
|
+
self.assertTagInHTML(
|
|
3020
|
+
'<button type="button" data-a11y-dialog-show="schedule-publishing-dialog">Edit schedule</button>',
|
|
3021
|
+
html,
|
|
3022
|
+
count=0,
|
|
3023
|
+
allow_extra_attrs=True,
|
|
3024
|
+
)
|
|
3025
|
+
|
|
3026
|
+
# Should not show button to cancel scheduled publishing
|
|
3027
|
+
unschedule_url = reverse(
|
|
3028
|
+
"wagtailsnippets_tests_draftstatemodel:revisions_unschedule",
|
|
3029
|
+
args=[self.test_snippet.pk, self.latest_revision.pk],
|
|
3030
|
+
)
|
|
3031
|
+
self.assertTagInHTML(
|
|
3032
|
+
f'<button data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{unschedule_url}">Cancel scheduled publish</button>',
|
|
3033
|
+
html,
|
|
3034
|
+
count=0,
|
|
3035
|
+
allow_extra_attrs=True,
|
|
3036
|
+
)
|
|
3037
|
+
|
|
3038
|
+
def test_edit_post_scheduled_for_publishing(self):
|
|
3039
|
+
response = self.post(
|
|
3040
|
+
post_data={
|
|
3041
|
+
"text": "I'm edited while it's locked for scheduled publishing!",
|
|
3042
|
+
"go_live_at": submittable_timestamp(self.go_live_at),
|
|
3043
|
+
}
|
|
3044
|
+
)
|
|
3045
|
+
|
|
3046
|
+
self.test_snippet.refresh_from_db()
|
|
3047
|
+
|
|
3048
|
+
# Should not create a new revision,
|
|
3049
|
+
# so the latest revision's content should still be the same
|
|
3050
|
+
self.assertEqual(self.test_snippet.latest_revision, self.latest_revision)
|
|
3051
|
+
self.assertEqual(
|
|
3052
|
+
self.test_snippet.latest_revision.content["text"],
|
|
3053
|
+
"I've been edited!",
|
|
3054
|
+
)
|
|
3055
|
+
|
|
3056
|
+
# Should show a message explaining why the changes were not saved
|
|
3057
|
+
self.assertContains(
|
|
3058
|
+
response,
|
|
3059
|
+
"The draft state model could not be saved as it is locked",
|
|
3060
|
+
count=1,
|
|
3061
|
+
)
|
|
3062
|
+
|
|
3063
|
+
# Should not show the lock message, as we already have the error message
|
|
3064
|
+
self.assertNotContains(
|
|
3065
|
+
response,
|
|
3066
|
+
"Draft state model 'I've been edited!' is locked and has been scheduled to go live at",
|
|
3067
|
+
)
|
|
3068
|
+
|
|
3069
|
+
# Should show the lock information in the status side panel
|
|
3070
|
+
self.assertContains(response, "Locked by schedule")
|
|
3071
|
+
self.assertContains(
|
|
3072
|
+
response,
|
|
3073
|
+
'<div class="w-help-text">Currently locked and will go live on the scheduled date</div>',
|
|
3074
|
+
html=True,
|
|
3075
|
+
count=1,
|
|
3076
|
+
)
|
|
3077
|
+
|
|
3078
|
+
html = response.content.decode()
|
|
3079
|
+
|
|
3080
|
+
# Should not show the "Edit schedule" button
|
|
3081
|
+
self.assertTagInHTML(
|
|
3082
|
+
'<button type="button" data-a11y-dialog-show="schedule-publishing-dialog">Edit schedule</button>',
|
|
3083
|
+
html,
|
|
3084
|
+
count=0,
|
|
3085
|
+
allow_extra_attrs=True,
|
|
3086
|
+
)
|
|
3087
|
+
|
|
3088
|
+
# Should not show button to cancel scheduled publishing as the lock message isn't shown
|
|
3089
|
+
unschedule_url = reverse(
|
|
3090
|
+
"wagtailsnippets_tests_draftstatemodel:revisions_unschedule",
|
|
3091
|
+
args=[self.test_snippet.pk, self.latest_revision.pk],
|
|
3092
|
+
)
|
|
3093
|
+
self.assertTagInHTML(
|
|
3094
|
+
f'<button data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{unschedule_url}">Cancel scheduled publish</button>',
|
|
3095
|
+
html,
|
|
3096
|
+
count=0,
|
|
3097
|
+
allow_extra_attrs=True,
|
|
3098
|
+
)
|
|
3099
|
+
|
|
3100
|
+
|
|
3101
|
+
class TestSnippetViewWithCustomPrimaryKey(WagtailTestUtils, TestCase):
|
|
3102
|
+
fixtures = ["test.json"]
|
|
3103
|
+
|
|
3104
|
+
def setUp(self):
|
|
3105
|
+
super().setUp()
|
|
3106
|
+
self.login()
|
|
3107
|
+
self.snippet_a = StandardSnippetWithCustomPrimaryKey.objects.create(
|
|
3108
|
+
snippet_id="snippet/01", text="Hello"
|
|
3109
|
+
)
|
|
3110
|
+
self.snippet_b = StandardSnippetWithCustomPrimaryKey.objects.create(
|
|
3111
|
+
snippet_id="abc_407269_1", text="Goodbye"
|
|
3112
|
+
)
|
|
3113
|
+
|
|
3114
|
+
def get(self, snippet, params=None):
|
|
3115
|
+
args = [quote(snippet.pk)]
|
|
3116
|
+
return self.client.get(
|
|
3117
|
+
reverse(snippet.snippet_viewset.get_url_name("edit"), args=args),
|
|
3118
|
+
params,
|
|
3119
|
+
)
|
|
3120
|
+
|
|
3121
|
+
def post(self, snippet, post_data=None):
|
|
3122
|
+
args = [quote(snippet.pk)]
|
|
3123
|
+
return self.client.post(
|
|
3124
|
+
reverse(snippet.snippet_viewset.get_url_name("edit"), args=args),
|
|
3125
|
+
post_data,
|
|
3126
|
+
)
|
|
3127
|
+
|
|
3128
|
+
def create(self, snippet, post_data=None, model=Advert):
|
|
3129
|
+
return self.client.post(
|
|
3130
|
+
reverse(snippet.snippet_viewset.get_url_name("add")),
|
|
3131
|
+
post_data,
|
|
3132
|
+
)
|
|
3133
|
+
|
|
3134
|
+
def test_show_edit_view(self):
|
|
3135
|
+
for snippet in [self.snippet_a, self.snippet_b]:
|
|
3136
|
+
with self.subTest(snippet=snippet):
|
|
3137
|
+
response = self.get(snippet)
|
|
3138
|
+
self.assertEqual(response.status_code, 200)
|
|
3139
|
+
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
|
|
3140
|
+
|
|
3141
|
+
def test_edit_invalid(self):
|
|
3142
|
+
response = self.post(self.snippet_a, post_data={"foo": "bar"})
|
|
3143
|
+
soup = self.get_soup(response.content)
|
|
3144
|
+
header_messages = soup.css.select(".messages[role='status'] ul > li")
|
|
3145
|
+
|
|
3146
|
+
# the top level message should indicate that the page could not be saved
|
|
3147
|
+
self.assertEqual(len(header_messages), 1)
|
|
3148
|
+
message = header_messages[0]
|
|
3149
|
+
self.assertIn(
|
|
3150
|
+
"The standard snippet with custom primary key could not be saved due to errors.",
|
|
3151
|
+
message.get_text(),
|
|
3152
|
+
)
|
|
3153
|
+
|
|
3154
|
+
# the top level message should provide a go to error button
|
|
3155
|
+
buttons = message.find_all("button")
|
|
3156
|
+
self.assertEqual(len(buttons), 1)
|
|
3157
|
+
self.assertEqual(buttons[0].attrs["data-controller"], "w-count w-focus")
|
|
3158
|
+
self.assertEqual(
|
|
3159
|
+
set(buttons[0].attrs["data-action"].split()),
|
|
3160
|
+
{"click->w-focus#focus", "wagtail:panel-init@document->w-count#count"},
|
|
3161
|
+
)
|
|
3162
|
+
self.assertIn("Go to the first error", buttons[0].get_text())
|
|
3163
|
+
|
|
3164
|
+
# the errors should appear against the fields with issues
|
|
3165
|
+
error_messages = soup.css.select(".error-message")
|
|
3166
|
+
self.assertEqual(len(error_messages), 2)
|
|
3167
|
+
error_message = error_messages[0]
|
|
3168
|
+
self.assertEqual(error_message.parent["id"], "panel-child-snippet_id-errors")
|
|
3169
|
+
self.assertIn("This field is required", error_message.get_text())
|
|
3170
|
+
|
|
3171
|
+
def test_edit(self):
|
|
3172
|
+
response = self.post(
|
|
3173
|
+
self.snippet_a,
|
|
3174
|
+
post_data={"text": "Edited snippet", "snippet_id": "snippet_id_edited"},
|
|
3175
|
+
)
|
|
3176
|
+
self.assertRedirects(
|
|
3177
|
+
response,
|
|
3178
|
+
reverse(
|
|
3179
|
+
"wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:list"
|
|
3180
|
+
),
|
|
3181
|
+
)
|
|
3182
|
+
|
|
3183
|
+
snippets = StandardSnippetWithCustomPrimaryKey.objects.all()
|
|
3184
|
+
self.assertEqual(snippets.count(), 3)
|
|
3185
|
+
# Saving with a new primary key creates a new instance
|
|
3186
|
+
self.assertTrue(snippets.filter(snippet_id="snippet_id_edited").exists())
|
|
3187
|
+
self.assertTrue(snippets.filter(snippet_id="snippet/01").exists())
|
|
3188
|
+
|
|
3189
|
+
def test_create(self):
|
|
3190
|
+
response = self.create(
|
|
3191
|
+
self.snippet_a,
|
|
3192
|
+
post_data={"text": "test snippet", "snippet_id": "snippet/02"},
|
|
3193
|
+
)
|
|
3194
|
+
self.assertRedirects(
|
|
3195
|
+
response,
|
|
3196
|
+
reverse(
|
|
3197
|
+
"wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:list"
|
|
3198
|
+
),
|
|
3199
|
+
)
|
|
3200
|
+
|
|
3201
|
+
snippets = StandardSnippetWithCustomPrimaryKey.objects.all()
|
|
3202
|
+
self.assertEqual(snippets.count(), 3)
|
|
3203
|
+
self.assertEqual(snippets.order_by("snippet_id").last().text, "test snippet")
|
|
3204
|
+
|
|
3205
|
+
def test_get_delete(self):
|
|
3206
|
+
for snippet in [self.snippet_a, self.snippet_b]:
|
|
3207
|
+
with self.subTest(snippet=snippet):
|
|
3208
|
+
response = self.client.get(
|
|
3209
|
+
reverse(
|
|
3210
|
+
"wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:delete",
|
|
3211
|
+
args=[quote(snippet.pk)],
|
|
3212
|
+
)
|
|
3213
|
+
)
|
|
3214
|
+
self.assertEqual(response.status_code, 200)
|
|
3215
|
+
self.assertTemplateUsed(
|
|
3216
|
+
response, "wagtailadmin/generic/confirm_delete.html"
|
|
3217
|
+
)
|
|
3218
|
+
|
|
3219
|
+
def test_usage_link(self):
|
|
3220
|
+
for snippet in [self.snippet_a, self.snippet_b]:
|
|
3221
|
+
with self.subTest(snippet=snippet):
|
|
3222
|
+
response = self.client.get(
|
|
3223
|
+
reverse(
|
|
3224
|
+
"wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:delete",
|
|
3225
|
+
args=[quote(snippet.pk)],
|
|
3226
|
+
)
|
|
3227
|
+
)
|
|
3228
|
+
self.assertEqual(response.status_code, 200)
|
|
3229
|
+
self.assertTemplateUsed(
|
|
3230
|
+
response, "wagtailadmin/generic/confirm_delete.html"
|
|
3231
|
+
)
|
|
3232
|
+
self.assertContains(
|
|
3233
|
+
response,
|
|
3234
|
+
"This standard snippet with custom primary key is referenced 0 times",
|
|
3235
|
+
)
|
|
3236
|
+
self.assertContains(
|
|
3237
|
+
response,
|
|
3238
|
+
reverse(
|
|
3239
|
+
"wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:usage",
|
|
3240
|
+
args=[quote(snippet.pk)],
|
|
3241
|
+
)
|
|
3242
|
+
+ "?describe_on_delete=1",
|
|
3243
|
+
)
|