8000 fix: Replaced `languages` field from `Page` which used to become inconsistent by fsbraun · Pull Request #8080 · django-cms/django-cms · GitHub
[go: up one dir, main page]

Skip to content

fix: Replaced languages field from Page which used to become inconsistent #8080

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 8 commits into from
Nov 27, 2024
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
6 changes: 4 additions & 2 deletions cms/admin/pageadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1227,14 +1227,16 @@ def delete_view(self, request, object_id, extra_context=None):
'language': force_str(get_language_object(language)['name'])
}
messages.success(request, message)
if language in page.admin_content_cache:
del page.admin_content_cache[language]
if language in page.page_content_cache:
del page.page_content_cache[language]

page_url.delete()
page_content.delete()
for p in saved_plugins:
p.delete()

page.remove_language(language)

send_post_page_operation(
request=request,
operation=operations.DELETE_PAGE_TRANSLATION,
Expand Down
11 changes: 3 additions & 8 deletions cms/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,10 @@ def create_page_content(language, title, page, menu_title=None, slug=None,
_thread_locals.user = created_by
created_by = get_clean_username(created_by)

page.urls.create(
slug=slug,
path=path,
page.urls.update_or_create(
page=page,
managed=not bool(overwrite_url),
language=language,
defaults=dict(slug=slug, path=path, managed=not bool(overwrite_url)),
)

# E.g., djangocms-versioning needs an User object to be passed when creating a versioned Object
Expand All @@ -340,11 +338,8 @@ def create_page_content(language, title, page, menu_title=None, slug=None,
xframe_options=xframe_options,
)
page_content.rescan_placeholders()
page._clear_internal_cache()

page_languages = page.get_languages()

if language not in page_languages:
page.update_languages(page_languages + [language])
return page_content


Expand Down
3 changes: 1 addition & 2 deletions cms/cms_menus.py
10000
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,6 @@ def get_nodes(self, request) -> list[NavigationNode]:
"soft_root",
"in_navigation",
"page__site_id",
"page__languages",
"page__parent_id",
"page__is_home",
"page__login_required",
Expand All @@ -352,7 +351,7 @@ def prefetch_urls(page_content: PageContent) -> PageContent:
else:
preview_url = None # No short-cut here
prefetched_urls = PageUrl.objects.filter(
language__in=self.languages,
language__in=(page_content.language for page_content in page_contents),
page_id__in=(page_content.page.pk for page_content in page_contents),
) # Fetch the PageUrl objects
# Prepare for filling urls_cache
Expand Down
3 changes: 0 additions & 3 deletions cms/management/commands/subcommands/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ def handle(self, *args, **options):
new_title["page"] = page
PageContent.objects.with_user(user).create(**new_title)

if to_lang not in page.get_languages():
page.update_languages(page.get_languages() + [to_lang])

if copy_content:
# copy plugins using API
if verbose:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.16 on 2024-11-14 06:48

from django.db import migrations
from django.db.models import Count


def remove_pageurl_duplicates(apps, schema_editor):
PageUrl = apps.get_model("cms", "PageUrl")
non_unique = PageUrl.objects.values("page_id", "language").annotate(total=Count("language")).filter(total__gt=1)
for item in non_unique:
for url in PageUrl.objects.filter(page_id=item["page_id"], language=item["language"]).order_by("-pk")[1:]:
url.delete()


class Migration(migrations.Migration):
dependencies = [
("cms", "0038_alter_page_site"),
]

operations = [
migrations.RunPython(remove_pageurl_duplicates),
migrations.RemoveField(
model_name="page",
name="languages",
),
migrations.AlterUniqueTogether(
name="pageurl",
unique_together={("language", "page")},
),
]
127 changes: 57 additions & 70 deletions cms/models/pagemodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@
from treebeard.mp_tree import MP_Node

from cms import constants
from cms.exceptions import LanguageError
from cms.models.managers import PageManager, PageUrlManager
from cms.utils import i18n
from cms.utils.compat.warnings import RemovedInDjangoCMS43Warning
from cms.utils.conf import get_cms_setting
from cms.utils.i18n import get_current_language, get_fallback_languages
from cms.utils.i18n import get_current_language
from cms.utils.page import get_clean_username
from menus.menu_pool import menu_pool

Expand Down Expand Up @@ -114,12 +113,6 @@ class Page(MP_Node):
blank=True,
null=True,
)
languages = models.CharField(
max_length=255,
editable=False,
blank=True,
null=True,
)
is_page_type = models.BooleanField(
default=False,
help_text=_("Mark this page as a page type"),
Expand All @@ -143,6 +136,7 @@ class Meta:
def __init__(self, *args, **kwargs):
A3D4 super().__init__(*args, **kwargs)
self.urls_cache = {}
#: Internal cache for page urls
self.page_content_cache = {}
#: Internal cache for page content objects visible publicly
self.admin_content_cache = AdminCacheDict()
Expand Down Expand Up @@ -438,7 +432,6 @@ def move_page(self, target_page, position='first-child'):
languages = (
self
.urls
.filter(language__in=self.get_languages())
.values_list('language', flat=True)
)

Expand Down Expand Up @@ -524,7 +517,6 @@ def copy(self, site, parent_page=None, parent_node=None, language=None,
)
placeholder.copy_plugins(new_placeholder, language=new_title.language)
new_page.page_content_cache[new_title.language] = new_title
new_page.update_languages([trans.language for trans in translations])

if extensions:
from cms.extensions import extension_pool
Expand Down Expand Up @@ -692,30 +684,46 @@ def get_parent_page(self):
)
return self.parent

def get_languages(self):
if self.languages:
return sorted(self.languages.split(','))
else:
return []
@property
def languages(self):
warnings.warn(
"Attribute `languages` is deprecated. Use `get_languages` instead.",
RemovedInDjangoCMS43Warning,
stacklevel=2
)
return ",".join(self.get_languages())

def get_languages(self, admin_manager=True):
"""Returns available languages for the page. This is potentially costly."""
if admin_manager:
if not self.admin_content_cache:
self.set_admin_content_cache()
return list(self.admin_content_cache.keys())
if not self.page_content_cache:
self._get_page_content_cache(get_language(), fallback=False, force_reload=False)
return list(self.page_content_cache.keys())

def remove_language(self, language):
page_languages = self.get_languages()

if language in page_languages:
page_languages.remove(language)
self.update_languages(page_languages)
warnings.warn(
"Method `remove_language` is deprecated and has no effect any more.",
RemovedInDjangoCMS43Warning,
stacklevel=2
)

def update_languages(self, languages):
languages = ",".join(set(languages))
# Update current instance
self.languages = languages
# Commit. It's important to not call save()
# we'd like to commit only the languages field and without
# any kind of signals.
self.update(languages=languages)
warnings.warn(
"Method `update_languages` is deprecated and has no effect any more.",
RemovedInDjangoCMS43Warning,
stacklevel=2
)

def get_published_languages(self):
return self.get_languages()
warnings.warn(
"Method `get_published_languages` is deprecated. Use `get_languages(admin_manager=False)` instead.",
RemovedInDjangoCMS43Warning,
stacklevel=2,
)
return self.get_languages(admin_manager=False)

def set_translations_cache(self):
warnings.warn(
Expand Down Expand Up @@ -800,58 +808,36 @@ def get_page_content_obj_attribute(self, attrname, language=None, fallback=True,
except AttributeError:
return None

def get_path(self, language, fallback=True):
def get_url_obj(self, language, fallback=True):
"""Get the path of the page depending on the given language"""
languages = [language]

if fallback:
languages.extend(self.get_fallbacks(language))

page_languages = self.get_languages()

for _language in languages:
if _language in page_languages:
language = _language
break

if language not in self.urls_cache:
self.urls_cache.update({
url.language: url for url in self.urls.all() if url.language in languages # TODO: overwrites multiple urls
})

for _language in languages:
self.urls_cache.setdefault(_language, None)
# `get_page_from_request` will fill the cache only for the current language
# Here, we fully fill it and try again
self.urls_cache = {
url.language: url for url in self.urls.all() if url.language in languages
}

return next(
(self.urls_cache[lang] for lang in languages if lang in self.urls_cache),
None
)

try:
return self.urls_cache[language].path
except (AttributeError, KeyError):
return None
def get_path(self, language, fallback=True):
url = self.get_url_obj(language, fallback)
if url:
return url.path
return None

def get_slug(self, language, fallback=True):
languages = [language]
url = self.get_url_obj(language, fallback)
if url:
return url.slug
return None

if fallback:
languages.extend(self.get_fallbacks(language))

page_languages = self.get_languages()

for _language in languages:
10000 if _language in page_languages:
language = _language
break

if language not in self.urls_cache:
self.urls_cache.update({
url.language: url for url in self.urls.filter(language__in=languages)
})

for _language in languages:
self.urls_cache.setdefault(_language, None)

try:
return self.urls_cache[language].slug
except (AttributeError, KeyError):
return None

def get_title(self, language=None, fallback=True, force_reload=False):
"""
Expand Down Expand Up @@ -1099,6 +1085,7 @@ class PageUrl(models.Model):
class Meta:
app_label = 'cms'
default_permissions = []
unique_together = ('language', 'page')

def __str__(self):
return f"{self.path or self.slug} ({self.language})"
Expand Down
2 changes: 1 addition & 1 deletion cms/templatetags/cms_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def get_page_display_name(cms_page):
page_content = cms_page.get_admin_content(language, fallback="force")
title = page_content.title or page_content.page_title or page_content.menu_title
if not title:
title = cms_page.get_slug(language)
title = cms_page.get_slug(language) or _("Empty")
return title if page_content.language == language else mark_safe(f"<em>{title} ({page_content.language})</em>")


Expand Down
3 changes: 1 addition & 2 deletions cms/tests/test_templatetags.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def test_get_preview_url(self):


def test_get_admin_tree_title(self):
page = create_page("page_a", "nav_playground.html", "en")
page = create_page("page_a", "nav_playground.html", "en", slug="slug-test2")
self.assertEqual(get_page_display_name(page), 'page_a')
languages = {
1: [
Expand All @@ -108,7 +108,6 @@ def test_get_admin_tree_title(self):
page.admin_content_cache = {'en': PageContent(menu_title="menu test2", language="en")}
self.assertEqual('<em>menu test2 (en)</em>', force_str(get_page_display_name(page)))
page.admin_content_cache = {'en': PageContent(language="en")}
page.urls_cache = {'en': PageUrl(slug='slug-test2')}
self.assertEqual('<em>slug-test2 (en)</em>', force_str(get_page_display_name(page)))
page.admin_content_cache = {'en': PageContent(language="en"), 'fr': EmptyPageContent('fr')}
self.assertEqual('<em>slug-test2 (en)</em>', force_str(get_page_display_name(page)))
Expand Down
5 changes: 3 additions & 2 deletions cms/tests/test_toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ def get_delete_url(pk):
page = create_page("english-page", "nav_playground.html", "en")
german_content = create_page_content("de", "german content", page)
english_content = page.get_content_obj('en')
page_languages = page.get_languages()
edit_url = get_object_edit_url(english_content)
staff = self.get_staff()
self.client.force_login(staff)
Expand All @@ -541,8 +542,8 @@ def get_delete_url(pk):
menus = response.context['cms_toolbar'].menus
language_menu = menus['language-menu']
delete = language_menu.items[-2]
german_delete = delete.items[0]
english_delete = delete.items[1]
german_delete = delete.items[page_languages.index("de")]
english_delete = delete.items[page_languages.index("en")]

copy = language_menu.items[-1]
copy_german = copy.items[0]
Expand Down
12 changes: 12 additions & 0 deletions cms/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,18 @@ def test_malicious_content_login_request(self):

self.assertNotIn(response.url, "<script>alert('Attack')</script>")

def test_queries(self):
create_page("home", "simple.html", "en")
cms_page = create_page("dreinhardt", "simple.html", "en")
url = cms_page.get_absolute_url()
with self.assertNumQueries(5):
# 1. get_page_from_request: checks PageUrl
# 2. get page contents: PageContent
# 3. Check permissions
# 4. Get placeholders
# 5. Get plugins
self.client.get(url)


@override_settings(ROOT_URLCONF='cms.test_utils.project.urls')
class ContextTests(CMSTestCase):
Expand Down
6 changes: 3 additions & 3 deletions cms/utils/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.utils.encoding import force_str

from cms.constants import PAGE_USERNAME_MAX_LENGTH
from cms.utils import get_current_site
from cms.utils import get_current_site, get_language_from_request
from cms.utils.conf import get_cms_setting

SUFFIX_REGEX = re.compile(r'^(.*)-(\d+)$')
Expand Down Expand Up @@ -105,10 +105,10 @@ def get_page_from_request(request, use_path=None, clean_path=None):
page_urls = list(page_urls) # force queryset evaluation to save 1 query
try:
page = page_urls[0].page
if page_urls[0].language == get_language_from_request(request):
page.urls_cache = {url.language: url for url in page_urls}
except IndexError:
page = None
else:
page.urls_cache = {url.language: url for url in page_urls}
return page


Expand Down
Loading
0