Skip to content

fix: Allow lazy wizard initialization #8267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cms/admin/pageadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class PageAdmin(admin.ModelAdmin):
move_form = MovePageForm
inlines = PERMISSION_ADMIN_INLINES
title_frontend_editable_fields = ['title', 'menu_title', 'page_title']
search_fields = ('=id', 'urls__slug', 'pagecontent_set__title', 'reverse_id')

def has_add_permission(self, request):
return False
Expand Down
14 changes: 11 additions & 3 deletions cms/admin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,16 +405,24 @@ def get_grouping_from_request(self, request: HttpRequest) -> None:
@property
def current_content_filters(self) -> typing.Dict[str, typing.Any]:
"""Filters needed to get the correct content model instance"""
return {field: getattr(self, field) for field in self.extra_grouping_fields}
return {field: getattr(self, field, self.get_extra_grouping_field(field)) for field in self.extra_grouping_fields}

def get_language(self) -> str:
"""Hook on how to get the current language. By default, Django provides it."""
return get_language()
"""Hook on how to get the current language. By default, if it is set as a
property, use the property, otherwise let Django provide it."""
return getattr(self, "language", get_language())

def get_language_tuple(self) -> typing.Tuple[typing.Tuple[str, str], ...]:
"""Hook on how to get all available languages for the language selector."""
return get_language_tuple()

def get_extra_grouping_field(self, field):
"""Retrieves the current value for grouping fields - by default by calling self.get_<field>, e.g.,
self.get_language(). If those are not implemented, this method will fail."""
if callable(getattr(self, f"get_{field}", None)):
return getattr(self, f"get_{field}")()
raise ValueError("Cannot get extra grouping field")

def get_changelist(self, request: HttpRequest, **kwargs) -> type:
"""Allow for extra grouping fields as a non-filter parameter"""
return type(
Expand Down
6 changes: 5 additions & 1 deletion cms/forms/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,9 @@ def validate_url_uniqueness(site, path, language, user_language=None, exclude_pa
message = gettext('Page %(conflict_page)s has the same url \'%(url)s\' as current page "%(instance)s".')
else:
message = gettext('Page %(conflict_page)s has the same url \'%(url)s\' as current page.')
message = message % {'conflict_page': conflict_url, 'url': path, 'instance': exclude_page}
message = message % {
'conflict_page': conflict_url,
'url': path,
'instance': exclude_page.get_title(language) if exclude_page else ''
}
raise ValidationError(mark_safe(message))
3 changes: 3 additions & 0 deletions cms/test_utils/util/fuzzy_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ def __eq__(self, other):

def __repr__(self):
return "[%d..%d]" % (self.lowest, self.highest)

def __hash__(self):
return hash((self.lowest, self.highest))
2 changes: 1 addition & 1 deletion cms/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def test_search_fields(self):
if not admin_instance.search_fields:
continue
url = admin_reverse('cms_%s_changelist' % model._meta.model_name)
response = self.client.get('%s?q=1' % url)
response = self.client.get('%s?q=1' % url, follow=True) # Page redirects to PageContent
errmsg = response.content
self.assertEqual(response.status_code, 200, errmsg)

Expand Down
36 changes: 2 additions & 34 deletions cms/tests/test_grouper_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.contrib.admin import site
from django.templatetags.static import static
from django.utils.crypto import get_random_string
from django.utils.translation import get_language, override as force_language

from cms.admin.utils import CONTENT_PREFIX
from cms.test_utils.project.sampleapp.models import (
Expand Down Expand Up @@ -177,7 +178,7 @@ def test_extra_grouping_field_fixed(self):
current_content_filters = self.admin.current_content_filters

self.assertEqual(admin_language, expected_language)
self.assertEqual(current_content_filters["language"], expected_language)
self.assertEqual(current_content_filters["language"], expected_language)

def test_extra_grouping_field_current(self):
"""Extra grouping fields (language) when not set return current default correctly"""
Expand All @@ -190,39 +191,6 @@ def test_extra_grouping_field_current(self):
self.assertEqual(admin_language, expected_language)
self.assertEqual(current_content_filters["language"], expected_language)

def test_prepopulated_fields_pass_checks(self):
"""Prepopulated fields work for content field"""
# Arrange
admin = copy.copy(self.admin)
admin.prepopulated_fields = dict(
category_name=["category_name"], # Both key and value from GrouperModel
some_field=["content__secret_greeting"], # Value from ContentModel
content__secret_greeting=["category_name"], # Key from GrouperModel
content__region=["content__secret_greeting"], # Both key and value from ContentModel
)

# Act
check_results = admin.check()

# Assert
self.assertEqual(check_results, []) # No errors

def test_invalid_prepopulated_content_fields_fail_checks(self):
"""Prepopulated fields with invalid content field names fail checks"""
# Arrange
admin = copy.copy(self.admin)
admin.prepopulated_fields = dict(
some_field=["content__public_greeting"], # Value from ContentModel: 1 error
content__public_greeting=["category_name"], # Key from GrouperModel: 1 error
content__country=["content__public_greeting"], # Both key and value from ContentModel: 2 errors
)

# Act
check_results = admin.check()

# Assert
self.assertEqual(len(check_results), 4) # No errors


class GrouperChangeListTestCase(SetupMixin, CMSTestCase):
def test_language_selector(self):
Expand Down
5 changes: 4 additions & 1 deletion cms/tests/test_log_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from cms.models import Page, Placeholder, UserSettings
from cms.test_utils.testcases import URL_CMS_PAGE_MOVE, CMSTestCase
from cms.utils import get_current_site
from cms.utils.i18n import force_language
from cms.wizards.forms import WizardStep2BaseForm, step2_form_factory

# Snippet to create wizard page taken from: test_wizards.py
Expand Down Expand Up @@ -214,6 +215,8 @@ def test_log_for_delete_translation(self):
title_en = "page_a"
page = create_page(title_en, "nav_playground.html", "en")
create_page_content(language='de', title="other title %s" % title_en, page=page)
with force_language("de"): # The remaining language
expected_entry = str(page)
endpoint = self.get_page_delete_translation_uri('en', page)
post_data = {'post': 'yes', 'language': 'en'}

Expand All @@ -233,7 +236,7 @@ def test_log_for_delete_translation(self):
# Check the object id is set correctly
self.assertEqual(str(page.pk), log_entry.object_id)
# Check the object_repr is set correctly
self.assertEqual(str(page), log_entry.object_repr)
self.assertEqual(expected_entry, log_entry.object_repr)
# Check that the correct user created the log
self.assertEqual(self._admin_user.pk, log_entry.user_id)

Expand Down
2 changes: 1 addition & 1 deletion cms/utils/placeholder.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from sekizai.helpers import get_varname

from cms.exceptions import DuplicatePlaceholderWarning
from cms.models import Placeholder
from cms.models import EmptyPageContent, Placeholder
from cms.utils.conf import get_cms_setting

RANGE_START = 128
Expand Down
4 changes: 2 additions & 2 deletions cms/wizards/helpers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import warnings

from cms.utils.compat.warnings import RemovedInDjangoCMS60Warning
from cms.utils.compat.warnings import RemovedInDjangoCMS50Warning

from .wizard_base import get_entries, get_entry # noqa: F401

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,
RemovedInDjangoCMS50Warning,
stacklevel=2,
)
3 changes: 1 addition & 2 deletions cms/wizards/wizard_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from django.utils.translation import gettext as _

from cms.utils.compat.warnings import RemovedInDjangoCMS50Warning
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):
Expand Down
Loading
Loading