wagtail 6.0.1__py3-none-any.whl → 6.1rc1__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/admin/checks.py +51 -0
- wagtail/admin/compare.py +1 -1
- wagtail/admin/filters.py +70 -1
- wagtail/admin/forms/account.py +1 -1
- wagtail/admin/forms/collections.py +15 -0
- wagtail/admin/forms/pages.py +49 -0
- wagtail/admin/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ca/LC_MESSAGES/django.po +122 -0
- wagtail/admin/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/de/LC_MESSAGES/django.po +5 -5
- wagtail/admin/locale/en/LC_MESSAGES/django.po +474 -385
- wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +3 -3
- wagtail/admin/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/es/LC_MESSAGES/django.po +6 -6
- wagtail/admin/locale/fr/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/fr/LC_MESSAGES/django.po +70 -3
- wagtail/admin/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/he_IL/LC_MESSAGES/django.po +2 -6
- wagtail/admin/locale/he_IL/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/he_IL/LC_MESSAGES/djangojs.po +2 -2
- wagtail/admin/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/hr_HR/LC_MESSAGES/django.po +4 -0
- wagtail/admin/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/hu/LC_MESSAGES/django.po +142 -2
- wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/it/LC_MESSAGES/django.po +80 -8
- wagtail/admin/locale/it/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/it/LC_MESSAGES/djangojs.po +14 -2
- wagtail/admin/locale/lv/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/lv/LC_MESSAGES/django.po +154 -1
- wagtail/admin/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/pt_PT/LC_MESSAGES/django.po +73 -2
- wagtail/admin/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ro/LC_MESSAGES/django.po +3 -3
- wagtail/admin/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/sl/LC_MESSAGES/django.po +145 -2
- wagtail/admin/locale/sv/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/sv/LC_MESSAGES/django.po +77 -3
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/zh_Hant/LC_MESSAGES/django.po +17 -1
- wagtail/admin/panels/comment_panel.py +1 -1
- wagtail/admin/panels/field_panel.py +1 -1
- wagtail/admin/rich_text/converters/editor_html.py +3 -1
- wagtail/admin/rich_text/editors/draftail/__init__.py +28 -2
- wagtail/admin/static/wagtailadmin/css/core.css +1 -1
- wagtail/admin/static/wagtailadmin/css/panels/draftail.css +1 -1
- wagtail/admin/static/wagtailadmin/images/favicon.ico +0 -0
- wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
- wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
- wagtail/admin/static/wagtailadmin/js/core.js +1 -1
- wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +1 -1
- wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
- wagtail/admin/static/wagtailadmin/js/expanding-formset.js +1 -1
- wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
- wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/preview-panel.js +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/task-chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
- wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +4 -4
- wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
- wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
- wagtail/admin/staticfiles.py +1 -0
- wagtail/admin/templates/wagtailadmin/admin_base.html +1 -0
- wagtail/admin/templates/wagtailadmin/base.html +1 -0
- wagtail/admin/templates/wagtailadmin/collection_privacy/set_privacy.html +3 -1
- wagtail/admin/templates/wagtailadmin/collections/edit.html +0 -1
- wagtail/admin/templates/wagtailadmin/collections/index_results.html +10 -0
- wagtail/admin/templates/wagtailadmin/generic/base.html +1 -9
- wagtail/admin/templates/wagtailadmin/generic/form.html +4 -2
- wagtail/admin/templates/wagtailadmin/generic/history/action_cell.html +27 -0
- wagtail/admin/templates/wagtailadmin/generic/index_results.html +8 -0
- wagtail/admin/templates/wagtailadmin/home/workflow_objects_to_moderate.html +3 -4
- wagtail/admin/templates/wagtailadmin/icons/keyboard.svg +1 -0
- wagtail/admin/templates/wagtailadmin/page_privacy/set_privacy.html +3 -1
- wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -15
- wagtail/admin/templates/wagtailadmin/pages/action_menu/save_draft.html +3 -1
- wagtail/admin/templates/wagtailadmin/pages/choose_parent.html +17 -0
- wagtail/admin/templates/wagtailadmin/pages/explorable_index.html +8 -0
- wagtail/admin/templates/wagtailadmin/pages/history.html +1 -61
- wagtail/admin/templates/wagtailadmin/pages/index.html +1 -5
- wagtail/admin/templates/wagtailadmin/pages/listing/_locked_indicator.html +2 -2
- wagtail/admin/templates/wagtailadmin/pages/listing/_page_title_column_header.html +25 -27
- wagtail/admin/templates/wagtailadmin/pages/page_listing_header.html +2 -1
- wagtail/admin/templates/wagtailadmin/panels/multi_field_panel_child.html +1 -1
- wagtail/admin/templates/wagtailadmin/panels/publishing/schedule_publishing_panel.html +3 -1
- wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/active_filters.html +2 -1
- wagtail/admin/templates/wagtailadmin/shared/breadcrumbs.html +8 -0
- wagtail/admin/templates/wagtailadmin/shared/forms/single_checkbox.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/headers/page_edit_header.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +21 -9
- wagtail/admin/templates/wagtailadmin/shared/human_readable_date.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/keyboard_shortcuts_dialog.html +29 -0
- wagtail/admin/templates/wagtailadmin/shared/side_panel_toggle.html +2 -1
- wagtail/admin/templates/wagtailadmin/skeleton.html +2 -1
- wagtail/admin/templates/wagtailadmin/tables/related_objects_cell.html +9 -0
- wagtail/admin/templates/wagtailadmin/tables/title_cell.html +9 -7
- wagtail/admin/templates/wagtailadmin/widgets/draftail_rich_text_area.html +1 -1
- wagtail/admin/templates/wagtailadmin/workflows/create.html +6 -23
- wagtail/admin/templates/wagtailadmin/workflows/create_task.html +6 -15
- wagtail/admin/templates/wagtailadmin/workflows/edit.html +6 -23
- wagtail/admin/templates/wagtailadmin/workflows/edit_task.html +6 -13
- wagtail/admin/templates/wagtailadmin/workflows/includes/task_usage_cell.html +4 -4
- wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_tasks_cell.html +18 -0
- wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_title_cell.html +7 -0
- wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_used_by_cell.html +25 -0
- wagtail/admin/templates/wagtailadmin/workflows/index.html +0 -99
- wagtail/admin/templates/wagtailadmin/workflows/index_results.html +10 -0
- wagtail/admin/templates/wagtailadmin/workflows/task_index.html +0 -30
- wagtail/admin/templates/wagtailadmin/workflows/task_index_results.html +10 -0
- wagtail/admin/templates/wagtailadmin/workflows/usage.html +1 -1
- wagtail/admin/templatetags/wagtailadmin_tags.py +116 -39
- wagtail/admin/tests/api/test_pages.py +26 -10
- wagtail/admin/tests/pages/test_create_page.py +10 -4
- wagtail/admin/tests/pages/test_custom_listing.py +37 -0
- wagtail/admin/tests/pages/test_edit_page.py +6 -6
- wagtail/admin/tests/pages/test_explorer_view.py +19 -18
- wagtail/admin/tests/pages/test_move_page.py +1 -1
- wagtail/admin/tests/pages/test_page_usage.py +50 -2
- wagtail/admin/tests/pages/test_parent_page_chooser_view.py +119 -0
- wagtail/admin/tests/pages/test_preview.py +18 -4
- wagtail/admin/tests/test_account_management.py +20 -1
- wagtail/admin/tests/test_audit_log.py +172 -5
- wagtail/admin/tests/test_checks.py +92 -0
- wagtail/admin/tests/test_collections_views.py +19 -5
- wagtail/admin/tests/test_compare.py +6 -6
- wagtail/admin/tests/test_dashboard.py +404 -0
- wagtail/admin/tests/test_dbwhitelister.py +4 -5
- wagtail/admin/tests/test_edit_handlers.py +2 -2
- wagtail/admin/tests/test_keyboard_shortcuts.py +84 -0
- wagtail/admin/tests/test_page_chooser.py +31 -18
- wagtail/admin/tests/test_privacy.py +36 -2
- wagtail/admin/tests/test_rich_text.py +168 -23
- wagtail/admin/tests/test_templatetags.py +411 -43
- wagtail/admin/tests/test_views.py +4 -2
- wagtail/admin/tests/test_workflows.py +531 -9
- wagtail/admin/tests/tests.py +3 -1
- wagtail/admin/tests/ui/test_tables.py +48 -1
- wagtail/admin/tests/viewsets/test_model_viewset.py +130 -23
- wagtail/admin/ui/side_panels.py +3 -1
- wagtail/admin/ui/tables/__init__.py +13 -1
- wagtail/admin/ui/tables/pages.py +17 -6
- wagtail/admin/urls/__init__.py +8 -3
- wagtail/admin/urls/pages.py +5 -0
- wagtail/admin/urls/workflows.py +10 -0
- wagtail/admin/views/chooser.py +20 -24
- wagtail/admin/views/collections.py +17 -1
- wagtail/admin/views/generic/base.py +34 -4
- wagtail/admin/views/generic/history.py +220 -51
- wagtail/admin/views/generic/mixins.py +7 -4
- wagtail/admin/views/generic/models.py +54 -47
- wagtail/admin/views/generic/multiple_upload.py +17 -8
- wagtail/admin/views/generic/usage.py +17 -11
- wagtail/admin/views/home.py +15 -12
- wagtail/admin/views/mixins.py +30 -0
- wagtail/admin/views/pages/choose_parent.py +73 -0
- wagtail/admin/views/pages/history.py +54 -66
- wagtail/admin/views/pages/listing.py +187 -106
- wagtail/admin/views/pages/usage.py +6 -1
- wagtail/admin/views/pages/utils.py +70 -1
- wagtail/admin/views/workflows.py +150 -21
- wagtail/admin/viewsets/model.py +2 -2
- wagtail/admin/viewsets/pages.py +77 -0
- wagtail/admin/wagtail_hooks.py +40 -2
- wagtail/admin/widgets/button.py +10 -10
- wagtail/api/v2/filters.py +1 -1
- wagtail/api/v2/tests/test_pages.py +1 -1
- wagtail/blocks/base.py +18 -9
- wagtail/blocks/field_block.py +9 -7
- wagtail/blocks/list_block.py +16 -6
- wagtail/blocks/static_block.py +3 -0
- wagtail/blocks/stream_block.py +58 -23
- wagtail/blocks/struct_block.py +15 -9
- wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +39 -47
- wagtail/contrib/forms/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/forms/models.py +5 -5
- wagtail/contrib/forms/templates/wagtailforms/list_submissions.html +44 -33
- wagtail/contrib/forms/templates/wagtailforms/submissions_index.html +2 -63
- wagtail/contrib/forms/tests/test_models.py +26 -0
- wagtail/contrib/forms/urls.py +6 -0
- wagtail/contrib/forms/views.py +52 -49
- wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.po +3 -3
- wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +34 -42
- wagtail/contrib/redirects/locale/fr/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/redirects/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/redirects/signal_handlers.py +1 -1
- wagtail/contrib/redirects/templates/wagtailredirects/index.html +1 -36
- wagtail/contrib/redirects/templates/wagtailredirects/index_results.html +18 -0
- wagtail/contrib/redirects/templates/wagtailredirects/redirect_target_cell.html +8 -0
- wagtail/contrib/redirects/tests/test_import_command.py +1 -1
- wagtail/contrib/redirects/tests/test_redirects.py +79 -8
- wagtail/contrib/redirects/urls.py +2 -1
- wagtail/contrib/redirects/views.py +85 -55
- wagtail/contrib/search_promotions/admin_urls.py +2 -1
- wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +41 -64
- wagtail/contrib/search_promotions/locale/fr/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/search_promotions/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/search_promotions/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/hr_HR/LC_MESSAGES/django.po +41 -2
- wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.po +9 -3
- wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index.html +1 -16
- wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index_results.html +11 -0
- wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/list.html +0 -51
- wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/results.html +3 -16
- wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/search_promotion_column.html +15 -0
- wagtail/contrib/search_promotions/tests.py +122 -9
- wagtail/contrib/search_promotions/views.py +66 -65
- wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +3 -3
- wagtail/contrib/settings/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/settings/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/tr/LC_MESSAGES/django.po +6 -2
- wagtail/contrib/settings/registry.py +10 -5
- wagtail/contrib/settings/tests/generic/test_admin.py +9 -0
- wagtail/contrib/settings/tests/site_specific/test_admin.py +10 -1
- wagtail/contrib/settings/tests/site_specific/test_model.py +3 -3
- wagtail/contrib/settings/tests/site_specific/test_templates.py +1 -1
- wagtail/contrib/settings/views.py +3 -1
- wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/simple_translation/tests/test_wagtail_hooks.py +2 -2
- wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
- wagtail/contrib/styleguide/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/styleguide/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/contrib/table_block/blocks.py +2 -2
- wagtail/contrib/table_block/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/ca/LC_MESSAGES/django.po +27 -2
- wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/table_block/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/hu/LC_MESSAGES/django.po +27 -2
- wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.po +27 -2
- wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
- wagtail/contrib/table_block/tests.py +6 -0
- wagtail/contrib/typed_table_block/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/typed_table_block/locale/ca/LC_MESSAGES/django.po +12 -2
- wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/typed_table_block/locale/hu/LC_MESSAGES/django.po +12 -2
- wagtail/contrib/typed_table_block/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/typed_table_block/locale/it/LC_MESSAGES/django.po +12 -2
- wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
- wagtail/coreutils.py +3 -2
- wagtail/documents/admin_urls.py +2 -2
- wagtail/documents/locale/en/LC_MESSAGES/django.po +22 -22
- wagtail/documents/locale/fr/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/documents/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/hr_HR/LC_MESSAGES/django.po +19 -2
- wagtail/documents/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/hu/LC_MESSAGES/django.po +16 -2
- wagtail/documents/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/it/LC_MESSAGES/django.po +19 -2
- wagtail/documents/migrations/0013_delete_uploadeddocument.py +16 -0
- wagtail/documents/models.py +1 -20
- wagtail/documents/rich_text/__init__.py +11 -7
- wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
- wagtail/documents/templates/wagtaildocs/documents/index.html +0 -16
- wagtail/documents/tests/test_admin_views.py +155 -23
- wagtail/documents/tests/test_collection_privacy.py +55 -1
- wagtail/documents/tests/test_rich_text.py +14 -0
- wagtail/documents/views/documents.py +25 -22
- wagtail/documents/views/multiple.py +6 -7
- wagtail/documents/views/serve.py +16 -1
- wagtail/documents/wagtail_hooks.py +20 -15
- wagtail/embeds/blocks.py +5 -0
- wagtail/embeds/locale/en/LC_MESSAGES/django.po +2 -2
- wagtail/embeds/locale/fr/LC_MESSAGES/django.po +2 -2
- wagtail/embeds/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/embeds/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/embeds/rich_text/__init__.py +1 -1
- wagtail/embeds/tests/test_rich_text.py +14 -0
- wagtail/embeds/wagtail_hooks.py +4 -14
- wagtail/fields.py +3 -48
- wagtail/images/admin_urls.py +2 -2
- wagtail/images/check_files/wagtail.jpg +0 -0
- wagtail/images/check_files/wagtail.png +0 -0
- wagtail/images/fields.py +2 -0
- wagtail/images/image_operations.py +1 -1
- wagtail/images/locale/ca/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/ca/LC_MESSAGES/django.po +12 -0
- wagtail/images/locale/en/LC_MESSAGES/django.po +33 -45
- wagtail/images/locale/fr/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/images/locale/hu/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/hu/LC_MESSAGES/django.po +28 -2
- wagtail/images/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/it/LC_MESSAGES/django.po +14 -2
- wagtail/images/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/pt_PT/LC_MESSAGES/django.po +4 -0
- wagtail/images/migrations/0026_delete_uploadedimage.py +16 -0
- wagtail/images/models.py +49 -43
- wagtail/images/rich_text/__init__.py +18 -8
- wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
- wagtail/images/templates/wagtailimages/images/image_listing_header.html +6 -0
- wagtail/images/templates/wagtailimages/images/index.html +11 -51
- wagtail/images/tests/test_admin_views.py +119 -62
- wagtail/images/tests/test_image_operations.py +10 -0
- wagtail/images/tests/test_models.py +35 -33
- wagtail/images/tests/test_rich_text.py +14 -0
- wagtail/images/tests/utils.py +1 -1
- wagtail/images/views/images.py +35 -64
- wagtail/images/views/multiple.py +6 -7
- wagtail/images/wagtail_hooks.py +4 -14
- wagtail/locale/en/LC_MESSAGES/django.po +150 -136
- wagtail/locale/es/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/es/LC_MESSAGES/django.po +3 -2
- wagtail/locale/fr/LC_MESSAGES/django.po +2 -2
- wagtail/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/it/LC_MESSAGES/django.po +5 -5
- wagtail/locale/sl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/sl/LC_MESSAGES/django.po +27 -2
- wagtail/locales/locale/ar/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/be/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/bg/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ca/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/cs/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/cy/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/da/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/de/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/el/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/es/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/et/LC_MESSAGES/django.po +2 -2
- wagtail/locales/locale/fa/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/fi/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/fr/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/gl/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/he_IL/LC_MESSAGES/django.po +3 -3
- wagtail/locales/locale/hr_HR/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/hu/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/id_ID/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/is_IS/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/it/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ja/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ko/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/lt/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/lv/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/mi/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/mn/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/my/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/nb/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/nl/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/pl/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/pt_BR/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/pt_PT/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ro/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/ru/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/sk_SK/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/sl/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/sv/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/tet/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/th/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/tr/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/tr_TR/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/uk/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/vi/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/zh/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
- wagtail/locales/tests.py +18 -3
- wagtail/locales/views.py +0 -1
- wagtail/management/commands/rebuild_references_index.py +3 -1
- wagtail/migrations/0092_alter_collectionviewrestriction_password_and_more.py +33 -0
- wagtail/migrations/0093_uploadedfile.py +53 -0
- wagtail/models/__init__.py +147 -32
- wagtail/models/i18n.py +1 -1
- wagtail/models/{collections.py → media.py} +33 -2
- wagtail/models/reference_index.py +1 -1
- wagtail/models/view_restrictions.py +10 -3
- wagtail/project_template/project_name/settings/base.py +6 -0
- wagtail/project_template/requirements.txt +1 -1
- wagtail/rich_text/__init__.py +25 -8
- wagtail/rich_text/pages.py +19 -8
- wagtail/rich_text/rewriters.py +140 -68
- wagtail/search/backends/database/mysql/mysql.py +3 -3
- wagtail/search/backends/database/postgres/postgres.py +3 -3
- wagtail/search/backends/database/sqlite/sqlite.py +2 -2
- wagtail/search/backends/elasticsearch7.py +4 -0
- wagtail/search/locale/en/LC_MESSAGES/django.po +3 -3
- wagtail/search/tests/test_postgres_backend.py +50 -0
- wagtail/sites/locale/en/LC_MESSAGES/django.po +8 -8
- wagtail/sites/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/sites/locale/ro/LC_MESSAGES/django.mo +0 -0
- wagtail/sites/locale/ro/LC_MESSAGES/django.po +3 -2
- wagtail/sites/tests.py +35 -9
- wagtail/sites/views.py +3 -1
- wagtail/snippets/locale/de/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/de/LC_MESSAGES/django.po +7 -8
- wagtail/snippets/locale/en/LC_MESSAGES/django.po +16 -56
- wagtail/snippets/locale/fr/LC_MESSAGES/django.po +2 -2
- wagtail/snippets/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/snippets/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/hr_HR/LC_MESSAGES/django.po +6 -2
- wagtail/snippets/locale/lv/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/lv/LC_MESSAGES/django.po +12 -0
- wagtail/snippets/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/zh_Hant/LC_MESSAGES/django.po +4 -0
- wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
- wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
- wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/publish.html +3 -1
- wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/save.html +3 -1
- wagtail/snippets/templates/wagtailsnippets/snippets/create.html +2 -3
- wagtail/snippets/templates/wagtailsnippets/snippets/edit.html +2 -3
- wagtail/snippets/tests/test_preview.py +13 -2
- wagtail/snippets/tests/test_snippets.py +41 -16
- wagtail/snippets/tests/test_viewset.py +95 -18
- wagtail/snippets/tests/test_workflows.py +12 -0
- wagtail/snippets/views/snippets.py +1 -40
- wagtail/templatetags/wagtailcore_tags.py +1 -1
- wagtail/test/demosite/models.py +1 -1
- wagtail/test/middleware.py +14 -1
- wagtail/test/testapp/fixtures/test.json +20 -0
- wagtail/test/testapp/migrations/0001_squashed_0073_revisablechildmodel_secret_text.py +8 -8
- wagtail/test/testapp/migrations/0023_snippetchoosermodel_full_featured.py +1 -0
- wagtail/test/testapp/migrations/0034_custompermissionmodel.py +44 -0
- wagtail/test/testapp/migrations/0035_modelwithcustommanager.py +30 -0
- wagtail/test/testapp/migrations/0036_complexdefaultstreampage.py +28 -0
- wagtail/test/testapp/models.py +79 -2
- wagtail/test/testapp/templates/tests/custom_docs_password_required.html +10 -0
- wagtail/test/testapp/templates/tests/custom_page_password_required.html +10 -0
- wagtail/test/testapp/views.py +24 -2
- wagtail/test/testapp/wagtail_hooks.py +19 -0
- wagtail/test/utils/wagtail_tests.py +2 -2
- wagtail/tests/test_blocks.py +262 -1
- wagtail/tests/test_migrations.py +1 -1
- wagtail/tests/test_page_model.py +77 -0
- wagtail/tests/test_page_privacy.py +18 -1
- wagtail/tests/test_rich_text.py +95 -5
- wagtail/tests/test_streamfield.py +43 -0
- wagtail/tests/test_utils.py +8 -2
- wagtail/tests/test_views.py +52 -1
- wagtail/tests/test_whitelist.py +7 -7
- wagtail/users/forms.py +3 -1
- wagtail/users/locale/en/LC_MESSAGES/django.po +124 -96
- wagtail/users/locale/fr/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/he_IL/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/he_IL/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/hr_HR/LC_MESSAGES/django.po +13 -2
- wagtail/users/migrations/0013_userprofile_density.py +23 -0
- wagtail/users/models.py +14 -3
- wagtail/users/templates/wagtailusers/groups/create.html +1 -7
- wagtail/users/templates/wagtailusers/groups/edit.html +1 -13
- wagtail/users/templates/wagtailusers/groups/includes/formatted_permissions.html +46 -2
- wagtail/users/templates/wagtailusers/groups/includes/group_form_js.html +0 -3
- wagtail/users/templates/wagtailusers/users/create.html +1 -14
- wagtail/users/templates/wagtailusers/users/edit.html +1 -14
- wagtail/users/templates/wagtailusers/users/index.html +2 -5
- wagtail/users/templates/wagtailusers/users/index_results.html +3 -13
- wagtail/users/templates/wagtailusers/users/user_cell.html +9 -0
- wagtail/users/templatetags/wagtailusers_tags.py +107 -20
- wagtail/users/tests/test_admin_views.py +669 -90
- wagtail/users/views/groups.py +58 -61
- wagtail/users/views/users.py +211 -92
- wagtail/users/wagtail_hooks.py +6 -38
- wagtail/users/widgets.py +3 -5
- wagtail/utils/text.py +1 -1
- wagtail/views.py +5 -9
- wagtail/whitelist.py +1 -1
- {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/METADATA +5 -6
- {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/RECORD +496 -477
- wagtail/admin/static/wagtailadmin/js/page-editor.js +0 -1
- wagtail/admin/static/wagtailadmin/js/vendor/mousetrap.min.js +0 -1
- wagtail/admin/static/wagtailadmin/js/vendor/urlify.js +0 -1
- wagtail/admin/static/wagtailadmin/js/vendor/xregexp.min.js +0 -1
- wagtail/admin/templates/wagtailadmin/collections/index.html +0 -34
- wagtail/admin/templates/wagtailadmin/pages/revisions/_actions.html +0 -22
- wagtail/admin/templates/wagtailadmin/shared/page_breadcrumbs.html +0 -55
- wagtail/admin/tests/pages/test_dashboard.py +0 -172
- wagtail/contrib/redirects/templates/wagtailredirects/results.html +0 -23
- wagtail/documents/templates/wagtaildocs/documents/list.html +0 -2
- wagtail/search/tests/test_postgres_stemming.py +0 -40
- wagtail/sites/templates/wagtailsites/create.html +0 -7
- wagtail/sites/templates/wagtailsites/edit.html +0 -7
- wagtail/snippets/templates/wagtailsnippets/snippets/revisions/_actions.html +0 -36
- wagtail/users/templates/wagtailusers/users/list.html +0 -62
- wagtail/users/urls/users.py +0 -12
- {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/LICENSE +0 -0
- {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/WHEEL +0 -0
- {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/entry_points.txt +0 -0
- {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/top_level.txt +0 -0
|
@@ -153,13 +153,13 @@ class TestRichTextFieldComparison(TestFieldComparison):
|
|
|
153
153
|
SimplePage._meta.get_field("content"),
|
|
154
154
|
SimplePage(content="Original content"),
|
|
155
155
|
SimplePage(
|
|
156
|
-
content='<script type="text/javascript">doSomethingBad();</script>'
|
|
156
|
+
content='Do something good. <script type="text/javascript">doSomethingBad();</script>'
|
|
157
157
|
),
|
|
158
158
|
)
|
|
159
159
|
|
|
160
160
|
self.assertEqual(
|
|
161
161
|
comparison.htmldiff(),
|
|
162
|
-
'<span class="deletion">Original content</span><span class="addition">
|
|
162
|
+
'<span class="deletion">Original content</span><span class="addition">Do something good.</span>',
|
|
163
163
|
)
|
|
164
164
|
self.assertIsInstance(comparison.htmldiff(), SafeString)
|
|
165
165
|
|
|
@@ -491,7 +491,7 @@ class TestStreamFieldComparison(TestCase):
|
|
|
491
491
|
|
|
492
492
|
self.assertEqual(
|
|
493
493
|
comparison.htmldiff(),
|
|
494
|
-
'<div class="comparison__child-object">I really like <span class="deletion">Wagtail <3</span><span class="addition">evil code >_<
|
|
494
|
+
'<div class="comparison__child-object">I really like <span class="deletion">Wagtail <3</span><span class="addition">evil code >_<</span></div>',
|
|
495
495
|
)
|
|
496
496
|
self.assertIsInstance(comparison.htmldiff(), SafeString)
|
|
497
497
|
|
|
@@ -525,7 +525,7 @@ class TestStreamFieldComparison(TestCase):
|
|
|
525
525
|
|
|
526
526
|
self.assertEqual(
|
|
527
527
|
comparison.htmldiff(),
|
|
528
|
-
'<div class="comparison__child-object">Original and unchanged content</div>\n<div class="comparison__child-object addition">I really like evil code >_<
|
|
528
|
+
'<div class="comparison__child-object">Original and unchanged content</div>\n<div class="comparison__child-object addition">I really like evil code >_<</div>',
|
|
529
529
|
)
|
|
530
530
|
self.assertIsInstance(comparison.htmldiff(), SafeString)
|
|
531
531
|
|
|
@@ -559,7 +559,7 @@ class TestStreamFieldComparison(TestCase):
|
|
|
559
559
|
|
|
560
560
|
self.assertEqual(
|
|
561
561
|
comparison.htmldiff(),
|
|
562
|
-
'<div class="comparison__child-object">Original and unchanged content</div>\n<div class="comparison__child-object deletion">I really like evil code >_<
|
|
562
|
+
'<div class="comparison__child-object">Original and unchanged content</div>\n<div class="comparison__child-object deletion">I really like evil code >_<</div>',
|
|
563
563
|
)
|
|
564
564
|
self.assertIsInstance(comparison.htmldiff(), SafeString)
|
|
565
565
|
|
|
@@ -1228,7 +1228,7 @@ class TestChildRelationComparison(TestCase):
|
|
|
1228
1228
|
def test_has_changed(self):
|
|
1229
1229
|
# Father Christmas renamed to Santa Claus. And Father Ted added.
|
|
1230
1230
|
# Father Christmas should be mapped to Father Ted because they
|
|
1231
|
-
# are most alike. Santa
|
|
1231
|
+
# are most alike. Santa Claus should be displayed as "new"
|
|
1232
1232
|
event_page = EventPage(title="Event page", slug="event")
|
|
1233
1233
|
event_page.speakers.add(
|
|
1234
1234
|
EventPageSpeaker(
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
from django.contrib.auth import get_user_model
|
|
2
|
+
from django.contrib.auth.models import Group, Permission
|
|
3
|
+
from django.contrib.contenttypes.models import ContentType
|
|
4
|
+
from django.test import TestCase
|
|
5
|
+
from django.urls import reverse
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
from freezegun import freeze_time
|
|
8
|
+
|
|
9
|
+
from wagtail.admin.views.home import (
|
|
10
|
+
LockedPagesPanel,
|
|
11
|
+
RecentEditsPanel,
|
|
12
|
+
UserObjectsInWorkflowModerationPanel,
|
|
13
|
+
WorkflowObjectsToModeratePanel,
|
|
14
|
+
)
|
|
15
|
+
from wagtail.coreutils import get_dummy_request
|
|
16
|
+
from wagtail.models import GroupPagePermission, Page, Workflow, WorkflowContentType
|
|
17
|
+
from wagtail.test.testapp.models import FullFeaturedSnippet, SimplePage
|
|
18
|
+
from wagtail.test.utils import WagtailTestUtils
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestRecentEditsPanel(WagtailTestUtils, TestCase):
|
|
22
|
+
def setUp(self):
|
|
23
|
+
# Find root page
|
|
24
|
+
self.root_page = Page.objects.get(id=2)
|
|
25
|
+
|
|
26
|
+
# Add child page
|
|
27
|
+
child_page = SimplePage(
|
|
28
|
+
title="Hello world!",
|
|
29
|
+
slug="hello-world",
|
|
30
|
+
content="Some content here",
|
|
31
|
+
)
|
|
32
|
+
self.root_page.add_child(instance=child_page)
|
|
33
|
+
self.revision = child_page.save_revision()
|
|
34
|
+
self.revision.publish()
|
|
35
|
+
self.child_page = SimplePage.objects.get(id=child_page.id)
|
|
36
|
+
|
|
37
|
+
self.user_alice = self.create_superuser(username="alice", password="password")
|
|
38
|
+
self.create_superuser(username="bob", password="password")
|
|
39
|
+
|
|
40
|
+
def change_something(self, title):
|
|
41
|
+
post_data = {"title": title, "content": "Some content", "slug": "hello-world"}
|
|
42
|
+
response = self.client.post(
|
|
43
|
+
reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)), post_data
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Should be redirected to edit page
|
|
47
|
+
self.assertRedirects(
|
|
48
|
+
response, reverse("wagtailadmin_pages:edit", args=(self.child_page.id,))
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# The page should have "has_unpublished_changes" flag set
|
|
52
|
+
child_page_new = SimplePage.objects.get(id=self.child_page.id)
|
|
53
|
+
self.assertTrue(child_page_new.has_unpublished_changes)
|
|
54
|
+
|
|
55
|
+
def go_to_dashboard_response(self):
|
|
56
|
+
response = self.client.get(reverse("wagtailadmin_home"))
|
|
57
|
+
self.assertEqual(response.status_code, 200)
|
|
58
|
+
return response
|
|
59
|
+
|
|
60
|
+
def test_your_recent_edits(self):
|
|
61
|
+
# Login as Bob
|
|
62
|
+
self.login(username="bob", password="password")
|
|
63
|
+
|
|
64
|
+
# Bob hasn't edited anything yet
|
|
65
|
+
response = self.client.get(reverse("wagtailadmin_home"))
|
|
66
|
+
self.assertNotIn("Your most recent edits", response.content.decode("utf-8"))
|
|
67
|
+
|
|
68
|
+
# Login as Alice
|
|
69
|
+
self.client.logout()
|
|
70
|
+
self.login(username="alice", password="password")
|
|
71
|
+
|
|
72
|
+
# Alice changes something
|
|
73
|
+
self.change_something("Alice's edit")
|
|
74
|
+
|
|
75
|
+
# Edit should show up on dashboard
|
|
76
|
+
response = self.go_to_dashboard_response()
|
|
77
|
+
self.assertIn("Your most recent edits", response.content.decode("utf-8"))
|
|
78
|
+
|
|
79
|
+
# Bob changes something
|
|
80
|
+
self.login(username="bob", password="password")
|
|
81
|
+
self.change_something("Bob's edit")
|
|
82
|
+
|
|
83
|
+
# Edit shows up on Bobs dashboard
|
|
84
|
+
response = self.go_to_dashboard_response()
|
|
85
|
+
self.assertIn("Your most recent edits", response.content.decode("utf-8"))
|
|
86
|
+
|
|
87
|
+
# Login as Alice again
|
|
88
|
+
self.client.logout()
|
|
89
|
+
self.login(username="alice", password="password")
|
|
90
|
+
|
|
91
|
+
# Alice's dashboard should still list that first edit
|
|
92
|
+
response = self.go_to_dashboard_response()
|
|
93
|
+
self.assertIn("Your most recent edits", response.content.decode("utf-8"))
|
|
94
|
+
|
|
95
|
+
def test_missing_page_record(self):
|
|
96
|
+
# Ensure that the panel still renders when one of the page IDs returned from querying
|
|
97
|
+
# PageLogEntry has no corresponding Page object. This can happen if a page is deleted,
|
|
98
|
+
# because PageLogEntry records are kept on deletion.
|
|
99
|
+
|
|
100
|
+
self.login(username="alice", password="password")
|
|
101
|
+
self.change_something("Alice's edit")
|
|
102
|
+
self.child_page.delete()
|
|
103
|
+
response = self.client.get(reverse("wagtailadmin_home"))
|
|
104
|
+
self.assertEqual(response.status_code, 200)
|
|
105
|
+
|
|
106
|
+
def test_panel(self):
|
|
107
|
+
"""Test if the panel actually returns expected pages"""
|
|
108
|
+
self.login(username="bob", password="password")
|
|
109
|
+
# change a page
|
|
110
|
+
|
|
111
|
+
edit_timestamp = timezone.now()
|
|
112
|
+
with freeze_time(edit_timestamp):
|
|
113
|
+
self.change_something("Bob's edit")
|
|
114
|
+
|
|
115
|
+
# set a user to 'mock' a request
|
|
116
|
+
self.client.user = get_user_model().objects.get(email="bob@example.com")
|
|
117
|
+
# get the panel to get the last edits
|
|
118
|
+
panel = RecentEditsPanel()
|
|
119
|
+
ctx = panel.get_context_data({"request": self.client})
|
|
120
|
+
|
|
121
|
+
page = Page.objects.get(pk=self.child_page.id).specific
|
|
122
|
+
|
|
123
|
+
# check the timestamp matches the edit
|
|
124
|
+
self.assertEqual(ctx["last_edits"][0][0], edit_timestamp)
|
|
125
|
+
# check if the page in this list is the specific page
|
|
126
|
+
self.assertEqual(ctx["last_edits"][0][1], page)
|
|
127
|
+
|
|
128
|
+
def test_copying_does_not_count_as_an_edit(self):
|
|
129
|
+
self.login(username="bob", password="password")
|
|
130
|
+
# change a page
|
|
131
|
+
self.change_something("Bob was ere")
|
|
132
|
+
|
|
133
|
+
# copy the page
|
|
134
|
+
post_data = {
|
|
135
|
+
"new_title": "Goodbye world!",
|
|
136
|
+
"new_slug": "goodbye-world",
|
|
137
|
+
"new_parent_page": str(self.root_page.id),
|
|
138
|
+
"copy_subpages": False,
|
|
139
|
+
"alias": False,
|
|
140
|
+
}
|
|
141
|
+
self.client.post(
|
|
142
|
+
reverse("wagtailadmin_pages:copy", args=(self.child_page.id,)), post_data
|
|
143
|
+
)
|
|
144
|
+
# check that page has been copied
|
|
145
|
+
self.assertTrue(Page.objects.get(title="Goodbye world!"))
|
|
146
|
+
|
|
147
|
+
response = self.client.get(reverse("wagtailadmin_home"))
|
|
148
|
+
self.assertEqual(response.status_code, 200)
|
|
149
|
+
self.assertContains(response, "Your most recent edits")
|
|
150
|
+
self.assertContains(response, "Bob was ere")
|
|
151
|
+
self.assertNotContains(response, "Goodbye world!")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestRecentEditsQueryCount(WagtailTestUtils, TestCase):
|
|
155
|
+
fixtures = ["test.json"]
|
|
156
|
+
|
|
157
|
+
def setUp(self):
|
|
158
|
+
self.bob = self.create_superuser(username="bob", password="password")
|
|
159
|
+
self.dummy_request = get_dummy_request()
|
|
160
|
+
self.dummy_request.user = self.bob
|
|
161
|
+
workflow = Workflow.objects.first()
|
|
162
|
+
workflow_pages = {5, 6}
|
|
163
|
+
locked_pages = {6, 9}
|
|
164
|
+
scheduled_pages = {9, 12}
|
|
165
|
+
# make a bunch of page edits (all to EventPages, so that calls to specific() don't add
|
|
166
|
+
# an unpredictable number of queries)
|
|
167
|
+
pages_to_edit = list(
|
|
168
|
+
Page.objects.filter(id__in=[4, 5, 6, 9, 12, 13]).order_by("pk").specific()
|
|
169
|
+
)
|
|
170
|
+
for page in pages_to_edit:
|
|
171
|
+
revision = page.save_revision(user=self.bob, log_action=True)
|
|
172
|
+
if page.pk in workflow_pages:
|
|
173
|
+
workflow.start(page, self.bob)
|
|
174
|
+
if page.pk in locked_pages:
|
|
175
|
+
page.locked = True
|
|
176
|
+
page.locked_by = self.bob
|
|
177
|
+
page.locked_at = timezone.now()
|
|
178
|
+
page.save()
|
|
179
|
+
if page.pk in scheduled_pages:
|
|
180
|
+
revision.approved_go_live_at = timezone.now()
|
|
181
|
+
revision.save()
|
|
182
|
+
|
|
183
|
+
def test_panel_query_count(self):
|
|
184
|
+
panel = RecentEditsPanel()
|
|
185
|
+
parent_context = {"request": self.dummy_request}
|
|
186
|
+
# Warm up the cache
|
|
187
|
+
html = panel.render_html(parent_context)
|
|
188
|
+
|
|
189
|
+
with self.assertNumQueries(5):
|
|
190
|
+
# Rendering RecentEditsPanel should not generate N+1 queries -
|
|
191
|
+
# i.e. any number less than 6 would be reasonable here
|
|
192
|
+
html = panel.render_html(parent_context)
|
|
193
|
+
# check that the panel is still actually returning results
|
|
194
|
+
self.assertIn("Ameristralia Day", html)
|
|
195
|
+
soup = self.get_soup(html)
|
|
196
|
+
self.assertEqual(len(soup.select('svg use[href="#icon-lock"]')), 2)
|
|
197
|
+
expected_statuses = [
|
|
198
|
+
"live + draft",
|
|
199
|
+
"live + scheduled",
|
|
200
|
+
"live + scheduled",
|
|
201
|
+
"in moderation",
|
|
202
|
+
"in moderation",
|
|
203
|
+
]
|
|
204
|
+
statuses = [
|
|
205
|
+
"".join(e.find_all(string=True, recursive=False)).strip()
|
|
206
|
+
for e in soup.select(".w-status")
|
|
207
|
+
]
|
|
208
|
+
self.assertEqual(statuses, expected_statuses)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TestLockedPagesQueryCount(WagtailTestUtils, TestCase):
|
|
212
|
+
fixtures = ["test.json"]
|
|
213
|
+
|
|
214
|
+
def setUp(self):
|
|
215
|
+
self.bob = self.create_superuser(username="bob", password="password")
|
|
216
|
+
self.dummy_request = get_dummy_request()
|
|
217
|
+
self.dummy_request.user = self.bob
|
|
218
|
+
|
|
219
|
+
pages = Page.objects.filter(pk__in=[9, 12, 13]).order_by("pk")
|
|
220
|
+
for i, page in enumerate(pages):
|
|
221
|
+
page.locked = True
|
|
222
|
+
page.locked_by = self.bob
|
|
223
|
+
page.locked_at = timezone.now() + timezone.timedelta(hours=i)
|
|
224
|
+
page.save()
|
|
225
|
+
|
|
226
|
+
def test_panel_query_count(self):
|
|
227
|
+
panel = LockedPagesPanel()
|
|
228
|
+
parent_context = {"request": self.dummy_request, "csrf_token": "dummy"}
|
|
229
|
+
# Warm up the cache
|
|
230
|
+
html = panel.render_html(parent_context)
|
|
231
|
+
|
|
232
|
+
with self.assertNumQueries(1):
|
|
233
|
+
html = panel.render_html(parent_context)
|
|
234
|
+
soup = self.get_soup(html)
|
|
235
|
+
# Should be sorted descending by locked_at
|
|
236
|
+
expected_titles = [
|
|
237
|
+
"Saint Patrick (single event)",
|
|
238
|
+
"Steal underpants",
|
|
239
|
+
"Ameristralia Day",
|
|
240
|
+
]
|
|
241
|
+
titles = [e.get_text(strip=True) for e in soup.select(".title-wrapper a")]
|
|
242
|
+
self.assertEqual(titles, expected_titles)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class UserObjectsInWorkflowModerationQueryCount(WagtailTestUtils, TestCase):
|
|
246
|
+
fixtures = ["test.json"]
|
|
247
|
+
|
|
248
|
+
def setUp(self):
|
|
249
|
+
self.superuser = self.create_superuser(username="admin", password="password")
|
|
250
|
+
self.bob = self.create_user(username="bob", password="password")
|
|
251
|
+
self.someone_else = self.create_user(
|
|
252
|
+
username="someoneelse", password="password"
|
|
253
|
+
)
|
|
254
|
+
editors = Group.objects.get(name="Editors")
|
|
255
|
+
editors.user_set.add(self.bob, self.someone_else)
|
|
256
|
+
|
|
257
|
+
workflow = Workflow.objects.first()
|
|
258
|
+
WorkflowContentType.objects.create(
|
|
259
|
+
workflow=workflow,
|
|
260
|
+
content_type=ContentType.objects.get_for_model(FullFeaturedSnippet),
|
|
261
|
+
)
|
|
262
|
+
GroupPagePermission.objects.create(
|
|
263
|
+
group=editors, page=Page.get_first_root_node(), permission_type="change"
|
|
264
|
+
)
|
|
265
|
+
editors.permissions.add(
|
|
266
|
+
Permission.objects.get(codename="change_fullfeaturedsnippet")
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Pages owned by bob, but workflow started by someone else
|
|
270
|
+
Page.objects.filter(id__in=[9, 12]).update(owner=self.bob)
|
|
271
|
+
for page in Page.objects.filter(id__in=[9, 12]).specific():
|
|
272
|
+
page.save_revision()
|
|
273
|
+
workflow.start(page, self.someone_else)
|
|
274
|
+
# Lock it to test the lock indicator
|
|
275
|
+
page.locked = True
|
|
276
|
+
page.locked_by = self.superuser
|
|
277
|
+
page.locked_at = timezone.now()
|
|
278
|
+
page.save()
|
|
279
|
+
|
|
280
|
+
# Page workflow started by bob
|
|
281
|
+
for page in Page.objects.filter(id__in=[4, 13]).specific():
|
|
282
|
+
page.save_revision()
|
|
283
|
+
workflow.start(page, self.bob)
|
|
284
|
+
|
|
285
|
+
# Snippet workflow started by bob
|
|
286
|
+
for i in range(1, 3):
|
|
287
|
+
obj = FullFeaturedSnippet.objects.create(text=f"Some obj {i}")
|
|
288
|
+
obj.save_revision()
|
|
289
|
+
workflow.start(obj, self.bob)
|
|
290
|
+
|
|
291
|
+
self.dummy_request = get_dummy_request()
|
|
292
|
+
self.dummy_request.user = self.bob
|
|
293
|
+
|
|
294
|
+
def test_panel_query_count(self):
|
|
295
|
+
panel = UserObjectsInWorkflowModerationPanel()
|
|
296
|
+
parent_context = {"request": self.dummy_request}
|
|
297
|
+
# Warm up the cache
|
|
298
|
+
html = panel.render_html(parent_context)
|
|
299
|
+
|
|
300
|
+
with self.assertNumQueries(4):
|
|
301
|
+
html = panel.render_html(parent_context)
|
|
302
|
+
|
|
303
|
+
soup = self.get_soup(html)
|
|
304
|
+
self.assertEqual(len(soup.select('svg use[href="#icon-lock"]')), 2)
|
|
305
|
+
expected_titles = [
|
|
306
|
+
"Some obj 2",
|
|
307
|
+
"Some obj 1",
|
|
308
|
+
"Saint Patrick (single event)",
|
|
309
|
+
"Christmas",
|
|
310
|
+
"Steal underpants",
|
|
311
|
+
"Ameristralia Day",
|
|
312
|
+
]
|
|
313
|
+
titles = [e.get_text(strip=True) for e in soup.select(".title-wrapper a")]
|
|
314
|
+
self.assertEqual(titles, expected_titles)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class WorkflowObjectsToModerateQueryCount(WagtailTestUtils, TestCase):
|
|
318
|
+
fixtures = ["test.json"]
|
|
319
|
+
|
|
320
|
+
def setUp(self):
|
|
321
|
+
self.superuser = self.create_superuser(username="admin", password="password")
|
|
322
|
+
self.bob = self.create_user(username="bob", password="password")
|
|
323
|
+
self.moderator = self.create_user(username="moderator", password="password")
|
|
324
|
+
|
|
325
|
+
editors = Group.objects.get(name="Editors")
|
|
326
|
+
moderators = Group.objects.get(name="Moderators")
|
|
327
|
+
|
|
328
|
+
editors.user_set.add(self.bob)
|
|
329
|
+
moderators.user_set.add(self.moderator)
|
|
330
|
+
|
|
331
|
+
root = Page.get_first_root_node()
|
|
332
|
+
GroupPagePermission.objects.create(
|
|
333
|
+
group=editors, page=root, permission_type="change"
|
|
334
|
+
)
|
|
335
|
+
GroupPagePermission.objects.create(
|
|
336
|
+
group=moderators, page=root, permission_type="change"
|
|
337
|
+
)
|
|
338
|
+
GroupPagePermission.objects.create(
|
|
339
|
+
group=moderators, page=root, permission_type="publish"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
editors.permissions.add(
|
|
343
|
+
Permission.objects.get(codename="change_fullfeaturedsnippet")
|
|
344
|
+
)
|
|
345
|
+
moderators.permissions.add(
|
|
346
|
+
*Permission.objects.filter(
|
|
347
|
+
codename__in=[
|
|
348
|
+
"change_fullfeaturedsnippet",
|
|
349
|
+
"publish_fullfeaturedsnippet",
|
|
350
|
+
]
|
|
351
|
+
),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
workflow = Workflow.objects.first()
|
|
355
|
+
WorkflowContentType.objects.create(
|
|
356
|
+
workflow=workflow,
|
|
357
|
+
content_type=ContentType.objects.get_for_model(FullFeaturedSnippet),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Pages workflow started by bob and locked by moderator
|
|
361
|
+
for page in Page.objects.filter(id__in=[9, 12]).specific():
|
|
362
|
+
page.save_revision()
|
|
363
|
+
workflow.start(page, self.bob)
|
|
364
|
+
# Lock it to test the lock indicator
|
|
365
|
+
page.locked = True
|
|
366
|
+
page.locked_by = self.moderator
|
|
367
|
+
page.locked_at = timezone.now()
|
|
368
|
+
page.save()
|
|
369
|
+
|
|
370
|
+
# Page workflow started by bob
|
|
371
|
+
for page in Page.objects.filter(id__in=[4, 13]).specific():
|
|
372
|
+
page.save_revision()
|
|
373
|
+
workflow.start(page, self.bob)
|
|
374
|
+
|
|
375
|
+
# Snippet workflow started by bob
|
|
376
|
+
for i in range(1, 3):
|
|
377
|
+
obj = FullFeaturedSnippet.objects.create(text=f"Some obj {i}")
|
|
378
|
+
obj.save_revision()
|
|
379
|
+
workflow.start(obj, self.bob)
|
|
380
|
+
|
|
381
|
+
self.dummy_request = get_dummy_request()
|
|
382
|
+
self.dummy_request.user = self.moderator
|
|
383
|
+
|
|
384
|
+
def test_panel_query_count(self):
|
|
385
|
+
panel = WorkflowObjectsToModeratePanel()
|
|
386
|
+
parent_context = {"request": self.dummy_request, "csrf_token": "dummy"}
|
|
387
|
+
# Warm up the cache
|
|
388
|
+
html = panel.render_html(parent_context)
|
|
389
|
+
|
|
390
|
+
with self.assertNumQueries(13):
|
|
391
|
+
html = panel.render_html(parent_context)
|
|
392
|
+
|
|
393
|
+
soup = self.get_soup(html)
|
|
394
|
+
self.assertEqual(len(soup.select('svg use[href="#icon-lock"]')), 2)
|
|
395
|
+
expected_titles = [
|
|
396
|
+
"Some obj 2",
|
|
397
|
+
"Some obj 1",
|
|
398
|
+
"Saint Patrick (single event)",
|
|
399
|
+
"Christmas",
|
|
400
|
+
"Steal underpants",
|
|
401
|
+
"Ameristralia Day",
|
|
402
|
+
]
|
|
403
|
+
titles = [e.get_text(strip=True) for e in soup.select(".title-wrapper a")]
|
|
404
|
+
self.assertEqual(titles, expected_titles)
|
|
@@ -9,7 +9,7 @@ class TestDbWhitelisterMethods(WagtailTestUtils, TestCase):
|
|
|
9
9
|
self.whitelister = EditorHTMLConverter().whitelister
|
|
10
10
|
|
|
11
11
|
def test_clean_tag_node_div(self):
|
|
12
|
-
soup = self.get_soup("<div>foo</div>"
|
|
12
|
+
soup = self.get_soup("<div>foo</div>")
|
|
13
13
|
tag = soup.div
|
|
14
14
|
self.assertEqual(tag.name, "div")
|
|
15
15
|
self.whitelister.clean_tag_node(soup, tag)
|
|
@@ -18,7 +18,6 @@ class TestDbWhitelisterMethods(WagtailTestUtils, TestCase):
|
|
|
18
18
|
def test_clean_tag_node_with_data_embedtype(self):
|
|
19
19
|
soup = self.get_soup(
|
|
20
20
|
'<p><a data-embedtype="image" data-id=1 data-format="left" data-alt="bar" irrelevant="baz">foo</a></p>',
|
|
21
|
-
"html5lib",
|
|
22
21
|
)
|
|
23
22
|
tag = soup.p
|
|
24
23
|
self.whitelister.clean_tag_node(soup, tag)
|
|
@@ -29,14 +28,13 @@ class TestDbWhitelisterMethods(WagtailTestUtils, TestCase):
|
|
|
29
28
|
def test_clean_tag_node_with_data_linktype(self):
|
|
30
29
|
soup = self.get_soup(
|
|
31
30
|
'<a data-linktype="document" data-id="1" irrelevant="baz">foo</a>',
|
|
32
|
-
"html5lib",
|
|
33
31
|
)
|
|
34
32
|
tag = soup.a
|
|
35
33
|
self.whitelister.clean_tag_node(soup, tag)
|
|
36
34
|
self.assertEqual(str(tag), '<a id="1" linktype="document">foo</a>')
|
|
37
35
|
|
|
38
36
|
def test_clean_tag_node(self):
|
|
39
|
-
soup = self.get_soup('<a irrelevant="baz">foo</a>'
|
|
37
|
+
soup = self.get_soup('<a irrelevant="baz">foo</a>')
|
|
40
38
|
tag = soup.a
|
|
41
39
|
self.whitelister.clean_tag_node(soup, tag)
|
|
42
40
|
self.assertEqual(str(tag), "<a>foo</a>")
|
|
@@ -52,7 +50,8 @@ class TestDbWhitelister(WagtailTestUtils, TestCase):
|
|
|
52
50
|
(necessary because we can't guarantee the order that attributes are output in)
|
|
53
51
|
"""
|
|
54
52
|
self.assertEqual(
|
|
55
|
-
self.get_soup(str1
|
|
53
|
+
self.get_soup(str1),
|
|
54
|
+
self.get_soup(str2),
|
|
56
55
|
)
|
|
57
56
|
|
|
58
57
|
def test_page_link_is_rewritten(self):
|
|
@@ -189,7 +189,7 @@ class TestGetFormForModel(TestCase):
|
|
|
189
189
|
self.assertIn("speakers", form.formsets)
|
|
190
190
|
self.assertNotIn("related_links", form.formsets)
|
|
191
191
|
|
|
192
|
-
def
|
|
192
|
+
def test_get_form_for_model_with_widget_overrides_by_class(self):
|
|
193
193
|
EventPageForm = get_form_for_model(
|
|
194
194
|
EventPage,
|
|
195
195
|
form_class=WagtailAdminPageForm,
|
|
@@ -201,7 +201,7 @@ class TestGetFormForModel(TestCase):
|
|
|
201
201
|
self.assertEqual(type(form.fields["date_from"]), forms.DateField)
|
|
202
202
|
self.assertEqual(type(form.fields["date_from"].widget), forms.PasswordInput)
|
|
203
203
|
|
|
204
|
-
def
|
|
204
|
+
def test_get_form_for_model_with_widget_overrides_by_instance(self):
|
|
205
205
|
EventPageForm = get_form_for_model(
|
|
206
206
|
EventPage,
|
|
207
207
|
form_class=WagtailAdminPageForm,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from django.test import TestCase
|
|
5
|
+
from django.test.client import Client
|
|
6
|
+
from django.urls import reverse
|
|
7
|
+
|
|
8
|
+
from wagtail.test.utils import WagtailTestUtils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestKeyboardShortcutsDialog(WagtailTestUtils, TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.login()
|
|
14
|
+
|
|
15
|
+
def test_keyboard_shortcuts_trigger_in_sidebar(self):
|
|
16
|
+
response = self.client.get(reverse("wagtailadmin_home"))
|
|
17
|
+
self.assertEqual(response.status_code, 200)
|
|
18
|
+
|
|
19
|
+
sidebar_data = (
|
|
20
|
+
self.get_soup(response.content)
|
|
21
|
+
.select_one("#wagtail-sidebar-props")
|
|
22
|
+
.contents[0]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
self.assertIn(
|
|
26
|
+
json.dumps(
|
|
27
|
+
{
|
|
28
|
+
"role": "button",
|
|
29
|
+
"data-a11y-dialog-show": "keyboard-shortcuts-dialog",
|
|
30
|
+
"data-action": "w-action#noop:prevent:stop",
|
|
31
|
+
"data-controller": "w-action",
|
|
32
|
+
}
|
|
33
|
+
),
|
|
34
|
+
sidebar_data,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def test_keyboard_shortcuts_dialog(self):
|
|
38
|
+
response = self.client.get(reverse("wagtailadmin_home"))
|
|
39
|
+
|
|
40
|
+
self.assertEqual(response.status_code, 200)
|
|
41
|
+
self.assertTemplateUsed(
|
|
42
|
+
response, "wagtailadmin/shared/keyboard_shortcuts_dialog.html"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
soup = self.get_soup(response.content)
|
|
46
|
+
|
|
47
|
+
# Check that the keyboard shortcuts dialog is present
|
|
48
|
+
shortcuts_dialog = soup.select_one("#keyboard-shortcuts-dialog")
|
|
49
|
+
self.assertIsNotNone(shortcuts_dialog)
|
|
50
|
+
|
|
51
|
+
# Check that the keyboard shortcuts dialog has basic accessible content
|
|
52
|
+
self.assertIn(
|
|
53
|
+
"All keyboard shortcuts", shortcuts_dialog.find("caption").prettify()
|
|
54
|
+
)
|
|
55
|
+
self.assertIn("Keyboard shortcut", shortcuts_dialog.find("thead").prettify())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestMacKeyboardShortcutsDialog(WagtailTestUtils, TestCase):
|
|
59
|
+
def setUp(self):
|
|
60
|
+
# Creates a client with a Mac user agent
|
|
61
|
+
self.client = Client(
|
|
62
|
+
headers={
|
|
63
|
+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
self.login()
|
|
67
|
+
|
|
68
|
+
def test_mac_useragent_and_behavior(self):
|
|
69
|
+
response = self.client.get(reverse("wagtailadmin_home"))
|
|
70
|
+
|
|
71
|
+
# Check that the user agent is a Mac
|
|
72
|
+
user_agent = response.context["request"].headers.get("User-Agent", "")
|
|
73
|
+
is_mac = re.search(r"Mac|iPod|iPhone|iPad", user_agent)
|
|
74
|
+
|
|
75
|
+
# Add assertions based on expected Mac behavior
|
|
76
|
+
self.assertTrue(is_mac)
|
|
77
|
+
|
|
78
|
+
# Check that the keyboard shortcuts dialog has Mac-specific content
|
|
79
|
+
soup = self.get_soup(response.content)
|
|
80
|
+
shortcuts_dialog = soup.select_one("#keyboard-shortcuts-dialog")
|
|
81
|
+
all_shortcuts = shortcuts_dialog.select("kbd")
|
|
82
|
+
for shortcut in all_shortcuts:
|
|
83
|
+
# All shortcuts should have the ⌘ symbol
|
|
84
|
+
self.assertIn("⌘", shortcut.prettify())
|