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 e8dc8a232e1..dc6d2060f7d 100644 --- a/cms/cms_toolbars.py +++ b/cms/cms_toolbars.py @@ -85,13 +85,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..14b1bf63dc5 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 RemovedInDjangoCMS60Warning -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.", + RemovedInDjangoCMS60Warning, + stacklevel=2, +) diff --git a/cms/wizards/views.py b/cms/wizards/views.py index ec11839ca55..306c12d6b31 100644 --- a/cms/wizards/views.py +++ b/cms/wizards/views.py @@ -15,7 +15,7 @@ from cms.utils.i18n import get_site_language_from_request from .forms import WizardStep1Form, WizardStep2BaseForm, step2_form_factory -from .wizard_pool import wizard_pool +from .wizard_base import get_entry class WizardCreateView(SessionWizardView): @@ -150,7 +150,7 @@ def done(self, form_list, **kwargs): def get_selected_entry(self): data = self.get_cleaned_data_for_step('0') - return wizard_pool.get_entry(data['entry']) + return get_entry(data['entry']) def get_origin_page(self): data = self.get_cleaned_data_for_step('0') diff --git a/cms/wizards/wizard_base.py b/cms/wizards/wizard_base.py index 0839cfa37e3..8fc9ec35cfc 100644 --- a/cms/wizards/wizard_base.py +++ b/cms/wizards/wizard_base.py @@ -13,6 +13,49 @@ ) +def get_entries(): + """ + Returns a list of Wizard objects (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 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 entry tuples of the form (wizard.id, wizard.title) 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) + + +def clear_wizard_cache(): + """ + Clears the wizard cache. This is useful if you have added or removed wizards + and want to ensure that the changes are reflected immediately. + """ + del apps.get_app_config('cms').cms_extension.wizards + + + class WizardBase: """ diff --git a/cms/wizards/wizard_pool.py b/cms/wizards/wizard_pool.py index cb2e0d31305..1ed43769ec3 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 RemovedInDjangoCMS60Warning +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.", + RemovedInDjangoCMS60Warning, + 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