8000 fix: Language chooser options pointing to the same language (#7698) · django-cms/django-cms@2d17eaa · GitHub
[go: up one dir, main page]

Skip to content

Commit 2d17eaa

Browse files
authored
fix: Language chooser options pointing to the same language (#7698)
* Fix: Language chooser * Ensure page language uniqueness * Fix linter issue * Add edit mode and preview mode to language chooser * Add option to register grouper field for frontend-editable models * Undo changes to cms_toolbars to avoid code redundancy * Simplify toolbar utils. * Remove not util function * Remove unneeded imports * fix: allow for `EmptyPageContent`
1 parent 5af7fc7 commit 2d17eaa

File tree

8 files changed

+145
-27
lines changed

8 files changed

+145
-27
lines changed

cms/api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ def _verify_apphook(apphook, namespace):
8989
try:
9090
assert apphook in apphook_pool.apps
9191
except AssertionError:
92-
print(apphook_pool.apps.values())
9392
raise
9493
apphook_name = apphook
9594
else:

cms/cms_config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
class CMSCoreConfig(CMSAppConfig):
1616
cms_enabled = True
1717
cms_wizards = [cms_page_wizard, cms_subpage_wizard]
18-
cms_toolbar_enabled_models = [(PageContent, render_pagecontent)]
18+
cms_toolbar_enabled_models = [(PageContent, render_pagecontent, "page")]
1919

2020

2121
class CMSCoreExtensions(CMSAppExtension):
2222

2323
def __init__(self):
2424
self.wizards = {}
2525
self.toolbar_enabled_models = {}
26+
self.model_groupers = {}
2627
self.toolbar_mixins = []
2728

2829
def configure_wizards(self, cms_config):
@@ -49,13 +50,15 @@ def configure_wizards(self, cms_config):
4950
def configure_toolbar_enabled_models(self, cms_config):
5051
if not isinstance(cms_config.cms_toolbar_enabled_models, Iterable):
5152
raise ImproperlyConfigured("cms_toolbar_enabled_models must be iterable")
52-
for model, render_func in cms_config.cms_toolbar_enabled_models:
53+
for model, render_func, *grouper in cms_config.cms_toolbar_enabled_models:
5354
if model in self.toolbar_enabled_models:
5455
logger.warning(
5556
"Model {} already registered for frontend rendering".format(model),
5657
)
5758
else:
5859
self.toolbar_enabled_models[model] = render_func
60+
if grouper:
61+
self.model_groupers[model] = grouper[0]
5962

6063
def configure_toolbar_mixin(self, cms_config):
6164
if not issubclass(cms_config.cms_toolbar_mixin, object):

cms/cms_toolbars.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ def add_language_menu(self):
360360
url = language_changer(code)
361361
except NoReverseMatch:
362362
url = DefaultLanguageChanger(self.request)(code)
363-
self._language_menu.add_link_item(name, url=url, active=self.current_lang == code)
363+
if url:
364+
self._language_menu.add_link_item(name, url=url, active=self.current_lang == code)
364365
else:
365366
# We do not have to check every time the toolbar is created
366367
self._language_menu = True # Pretend the language menu is already there
@@ -395,7 +396,7 @@ def get_page_content(self):
395396

396397
def has_page_change_permission(self):
397398
if not hasattr(self, 'page_change_permission'):
398-
self.page_change_permission = can_change_page(self.request)
399+
self.page_change_permission = can_change_page(self.request) and self.toolbar.object_is_editable()
399400
return self.page_change_permission
400401

401402
def in_apphook(self):
@@ -472,7 +473,6 @@ def change_language_menu(self):
472473
)
473474
else:
474475
can_change = False
475-
476476
if can_change:
477477
language_menu = self.toolbar.get_menu(LANGUAGE_MENU_IDENTIFIER)
478478
if not language_menu:
@@ -508,9 +508,10 @@ def change_language_menu(self):
508508
disabled = len(remove) == 1
509509
for code, name in remove:
510510
pagecontent = self.page.get_content_obj(code)
511-
translation_delete_url = admin_reverse('cms_pagecontent_delete', args=(pagecontent.pk,))
512-
url = add_url_parameters(translation_delete_url, language=code)
513-
remove_plugins_menu.add_modal_item(name, url=url, disabled=disabled)
511+
if pagecontent:
512+
translation_delete_url = admin_reverse('cms_pagecontent_delete', args=(pagecontent.pk,))
513+
url = add_url_parameters(translation_delete_url, language=code)
514+
remove_plugins_menu.add_modal_item(name, url=url, disabled=disabled)
514515

515516
if copy:
516517
copy_plugins_menu = language_menu.get_or_create_menu(

cms/models/pagemodel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ def remove_language(self, language):
731731
self.update_languages(page_languages)
732732

733733
def update_languages(self, languages):
734-
languages = ",".join(languages)
734+
languages = ",".join(set(languages))
735735
# Update current instance
736736
self.languages = languages
737737
# Commit. It's important to not call save()

cms/tests/test_toolbar.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from cms.toolbar.utils import (
5151
add_live_url_querystring_param,
5252
get_object_edit_url,
53+
get_object_for_language,
5354
get_object_preview_url,
5455
get_object_structure_url,
5556
)
@@ -690,20 +691,26 @@ def test_double_menus(self):
690691
"""
691692
user = self.get_staff()
692693
page = create_page('test', 'nav_playground.html', 'en')
694+
for code, verbose in get_language_tuple():
695+
if code != "en":
696+
create_page_content(code, f"test {code}", page)
693697
page_content = self.get_page_title_obj(page)
694698
edit_url = get_object_edit_url(page_content)
695699
en_request = self.get_page_request(None, user, edit_url)
696700
toolbar = CMSToolbar(en_request)
701+
toolbar.set_object(page_content)
697702
toolbar.populated = False
698703
toolbar.populate()
699704
toolbar.populated = False
700705
toolbar.populate()
701706
toolbar.populated = False
702707
toolbar.post_template_populate()
708+
get_object_for_language(page_content, "de")
703709
admin = toolbar.get_left_items()[0]
704710
lang = toolbar.get_left_items()[1]
705711
self.assertEqual(len(admin.get_items()), 15)
706712
self.assertEqual(len(lang.get_items()), len(get_language_tuple(1)))
713+
self.assertIn(edit_url, [item.url for item in lang.get_items()]) # Edit urls returned
707714

708715
@override_settings(CMS_PLACEHOLDER_CONF={'col_left': {'name': 'PPPP'}})
709716
def test_placeholder_name(self):
@@ -2093,6 +2100,35 @@ def test_add_live_url_querystring_param_handles_wrong_content_type(self):
20932100
self.assertEqual(edit_url.count("?"), 0)
20942101
self.assertEqual(preview_url.count("?"), 0)
20952102

2103+
def test_get_object_for_language_one_language(self):
2104+
page = create_page('Test', 'col_two.html', 'en')
2105+
page_content = self.get_page_title_obj(page, "en")
2106+
2107+
self.assertEqual(page_content, get_object_for_language(page_content, "en"))
2108+
self.assertTrue(not hasattr(page_content, "_sibling_objects_for_language_cache"))
2109+
self.assertIsNone(get_object_for_language(page_content, "de"))
2110+
self.assertTrue(hasattr(page_content, "_sibling_objects_for_language_cache"))
2111+
self.assertEqual(len(page_content._sibling_objects_for_language_cache), 1)
2112+
2113+
def test_get_object_for_language_multiple_languages(self):
2114+
page = create_page('Test', 'col_two.html', 'en')
2115+
# Additional pages to ensure not a page content of another page is returned
2116+
for code, verbose in get_language_tuple():
2117+
create_page(f"Not this page ({verbose})", "col_two.html", code)
2118+
2119+
page_content = {
2120+
"en": self.get_page_title_obj(page, "en")
2121+
}
2122+
for code, verbose in get_language_tuple():
2123+
if code != "en":
2124+
page_content[code] = create_page_content(code, verbose, page)
2125+
2126+
self.assertEqual(page_content["en"], get_object_for_language(page_content["en"], "en"))
2127+
self.assertTrue(not hasattr(page_content["en"], "_sibling_objects_for_language_cache"))
2128+
self.assertEqual(get_object_for_language(page_content["en"], "de"), page_content["de"])
2129+
self.assertTrue(hasattr(page_content["en"], "_sibling_objects_for_language_cache"))
2130+
self.assertEqual(len(page_content["en"]._sibling_objects_for_language_cache), len(get_language_tuple()))
2131+
20962132

20972133
class CharPkFrontendPlaceholderAdminTest(ToolbarTestBase):
20982134

cms/toolbar/toolbar.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,12 @@ def get_object_structure_url(self):
403403
return get_object_structure_url(self.obj, language=self.request_language)
404404
return ''
405405

406-
def object_is_editable(self):
407-
if self.obj:
408-
if hasattr(self.obj, "is_editable"):
406+
def object_is_editable(self, obj=None):
407+
obj = obj or self.obj
408+
if obj:
409+
if hasattr(obj, "is_editable"):
409410
# Object can decide itself
410-
return self.obj.is_editable(self.request)
411+
return obj.is_editable(self.request)
411412
return True
412413
return False
413414

cms/toolbar/utils.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import json
22
from collections import defaultdict, deque
3+
from typing import Optional
34

5+
from django.apps import apps
46
from django.contrib.contenttypes.models import ContentType
7+
from django.db import models
8+
from django.urls import NoReverseMatch
59
from django.utils.encoding import force_str
610
from django.utils.translation import (
711
get_language,
@@ -145,7 +149,13 @@ def add_live_url_querystring_param(obj, url, language=None):
145149
return url
146150

147151

148-
def get_object_edit_url(obj, language=None):
152+
def get_object_edit_url(obj: models.Model, language: str = None) -> str:
153+
"""
154+
Returns the url of the edit endpoint for the given object. The object must be frontend-editable
155+
and registered as such with cms.
156+
157+
If the object has a language property, the language parameter is ignored.
158+
"""
149159
content_type = ContentType.objects.get_for_model(obj)
150160

151161
language = getattr(obj, "language", language) # Object trumps parameter
@@ -159,7 +169,13 @@ def get_object_edit_url(obj, language=None):
159169
return url
160170

161171

162-
def get_object_preview_url(obj, language=None):
172+
def get_object_preview_url(obj:models.Model, language: str = None) -> str:
173+
"""
174+
Returns the url of the preview endpoint for the given object. The object must be frontend-editable
175+
and registered as such with cms.
176+
177+
If the object has a language property, the language parameter is ignored.
178+
"""
163179
content_type = ContentType.objects.get_for_model(obj)
164180

165181
language = getattr(obj, "language", language) # Object trumps parameter
@@ -173,7 +189,14 @@ def get_object_preview_url(obj, language=None):
173189
return url
174190

175191

176-
def get_object_structure_url(obj, language=None):
192+
def get_object_structure_url(obj: models.Model, language: str = None) -> str:
193+
"""
194+
Returns the url of the structure endpoint for the given object. The object must be frontend-editable
195+
and registered as such with cms.
196+
197+
If the object has a language property, the language parameter is ignored.
198+
"""
199+
177200
content_type = ContentType.objects.get_for_model(obj)
178201

179202
language = getattr(obj, "language", language) # Object trumps parameter
@@ -182,3 +205,40 @@ def get_object_structure_url(obj, language=None):
182205

183206
with force_language(language):
184207
return admin_reverse('cms_placeholder_render_object_structure', args=[content_type.pk, obj.pk])
208+
209+
def get_object_for_language(obj: models.Model, language: str, latest: bool = False) -> Optional[models.Model]:
210+
"""
211+
Retrieves the correct content object for the target language. The object must be frontend-editable
212+
and registered as such with cms.
213+
214+
Two cases have to be distinguished:
215+
216+
1. **Object has a language property:** If the language of the passed object is different,
217+
sibling objects are retrieved from the database and cached in the object passed.
218+
2. **Object has no language property:** The placeholders of the object contain the different
219+
language content. The object itself is returned
220+
"""
221+
if getattr(obj, "language", language) == language:
222+
# Object does not have language field or language is requested language
223+
# Return object itself
224+
return obj
225+
# Does the object have a cache with sister objects
226+
cached_object = getattr(obj, "_sibling_objects_for_language_cache", {})
227+
if cached_object:
228+
return cached_object.get(language)
229+
230+
extension = apps.get_app_config('cms').cms_extension
231+
model = obj.__class__
232+
field = extension.model_groupers.get(model)
233+
if not field:
234+
# Cannot infer sister object if grouper field is unknown
235+
return None
236+
# Grouper model not registered or does not have a get_content_obj method,
237+
# or get_content_obj does not accept language parameter
238+
# Now query db
239+
grouper_filter = {field: getattr(obj, field)}
240+
qs = model.admin_manager.latest_content() if latest and hasattr(model, "admin_manager") else model.objects
241+
obj._sibling_objects_for_language_cache = {
242+
result.language: result for result in qs.filter(**grouper_filter)
243+
}
244+
return obj._sibling_objects_for_language_cache.get(language)

menus/utils.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.conf import settings
2-
from django.urls import NoReverseMatch, resolve, reverse
2+
from django.urls import NoReverseMatch, Resolver404, resolve, reverse
33

4+
from cms.toolbar.utils import get_object_edit_url, get_object_for_language, get_object_preview_url
45
from cms.utils import get_current_site, get_language_from_request
56
from cms.utils.i18n import (
67
force_language,
@@ -139,24 +140,41 @@ def __call__(self, lang):
139140
with force_language(page_language):
140141
try:
141142
view = resolve(self.request.path_info)
142-
except: # NOQA
143+
except (NoReverseMatch, Resolver404): # NOQA
143144
view = None
144-
if hasattr(self.request, 'toolbar') and self.request.toolbar.obj:
145-
with force_language(lang):
146-
try:
147-
return self.request.toolbar.obj.get_absolute_url()
148-
except: # NOQA
149-
pass
145+
if (
146+
hasattr(self.request, 'toolbar') and
147+
self.request.toolbar.obj and
148+
hasattr(self.request.toolbar.obj, "get_absolut FA2B e_url")
149+
):
150+
# Toolbar object
151+
if self.request.toolbar.edit_mode_active:
152+
lang_obj = get_object_for_language(self.request.toolbar.obj, lang, latest=True)
153+
return '' if lang_obj is None else get_object_edit_url(lang_obj, language=lang)
154+
if self.request.toolbar.preview_mode_active:
155+
lang_obj = get_object_for_language(self.request.toolbar.obj, lang, latest=True)
156+
return '' if lang_obj is None else get_object_preview_url(lang_obj, language=lang)
157+
try:
158+
# First see, if object can get language-specific absolute urls (like PageContent)
159+
return self.request.toolbar.obj.get_absolute_url(language=lang)
160+
except (TypeError, NoReverseMatch):
161+
# Object's get_absolute_url does not accept language parameter, set the language
162+
with force_language(lang):
163+
try:
164+
url = self.request.toolbar.obj.get_absolute_url()
165+
except NoReverseMatch:
166+
url = None
167+
if url:
168+
return url
150169
elif view and view.url_name not in ('pages-details-by-slug', 'pages-root'):
151170
view_name = view.url_name
152171
if view.namespace:
153172
view_name = "%s:%s" % (view.namespace, view_name)
154-
url = None
155173
with force_language(lang):
156174
try:
157175
url = reverse(view_name, args=view.args, kwargs=view.kwargs, current_app=view.app_name)
158176
except NoReverseMatch:
159-
pass
177+
url = None
160178
if url:
161179
return url
162180
return '%s%s' % (self.get_page_path(lang), self.app_path)

0 commit comments

Comments
 (0)
0