Skip to content

Commit

Permalink
Canonical translations are now linked too (django-cms#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
corentin-cres authored and Corentin Cres committed Sep 13, 2018
1 parent 06ce79c commit 37cc394
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 81 deletions.
143 changes: 89 additions & 54 deletions cms/admin/forms.py
Expand Up @@ -26,6 +26,7 @@
from cms.plugin_pool import plugin_pool
from cms.signals.apphook import set_restart_trigger
from cms.utils.conf import get_cms_setting
from cms.utils.page import generate_title_translations_from_canonical
from cms.utils.compat.forms import UserChangeForm
from cms.utils.i18n import get_language_list, get_language_object
from cms.utils.permissions import (
Expand Down Expand Up @@ -199,34 +200,83 @@ def __init__(self, *args, **kwargs):
if len(choices) < 2:
source_field.widget = forms.HiddenInput()

def clean(self):
def create_path(self, slug, lang, parent_node):
data = self.cleaned_data

if self._errors:
# Form already has errors, best to let those be
# addressed first.
return data

parent_node = data.get('parent_node')

if parent_node:
slug = data['slug']
parent_path = parent_node.item.get_path(self._language)
slug = slug
parent_path = parent_node.item.get_path(lang)
path = u'%s/%s' % (parent_path, slug) if parent_path else slug
else:
path = data['slug']
path = slug

try:
# Validate the url
validate_url_uniqueness(
self._site,
path=path,
language=self._language,
language=lang,
)
except ValidationError as error:
self.add_error('slug', error)

return path

def clean(self):
data = self.cleaned_data

if self._errors:
# Form already has errors, best to let those be
# addressed first.
return data

parent_node = data.get('parent_node')
canonical_url = data.get('canonical_url')

paths = {}
slugs = {}
titles = {}

if canonical_url:
# Remove language from the URL
from django.conf import settings
for lang in settings.LANGUAGES:
if canonical_url.startswith('/{}/'.format(lang[0])):
length = len(lang[0]) + 2 # + '/'*2
canonical_url = canonical_url[length:]
break
# Remove the trailing '/'
canonical_url = canonical_url.rstrip('/')
# Retrieve the targeted page object
canonical = Page.objects.get(
title_set__path=canonical_url,
publisher_is_draft=False,
)
for lang in canonical.get_languages():
if lang == self._language:
slugs[lang] = data['slug']
titles[lang] = data['title']
# Use create_path to validate the slug and display error if collision
paths[lang] = self.create_path(data['slug'], lang, parent_node)
else:
# Use generate_... to compute a valid (unique) slug automatically
# We avoid collisions hence we do not handle errors
slugs[lang], paths[lang], titles[lang] = generate_title_translations_from_canonical(
parent_node,
canonical,
self._site,
lang
)

data['canonical'] = canonical
else:
data['path'] = path
slug = data['slug']
paths[self._language] = self.create_path(slug, self._language, parent_node)
slugs[self._language] = slug
titles[self._language] = data['title']

data['paths'] = paths
data['slugs'] = slugs
data['titles']= titles
return data

def clean_parent_node(self):
Expand All @@ -236,14 +286,18 @@ def clean_parent_node(self):
raise ValidationError("Site doesn't match the parent's page site")
return parent_node

def create_translation(self, page):
def create_translation(self, lang, page):
data = self.cleaned_data

path = data['paths'][lang]
slug = data['slugs'][lang]
title = data['titles'][lang]
title_kwargs = {
'page': page,
'language': self._language,
'slug': data['slug'],
'path': data['path'],
'title': data['title'],
'language': lang,
'slug': slug,
'path': path,
'title': title,
}

if 'menu_title' in data:
Expand Down Expand Up @@ -274,59 +328,40 @@ def get_template(self):
def save(self, *args, **kwargs):
source = self.cleaned_data.get('source')
parent = self.cleaned_data.get('parent_node')
canonical_url = self.cleaned_data.get('canonical_url')
canonical = self.cleaned_data.get('canonical')

if canonical:
source = canonical
source.publisher_is_draft = True

if source:
new_page = self.from_source(source, parent=parent)

for lang in source.get_languages():
source._copy_contents(new_page, lang)
elif canonical_url:
# Remove language from the URL
from django.conf import settings
for lang in settings.LANGUAGES:
if canonical_url.startswith('/{}/'.format(lang[0])):
length = len(lang[0]) + 2 # + '/'*2
canonical_url = canonical_url[length:]
break
# Remove the trailing '/'
canonical_url = canonical_url.rstrip('/')

# Retrieve the targeted page object
canonical_page = Page.objects.get(
title_set__path=canonical_url,
publisher_is_draft=False,
title_set__language=self._language
)
# Do NOT use from_source : we want to copy extensions !
new_page = canonical_page.copy(
site=self._site,
parent_node=parent,
language=self._language,
translations=False,
permissions=False,
extensions=True,
)
new_page.publisher_is_draft = True
new_page.canonical = canonical_page
new_page.update(is_page_type=False, in_navigation=True)

for lang in canonical_page.get_languages():
canonical_page._copy_contents(new_page, lang)
if canonical:
new_page.canonical = source

else:
new_page = super(AddPageForm, self).save(commit=False)
new_page.template = self.get_template()
new_page.set_tree_node(self._site, target=parent, position='last-child')
new_page.save()

translation = self.create_translation(new_page)
translations = []
if canonical:
# Create all translations
for lang in source.get_languages():
translations.append(self.create_translation(lang, new_page))
else:
translations.append(self.create_translation(self._language, new_page))

if source:
extension_pool.copy_extensions(
source_page=source,
target_page=new_page,
languages=[translation.language],
languages=[translation.language for translation in translations],
)

is_first = not (
Expand All @@ -340,7 +375,7 @@ def save(self, *args, **kwargs):

if is_first and not new_page.is_page_type:
# its the first page. publish it right away
new_page.publish(translation.language)
new_page.publish(self._language)
new_page.set_as_homepage(self._user)
return new_page

Expand Down
65 changes: 38 additions & 27 deletions cms/models/pagemodel.py
Expand Up @@ -25,7 +25,7 @@
from cms.utils import i18n
from cms.utils.compat import DJANGO_1_11
from cms.utils.conf import get_cms_setting
from cms.utils.page import get_clean_username
from cms.utils.page import get_clean_username, get_available_slug, generate_title_translations_from_canonical
from cms.utils.i18n import get_current_language

from menus.menu_pool import menu_pool
Expand Down Expand Up @@ -940,6 +940,8 @@ def publish(self, language):
:returns: True if page was successfully published.
"""
from cms.utils.permissions import get_current_user_name
from cms import api
from cms.extensions import extension_pool

# Publish can only be called on draft pages
if not self.publisher_is_draft:
Expand Down Expand Up @@ -1006,34 +1008,22 @@ def publish(self, language):

cms_signals.post_publish.send(sender=Page, instance=self, language=language)


public_page.clear_cache(
language,
menu=True,
placeholder=True,
)

# Find related pages that define the current one as canonical to update them too.
if self.pk:

from cms.extensions import extension_pool
canonicals_published = Page.objects.filter(
canonical=public_page.pk,
publisher_is_draft=False
)
canonicals_draft = Page.objects.filter(
canonical=public_page.pk,
publisher_is_draft=True
)
# We WANT published before drafts
# The reason is, when we will delete the extensions of the pages,
# the published extensions will cascade and delete the drafted extensions...
# If we start the process with the drafted, it will be deleted when we will do the process for the published
# because of the cascading delete.
# So : published, then drafted
from itertools import chain
canonicals = list(chain(canonicals_published, canonicals_draft))

import logging
logger = logging.getLogger('jindexe_cms')

for canonical in canonicals:
# delete existing placeholders

for canonical in canonicals_draft:
# delete existing placeholders, they will be re-created from the current page
for placeholder in canonical.placeholders.iterator():
placeholder.delete()

Expand All @@ -1043,18 +1033,37 @@ def publish(self, language):
new_placeholder.pk = None
new_placeholder.save()
canonical.placeholders.add(new_placeholder)
placeholder.copy_plugins(new_placeholder, language=language)

# Copy the placeholder for each language
for lang in self.get_languages():
# Create translation if it does not exist
if lang not in canonical.get_languages():
slug, path, title = generate_title_translations_from_canonical(
canonical.node.parent,
public_page, # use public_page (self has outdated translations)
canonical.node.site,
lang
)

title_kwargs = {
'page': canonical,
'language': lang,
'slug': slug,
'path': path,
'title': title,
}
api.create_title(**title_kwargs)

# Go on with the copy
placeholder.copy_plugins(new_placeholder, language=lang)

for extension in extension_pool.get_page_extensions(canonical):
extension.delete()
extension_pool.copy_extensions(public_page, canonical)
canonical.save()
for lang in self.get_languages():
canonical.publish(lang)

public_page.clear_cache(
language,
menu=True,
placeholder=True,
)
return True

def clear_cache(self, language=None, menu=False, placeholder=False):
Expand Down Expand Up @@ -1480,6 +1489,8 @@ def _get_title_cache(self, language, fallback, force_reload):
if fallback and not self.title_cache.get(language):
# language can be in the cache but might be an EmptyTitle instance
fallback_langs = i18n.get_fallback_languages(language)
if language == 'fr':
fallback_langs = ['en']
for lang in fallback_langs:
if self.title_cache.get(lang):
return lang
Expand Down
21 changes: 21 additions & 0 deletions cms/utils/page.py
Expand Up @@ -214,3 +214,24 @@ def get_available_slug(site, path, language, suffix='copy', modified=False):
path = '%s/%s' % (base, slug) if base else slug
return get_available_slug(site, path, language, suffix, modified=True)
return slug

def generate_title_translations_from_canonical(parent_node, canonical, site, language):
"""
Generates title translations based on a given page's parent node and canonical
parent_node is used to infer the path of the new node (parent_node.item.get_path())
parent_node can be None
"""
if parent_node:
parent_path = parent_node.item.get_path(language)
else:
parent_path = ''

# Generate temporary slug / path (will be modified by get_available_slug if collision)
tmp_slug = canonical.get_title_obj_attribute('slug', language=language)
tmp_path = u'%s/%s' % (parent_path, tmp_slug) if parent_path else tmp_slug
# Final versions of slug and path
slug = get_available_slug(site, tmp_path, language, suffix='')
path = u'%s/%s' % (parent_path, slug) if parent_path else slug
title = canonical.get_title_obj_attribute('title', language=language)

return slug, path, title

0 comments on commit 37cc394

Please sign in to comment.