From 6302e6051e6069f20bc4a8996f239fa08e893b43 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 22 May 2025 08:42:55 +0200 Subject: [PATCH 01/10] chore: Move from script tags containing dom elements to template tags (#8233) (#8237) * [5.1.0dev1 release process] Bumped version to 5.1.0dev1 * [5.1.0dev1 release process] compilemessages * [5.1.0dev1 release process] compiling new static files * [5.1.0dev1 release process] updating latest docs * Prepare new main branch * Fix typos * fix: No changes to changelog * fix: Remove unnecessary change to migration * Remove circular import * Fix import order * Re-introduce dummy `PlaceholderField` for legacy migrations * fix: Do not assume page url cache to be filled * fix: Structure board update sometimes failed to add all interactive elements (#8227) * fix: Scan plugin data after structure mode Xhr load * Fix test * fix js linting issues * fix: Update assets * fix: Empty plugin selectors in all but first placeholder * chore: Move from script tags containing dom elements to template tags * Update cms/models/fields.py * Update cms/models/fields.py --------- Co-authored-by: Github Release Action --- cms/plugin_rendering.py | 8 ++++---- .../frontend/unit/fixtures/plugin_child_classes.html | 4 ++-- cms/tests/test_placeholder.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cms/plugin_rendering.py b/cms/plugin_rendering.py index d2965f61931..fae1bd780e4 100644 --- a/cms/plugin_rendering.py +++ b/cms/plugin_rendering.py @@ -764,9 +764,9 @@ def _preload_placeholders_for_page(self, page, slots=None, inherit=False): class StructureRenderer(BaseRenderer): load_structure = True placeholder_edit_template = """ - + {plugin_js}{placeholder_js} """ @@ -853,9 +853,9 @@ class LegacyRenderer(ContentRenderer): placeholder_edit_template = """ {content}
- + {plugin_js}{placeholder_js} """ diff --git a/cms/tests/frontend/unit/fixtures/plugin_child_classes.html b/cms/tests/frontend/unit/fixtures/plugin_child_classes.html index 3c97d9015cf..64e5c2de1f5 100644 --- a/cms/tests/frontend/unit/fixtures/plugin_child_classes.html +++ b/cms/tests/frontend/unit/fixtures/plugin_child_classes.html @@ -1,4 +1,4 @@ - + diff --git a/cms/tests/test_placeholder.py b/cms/tests/test_placeholder.py index 33764ba4110..c0c14de0199 100644 --- a/cms/tests/test_placeholder.py +++ b/cms/tests/test_placeholder.py @@ -1023,7 +1023,7 @@ def test_sets_source_when_external_object_is_rendered(self): for placeholder in declared_placeholders: self.assertContains( response, - '""" - def get_context(self, name, value, attrs): self._build_widgets() context = super().get_context(name, value, attrs) - context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs']) + context['widget']['script_data'] = {"name": name} return context def format_output(self, rendered_widgets): @@ -122,28 +111,17 @@ def get_ajax_url(self, ajax_view): 'You should provide an ajax_view argument that can be reversed to the PageSmartLinkWidget' ) - def _build_script(self, name, value, attrs={}): - return r"""""".format( - element_id=attrs.get('id', ''), - placeholder_text=attrs.get('placeholder_text', ''), - language_code=self.language, - ajax_url=force_str(self.ajax_url) - ) + def _build_script_data(self, name, value, attrs): + return { + "id": attrs.get('id', ''), + "text": str(attrs.get('placeholder_text', '')), + "lang": self.language, + "url": force_str(self.ajax_url), + } def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs']) + context['widget']['script_data'] = self._build_script_data(name, value, context['widget']['attrs']) return context @@ -220,10 +198,10 @@ class ApplicationConfigSelect(Select): Special widget -populate by javascript- that shows application configurations depending on selected Apphooks. - Required data are injected in the page as javascript data that cms.app_hook_select.js + Required data are injected in the page as JSON data that forms.apphookselect.js uses to create the appropriate data structure. - A stub 'add-another' link is created and filled in with the correct URL by the same + A stub 'addlink' link is created and filled in with the correct URL by the same javascript. """ template_name = 'cms/widgets/applicationconfigselect.html' @@ -237,29 +215,23 @@ def __init__(self, attrs=None, choices=(), app_configs={}): self.app_configs = app_configs super().__init__(attrs, choices) - def _build_script(self, name, value, attrs={}): - configs = [] - urls = [] - for application, cms_app in self.app_configs.items(): - configs.append("'{}': [{}]".format(application, ",".join( - ["['{}', '{}']".format(config.pk, escapejs(escape(config))) for config in cms_app.get_configs()]))) # noqa - for application, cms_app in self.app_configs.items(): - urls.append(f"'{application}': '{cms_app.get_config_add_url()}'") - return r"""""".format( - apphooks_configurations=','.join(configs), - apphooks_url=','.join(urls), - apphooks_value=value, - ) + def _build_script_data(self, name, value, attrs): + configs = { + str(application): [[str(config.pk), str(config)] for config in cms_app.get_configs()] + for application, cms_app in self.app_configs.items() + } + urls = { + str(application): cms_app.get_config_add_url() + for application, cms_app in self.app_configs.items() + } + + return { + "apphooks_configuration": configs, + "apphooks_configuration_url": urls, + "apphooks_configuration_value": value, + } def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs']) + context['widget']['script_data'] = self._build_script_data(name, value, context['widget']['attrs']) return context diff --git a/cms/static/cms/js/modules/cms.changeform.js b/cms/static/cms/js/modules/cms.changeform.js index a4b1913bb14..0371419cf22 100644 --- a/cms/static/cms/js/modules/cms.changeform.js +++ b/cms/static/cms/js/modules/cms.changeform.js @@ -19,6 +19,10 @@ $(function() { $(this).parent('.form-row').hide(); }); + $('#page_form_lang_tabs .language_button').on('click', function() { + CMS.API.changeLanguage(this.dataset.adminUrl); + }); + // public api for changing the language tabs // used in admin/cms/page/change_form.html window.CMS.API.changeLanguage = function(url) { diff --git a/cms/static/cms/js/widgets/forms.apphookselect.js b/cms/static/cms/js/widgets/forms.apphookselect.js index 6ab08446fbe..e2d7c1278ba 100644 --- a/cms/static/cms/js/widgets/forms.apphookselect.js +++ b/cms/static/cms/js/widgets/forms.apphookselect.js @@ -11,7 +11,18 @@ __webpack_public_path__ = require('../modules/get-dist-path')('bundle.forms.apph // APP HOOK SELECT require.ensure([], function (require) { var $ = require('jquery'); - var apphooks_configuration = window.apphooks_configuration || {}; + var apphookData = { + apphooks_configuration: {}, + apphooks_configuration_value: undefined, + apphooks_configuration_url: {} + }; + var dataElement = document.querySelector('script[data-cms-widget-applicationconfigselect]'); + + if (dataElement) { + apphookData = JSON.parse(dataElement.querySelector('script').textContent); + } + + var apphooks_configuration = apphookData.apphooks_configuration || {}; // shorthand for jQuery(document).ready(); $(function () { @@ -33,7 +44,7 @@ require.ensure([], function (require) { for (var i = 0; i < apphooks_configuration[opt.val()].length; i++) { var selectedCfgs = ''; - if (apphooks_configuration[opt.val()][i][0] === window.apphooks_configuration_value) { + if (apphooks_configuration[opt.val()][i][0] === apphookData.apphooks_configuration_value) { selectedCfgs = 'selected="selected"'; } appCfgs.append( @@ -42,11 +53,14 @@ require.ensure([], function (require) { '' ); } - appCfgsAdd.attr('href', window.apphooks_configuration_url[opt.val()] + + appCfgsAdd.attr('href', apphookData.apphooks_configuration_url[opt.val()] + // Here we check if we are on django>=1.8 by checking if the method introduced in that version // exists, and if it does - we add `_popup` ourselves, because otherwise the popup with // apphook creation form will not be dismissed correctly (window.showRelatedObjectPopup ? '?_popup=1' : '')); + appCfgsAdd.on('click', function () { + window.showAddAnotherPopup(this); + }); appCfgsRow.removeClass('hidden'); appNsRow.addClass('hidden'); } else { diff --git a/cms/static/cms/js/widgets/forms.pageselectwidget.js b/cms/static/cms/js/widgets/forms.pageselectwidget.js index 33ea3c20b60..5891e285682 100644 --- a/cms/static/cms/js/widgets/forms.pageselectwidget.js +++ b/cms/static/cms/js/widgets/forms.pageselectwidget.js @@ -82,11 +82,11 @@ require.ensure( // init $(function() { - if (window.CMS.Widgets !== undefined && window.CMS.Widgets._pageSelectWidgets !== undefined) { - window.CMS.Widgets._pageSelectWidgets.forEach(function (widget) { - new PageSelectWidget(widget); - }); - } + document.querySelectorAll('[data-cms-widget-pageselect]').forEach(function (el) { + var widget = JSON.parse(el.querySelector('script').textContent); + + new PageSelectWidget(widget); + }); }); }, 'admin.widget' diff --git a/cms/static/cms/js/widgets/forms.pagesmartlinkwidget.js b/cms/static/cms/js/widgets/forms.pagesmartlinkwidget.js index 0c184c32d4a..932740e4f50 100644 --- a/cms/static/cms/js/widgets/forms.pagesmartlinkwidget.js +++ b/cms/static/cms/js/widgets/forms.pagesmartlinkwidget.js @@ -95,10 +95,10 @@ require.ensure([], function (require) { window.CMS.PageSmartLinkWidget = PageSmartLinkWidget; $(function () { - if (window.CMS.Widgets !== undefined && window.CMS.Widgets._pageSmartLinkWidgets !== undefined) { - window.CMS.Widgets._pageSmartLinkWidgets.forEach(function (widget) { - new PageSmartLinkWidget(widget); - }); - } + document.querySelectorAll('[data-cms-widget-pagesmartlinkwidget]').forEach(function (el) { + var widget = JSON.parse(el.querySelector('script').textContent); + + new PageSmartLinkWidget(widget); + }); }); }, 'admin.widget'); diff --git a/cms/templates/admin/cms/page/change_form.html b/cms/templates/admin/cms/page/change_form.html index 6de5ea819cc..6a231404b5b 100644 --- a/cms/templates/admin/cms/page/change_form.html +++ b/cms/templates/admin/cms/page/change_form.html @@ -43,7 +43,7 @@ {% if show_language_tabs and not show_permissions %}
{% for lang_code, lang_name in language_tabs %} - {% endfor %} diff --git a/cms/templates/cms/widgets/applicationconfigselect.html b/cms/templates/cms/widgets/applicationconfigselect.html index a0874493954..28ec95ebba9 100644 --- a/cms/templates/cms/widgets/applicationconfigselect.html +++ b/cms/templates/cms/widgets/applicationconfigselect.html @@ -1,4 +1,6 @@ -{% load i18n static %} +{% load i18n %} {% include 'django/forms/widgets/select.html' %} -{{ widget.script_init|safe }} - + + diff --git a/cms/templates/cms/widgets/pageselectwidget.html b/cms/templates/cms/widgets/pageselectwidget.html index db16509a92c..3cbd3c2dcdc 100644 --- a/cms/templates/cms/widgets/pageselectwidget.html +++ b/cms/templates/cms/widgets/pageselectwidget.html @@ -1,2 +1,4 @@ {% include 'django/forms/widgets/multiwidget.html' %} -{{ widget.script_init|safe }} + diff --git a/cms/templates/cms/widgets/pagesmartlinkwidget.html b/cms/templates/cms/widgets/pagesmartlinkwidget.html index 81514a118d8..411e438ed5e 100644 --- a/cms/templates/cms/widgets/pagesmartlinkwidget.html +++ b/cms/templates/cms/widgets/pagesmartlinkwidget.html @@ -1,2 +1,4 @@ {% include 'django/forms/widgets/text.html' %} -{{ widget.script_init|safe }} + diff --git a/cms/tests/test_forms.py b/cms/tests/test_forms.py index 251a3da1a99..7581effea38 100644 --- a/cms/tests/test_forms.py +++ b/cms/tests/test_forms.py @@ -232,12 +232,7 @@ def get_config_add_url(self): app_config_select = ApplicationConfigSelect(app_configs=app_configs) output = app_config_select.render("application_configurations", 1) self.assertFalse('' in output) - self.assertTrue( - "\\u0026lt\\u003Bscript\\u0026gt\\u003Balert(" - "\\u0026quot\\u003Bbad\\u002Dstuff\\u0026quot" - "\\u003B)\\u003B\\u0026lt\\u003B/script\\u0026gt" - "\\u003B" in output - ) + self.assertTrue('\\u003Cscript\\u003Ealert(\\"bad-stuff\\");\\u003C/script\\u003E' in output) def test_move_page_form(self): """Test the MovePageForm validation and behavior""" From 6327e0f9663da417d7802755da314909ad8dfbdd Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Wed, 18 Jun 2025 13:12:34 +0200 Subject: [PATCH 07/10] chore: Add hash to `FuzzyInt` test class to satisfy ruff (#8259) --- cms/test_utils/util/fuzzy_int.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cms/test_utils/util/fuzzy_int.py b/cms/test_utils/util/fuzzy_int.py index 781dc1b7dcb..ed2b58932af 100644 --- a/cms/test_utils/util/fuzzy_int.py +++ b/cms/test_utils/util/fuzzy_int.py @@ -1,5 +1,3 @@ - - class FuzzyInt(int): def __new__(cls, lowest, highest): obj = super().__new__(cls, highest) @@ -12,3 +10,7 @@ def __eq__(self, other): def __repr__(self): return "[%d..%d]" % (self.lowest, self.highest) + + def __hash__(self): + # Combine the hash of the lowest and highest attributes, ensuring hash consistency. + return hash((self.lowest, self.highest)) From ec2f4e8155f69d44da2bab67482391eea97fdc04 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Tue, 24 Jun 2025 22:12:13 +0200 Subject: [PATCH 08/10] fix: Apphook widget detection (#8263) * fix: Apphook widget detection * Fix: Add related --- .../cms/js/widgets/forms.apphookselect.js | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/cms/static/cms/js/widgets/forms.apphookselect.js b/cms/static/cms/js/widgets/forms.apphookselect.js index e2d7c1278ba..38a2a9e2c90 100644 --- a/cms/static/cms/js/widgets/forms.apphookselect.js +++ b/cms/static/cms/js/widgets/forms.apphookselect.js @@ -10,39 +10,40 @@ __webpack_public_path__ = require('../modules/get-dist-path')('bundle.forms.apph // ############################################################################# // APP HOOK SELECT require.ensure([], function (require) { - var $ = require('jquery'); - var apphookData = { + const $ = require('jquery'); + let apphookData = { apphooks_configuration: {}, apphooks_configuration_value: undefined, apphooks_configuration_url: {} }; - var dataElement = document.querySelector('script[data-cms-widget-applicationconfigselect]'); - - if (dataElement) { - apphookData = JSON.parse(dataElement.querySelector('script').textContent); - } - - var apphooks_configuration = apphookData.apphooks_configuration || {}; // shorthand for jQuery(document).ready(); $(function () { - var appHooks = $('#application_urls, #id_application_urls'); - var selected = appHooks.find('option:selected'); - var appNsRow = $('.form-row.application_namespace, .form-row.field-application_namespace'); - var appNs = appNsRow.find('#application_namespace, #id_application_namespace'); - var appCfgsRow = $('.form-row.application_configs, .form-row.field-application_configs'); - var appCfgs = appCfgsRow.find('#application_configs, #id_application_configs'); - var appCfgsAdd = appCfgsRow.find('#add_application_configs'); - var original_ns = appNs.val(); + const dataElement = document.querySelector('div[data-cms-widget-applicationconfigselect]'); + + if (dataElement) { + apphookData = JSON.parse(dataElement.querySelector('script').textContent); + } + + const apphooks_configuration = apphookData.apphooks_configuration || {}; + + const appHooks = $('#application_urls, #id_application_urls'); + const selected = appHooks.find('option:selected'); + const appNsRow = $('.form-row.application_namespace, .form-row.field-application_namespace'); + const appNs = appNsRow.find('#application_namespace, #id_application_namespace'); + const appCfgsRow = $('.form-row.application_configs, .form-row.field-application_configs'); + const appCfgs = appCfgsRow.find('#application_configs, #id_application_configs'); + const appCfgsAdd = appCfgsRow.find('#add_application_configs'); + const original_ns = appNs.val(); // Shows / hides namespace / config selection widgets depending on the user input appHooks.setupNamespaces = function () { - var opt = $(this).find('option:selected'); + const opt = $(this).find('option:selected'); if ($(appCfgs).length > 0 && apphooks_configuration[opt.val()]) { appCfgs.html(''); - for (var i = 0; i < apphooks_configuration[opt.val()].length; i++) { - var selectedCfgs = ''; + for (let i = 0; i < apphooks_configuration[opt.val()].length; i++) { + let selectedCfgs = ''; if (apphooks_configuration[opt.val()][i][0] === apphookData.apphooks_configuration_value) { selectedCfgs = 'selected="selected"'; @@ -58,7 +59,8 @@ require.ensure([], function (require) { // exists, and if it does - we add `_popup` ourselves, because otherwise the popup with // apphook creation form will not be dismissed correctly (window.showRelatedObjectPopup ? '?_popup=1' : '')); - appCfgsAdd.on('click', function () { + appCfgsAdd.on('click', function (ev) { + ev.preventDefault(); window.showAddAnotherPopup(this); }); appCfgsRow.removeClass('hidden'); From 3651858481c7375d6faf717fb3660c975062f472 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sat, 28 Jun 2025 15:52:35 +0200 Subject: [PATCH 09/10] fix: Allow lazy wizard initialization (#8265) * fix: Allow lazy wizard initialization * Update cms/cms_toolbars.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update cms/cms_config.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- cms/cms_config.py | 35 +++++++++++++++++++--------- cms/cms_toolbars.py | 10 +++----- cms/tests/test_cms_config_wizards.py | 20 +++------------- cms/tests/test_wizards.py | 7 +++++- cms/wizards/helpers.py | 32 +++++++------------------ cms/wizards/wizard_base.py | 34 +++++++++++++++++++++++++++ cms/wizards/wizard_pool.py | 23 ++++++++---------- docs/reference/wizards.rst | 12 ++++------ 8 files changed, 93 insertions(+), 80 deletions(-) diff --git a/cms/cms_config.py b/cms/cms_config.py index 91cdd4066bb..c9c01441159 100644 --- a/cms/cms_config.py +++ b/cms/cms_config.py @@ -1,4 +1,5 @@ from collections.abc import Iterable +from functools import cached_property from logging import getLogger from django.core.exceptions import ImproperlyConfigured @@ -19,9 +20,8 @@ class CMSCoreConfig(CMSAppConfig): class CMSCoreExtensions(CMSAppExtension): - def __init__(self): - self.wizards = {} + self.lazy_wizards = [] self.toolbar_enabled_models = {} self.model_groupers = {} self.toolbar_mixins = [] @@ -33,17 +33,30 @@ def configure_wizards(self, cms_config): """ if not isinstance(cms_config.cms_wizards, Iterable): raise ImproperlyConfigured("cms_wizards must be iterable") - for wizard in cms_config.cms_wizards: - if not isinstance(wizard, Wizard): + + self.lazy_wizards.append(cms_config.cms_wizards) + + @cached_property + def wizards(self) -> dict[str, Wizard]: + """ + Returns a dictionary of wizard instances keyed by their unique IDs. + Iterates over all iterables in `self.lazy_wizards`, filters out objects that are instances + of the `Wizard` class, and constructs a dictionary where each key is the wizard's `id` + and the value is the corresponding `Wizard` instance. + Returns: + dict: A dictionary mapping wizard IDs to `Wizard` instances. + """ + + wizards = {} + for iterable in self.lazy_wizards: + new_wizards = {wizard.id: wizard for wizard in iterable} + if wizard := next((wizard for wizard in new_wizards.values() if not isinstance(wizard, Wizard)), None): + # If any wizard in the iterable is not an instance of Wizard, raise an exception raise ImproperlyConfigured( - "All wizards defined in cms_wizards must inherit " - "from cms.wizards.wizard_base.Wizard" + f"cms_wizards must be iterable of Wizard instances, got {type(wizard)}" ) - elif wizard.id in self.wizards: - msg = f"Wizard for model {wizard.get_model()} has already been registered" - logger.warning(msg) - else: - self.wizards[wizard.id] = wizard + wizards |= new_wizards + return wizards def configure_toolbar_enabled_models(self, cms_config): if not isinstance(cms_config.cms_toolbar_enabled_models, Iterable): diff --git a/cms/cms_toolbars.py b/cms/cms_toolbars.py index f87b5b6dc10..73481daee0b 100644 --- a/cms/cms_toolbars.py +++ b/cms/cms_toolbars.py @@ -86,13 +86,9 @@ def add_wizard_button(self): from cms.wizards.wizard_pool import entry_choices title = _("Create") - if self.page: - user = self.request.user - page_pk = self.page.pk - disabled = len(list(entry_choices(user, self.page))) == 0 - else: - page_pk = '' - disabled = True + user = self.request.user + page_pk = self.page.pk if self.page else "" + disabled = not list(entry_choices(user, self.page)) url = '{url}?page={page}&language={lang}&edit'.format( url=admin_reverse("cms_wizard_create"), diff --git a/cms/tests/test_cms_config_wizards.py b/cms/tests/test_cms_config_wizards.py index f446892a9b0..09421d4d041 100644 --- a/cms/tests/test_cms_config_wizards.py +++ b/cms/tests/test_cms_config_wizards.py @@ -49,8 +49,10 @@ def test_raises_exception_if_doesnt_inherit_from_wizard_class(self): wizard = Mock(id=3, spec=object) cms_config = Mock( cms_enabled=True, cms_wizards=[wizard]) + extensions.configure_wizards(cms_config) with self.assertRaises(ImproperlyConfigured): - extensions.configure_wizards(cms_config) + extensions.wizards + def test_raises_exception_if_not_iterable(self): """ @@ -63,22 +65,6 @@ def test_raises_exception_if_not_iterable(self): with self.assertRaises(ImproperlyConfigured): extensions.configure_wizards(cms_config) - @patch('cms.cms_config.logger.warning') - def test_warning_if_registering_the_same_wizard_twice(self, mocked_logger): - """ - If a wizard is already added to the dict log a warning. - """ - extensions = CMSCoreExtensions() - wizard = Mock(id=81, spec=Wizard) - cms_config = Mock( - cms_enabled=True, cms_wizards=[wizard, wizard]) - extensions.configure_wizards(cms_config) - warning_msg = f"Wizard for model {wizard.get_model()} has already been registered" - # Warning message displayed - mocked_logger.assert_called_once_with(warning_msg) - # wizards dict is still what we expect it to be - self.assertDictEqual(extensions.wizards, {81: wizard}) - class ConfigureWizardsIntegrationTestCase(CMSTestCase): diff --git a/cms/tests/test_wizards.py b/cms/tests/test_wizards.py index 681f2087690..6af83b3baab 100644 --- a/cms/tests/test_wizards.py +++ b/cms/tests/test_wizards.py @@ -219,7 +219,9 @@ def tearDown(self): # Clean up in case anything has been removed or added to the # registered wizards, so other tests don't have problems extension = apps.get_app_config('cms').cms_extension - extension.wizards = {} + # Reset the cached property 'wizards' + if hasattr(extension, 'wizards'): + del extension.wizards configs_with_wizards = [ app.cms_config for app in app_registration.get_cms_config_apps() if hasattr(app.cms_config, 'cms_wizards') @@ -235,6 +237,9 @@ def test_is_registered_for_registered_wizard(self): Test for backwards compatibility of is_registered when checking a registered wizard. """ + from django.apps import apps + + self.assertTrue(apps.get_app_config("cms").cms_extension.wizards) is_registered = wizard_pool.is_registered(cms_page_wizard) self.assertTrue(is_registered) diff --git a/cms/wizards/helpers.py b/cms/wizards/helpers.py index 3dd56d8fa78..38133425521 100644 --- a/cms/wizards/helpers.py +++ b/cms/wizards/helpers.py @@ -1,26 +1,12 @@ -from django.apps import apps +import warnings +from cms.utils.compat.warnings import RemovedInDjangoCMS51Warning -def get_entries(): - """ - Returns a list of (wizard.id, wizard) tuples (for all registered - wizards) ordered by weight +from .wizard_base import get_entries, get_entry # noqa: F401 - ``get_entries()`` is useful if it is required to have a list of all registered - wizards. Typically, this is used to iterate over them all. Note that they will - be returned in the order of their ``weight``: smallest numbers for weight are - returned first.:: - - for wizard_id, wizard in get_entries(): - # do something with a wizard... - """ - wizards = apps.get_app_config('cms').cms_extension.wizards - return [value for (key, value) in sorted( - wizards.items(), key=lambda e: e[1].weight)] - - -def get_entry(entry_key): - """ - Returns a wizard object based on its :attr:`~.cms.wizards.wizard_base.Wizard.id`. - """ - return apps.get_app_config('cms').cms_extension.wizards[entry_key] +warnings.warn( + "The cms.wizards.helpers module is deprecated and will be removed in django CMS 5.1. " + "Use cms.wizards.wizard_base.get_entries and cms.wizards.wizard_pool.get_entry instead.", + RemovedInDjangoCMS51Warning, + stacklevel=2, +) diff --git a/cms/wizards/wizard_base.py b/cms/wizards/wizard_base.py index 0839cfa37e3..ac2219ae37c 100644 --- a/cms/wizards/wizard_base.py +++ b/cms/wizards/wizard_base.py @@ -13,6 +13,40 @@ ) +def get_entries(): + """ + Returns a list of (wizard.id, wizard) tuples (for all registered + wizards) ordered by weight + + ``get_entries()`` is useful if it is required to have a list of all registered + wizards. Typically, this is used to iterate over them all. Note that they will + be returned in the order of their ``weight``: smallest numbers for weight are + returned first.:: + + for wizard_id, wizard in get_entries(): + # do something with a wizard... + """ + wizards = apps.get_app_config('cms').cms_extension.wizards + return sorted(wizards.values(), key=lambda e: e.weight) + + +def get_entry(entry_key): + """ + Returns a wizard object based on its :attr:`~.cms.wizards.wizard_base.Wizard.id`. + """ + return apps.get_app_config('cms').cms_extension.wizards[entry_key] + + +def entry_choices(user, page): + """ + Yields a list of wizard entries that the current user can use based on their + permission to add instances of the underlying model objects. + """ + for entry in get_entries(): + if entry.user_has_add_permission(user, page=page): + yield (entry.id, entry.title) + + class WizardBase: """ diff --git a/cms/wizards/wizard_pool.py b/cms/wizards/wizard_pool.py index cb2e0d31305..4a669ec8362 100644 --- a/cms/wizards/wizard_pool.py +++ b/cms/wizards/wizard_pool.py @@ -1,24 +1,15 @@ from django.apps import apps from django.utils.translation import gettext as _ -from cms.wizards.helpers import get_entries, get_entry -from cms.wizards.wizard_base import Wizard +from cms.utils.compat.warnings import RemovedInDjangoCMS51Warning +from cms.wizards.helpers import get_entries, get_entry # noqa: F401 +from cms.wizards.wizard_base import Wizard, entry_choices # noqa: F401 class AlreadyRegisteredException(Exception): pass -def entry_choices(user, page): - """ - Yields a list of wizard entries that the current user can use based on their - permission to add instances of the underlying model objects. - """ - for entry in get_entries(): - if entry.user_has_add_permission(user, page=page): - yield (entry.id, entry.title) - - class WizardPool: """ .. deprecated:: 4.0 @@ -49,7 +40,13 @@ def register(self, entry): name. In this case, the register method will raise a ``cms.wizards.wizard_pool.AlreadyRegisteredException``. """ - # TODO: Add deprecation warning + import warnings + + warnings.warn( + "Using wizard_pool is deprecated. Use the cms_config instead.", + RemovedInDjangoCMS51Warning, + stacklevel=2, + ) assert isinstance(entry, Wizard), "entry must be an instance of Wizard" if self.is_registered(entry, passive=True): model = entry.get_model() diff --git a/docs/reference/wizards.rst b/docs/reference/wizards.rst index aba8d953faf..348ba4821f6 100644 --- a/docs/reference/wizards.rst +++ b/docs/reference/wizards.rst @@ -39,7 +39,7 @@ When instantiating a Wizard object, use the keywords: CMS will create one from the pattern: "Create a new «model.verbose_name» instance." :edit_mode_on_success: Whether the user will get redirected to object edit url after a - successful creation or not. This only works if the object is registered + successful creation or not. This only works if the object is registered for toolbar enabled models. @@ -78,17 +78,13 @@ Wizard class :members: :inherited-members: - -******* -Helpers -******* - -.. module:: cms.wizards.helpers - .. autofunction:: get_entry .. autofunction:: get_entries +.. autofunction:: entry_choices + + *********** wizard_pool From 8b58e3d07bfb60a1ea64e59c892a5c24e6e20078 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sat, 28 Jun 2025 19:07:11 +0200 Subject: [PATCH 10/10] Update wizard_pool.py (#8268) --- cms/wizards/wizard_pool.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cms/wizards/wizard_pool.py b/cms/wizards/wizard_pool.py index 4a669ec8362..a125911e73c 100644 --- a/cms/wizards/wizard_pool.py +++ b/cms/wizards/wizard_pool.py @@ -2,8 +2,7 @@ from django.utils.translation import gettext as _ from cms.utils.compat.warnings import RemovedInDjangoCMS51Warning -from cms.wizards.helpers import get_entries, get_entry # noqa: F401 -from cms.wizards.wizard_base import Wizard, entry_choices # noqa: F401 +from cms.wizards.wizard_base import Wizard, entry_choices, get_entries, get_entry # noqa: F401 class AlreadyRegisteredException(Exception):