Commit 1f649a49 authored by Kazuhiko Shiozaki's avatar Kazuhiko Shiozaki

erp5_web: introduce translatable path.

If /help/ Web Section has translation : {'de': 'hilfe', 'fr': 'aide'},
this section can be accessible by the following path :

* /de/hilfe/
* /fr/aide/

Also access to non-translated paths will be redirected like (for GET method) :

* /de/help/* => /de/hilfe/*
* /fr/help/* => /fr/aide/*
parent f13ea212
Pipeline #22985 failed with stage
in 0 seconds
......@@ -27,15 +27,17 @@
#
##############################################################################
import re
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet
from erp5.component.document.Domain import Domain
from Products.ERP5.Document.WebSection import WebSectionTraversalHook
from erp5.component.mixin.DocumentExtensibleTraversableMixin import DocumentExtensibleTraversableMixin
from Acquisition import aq_base, aq_inner
from Acquisition import aq_base, aq_inner, aq_parent
from Products.ERP5Type.UnrestrictedMethod import unrestricted_apply
from AccessControl import Unauthorized
from OFS.Traversable import NotFound
from OFS.ObjectManager import checkValidId
from ZPublisher import BeforeTraverse
from Products.CMFCore.utils import _checkConditionalGET, _setCacheHeaders, _ViewEmulator
......@@ -45,6 +47,7 @@ from Products.ERP5Type.Cache import getReadOnlyTransactionCache
WEBSECTION_KEY = 'web_section_value'
MARKER = []
WEB_SECTION_PORTAL_TYPE_TUPLE = ('Web Section', 'Web Site')
INTERNAL_TRANSLATED_PATH_DICT_NAME = '__translated_path_dict'
class WebSection(Domain, DocumentExtensibleTraversableMixin):
"""
......@@ -102,6 +105,18 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin):
If no subobject is found through Folder API
then try to lookup the object by invoking getDocumentValue
"""
# Check translated path.
is_static_language_selection = self.isStaticLanguageSelection()
if is_static_language_selection:
language = self.getPortalObject().Localizer.get_selected_language()
if language:
translated_path_dict = self._getTranslatedPathDict()
try:
section = self[translated_path_dict[(name, language)]]
except KeyError:
pass
else:
return section.asContext(id=name).__of__(self)
# Register current web site physical path for later URL generation
if request.get(self.web_section_key, MARKER) is MARKER:
request[self.web_section_key] = self.getPhysicalPath()
......@@ -128,8 +143,51 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin):
# if no document found, fallback on default page template
document = DocumentExtensibleTraversableMixin.__bobo_traverse__(self, request,
'404.error.page')
portal_type = getattr(document, 'getPortalType', lambda: None)()
# Redirect /lang/original_id/* to /lang/translated_id/* if request is GET.
if is_static_language_selection and portal_type == 'Web Section' and self.REQUEST.get('method') == 'GET':
translated_id = document.getTranslatedTranslatableId()
if translated_id and document.getId() != translated_id:
actual_url = self.REQUEST.get('ACTUAL_URL', '').strip()
section_url = document.absolute_url()
translated_section_url = '/'.join([
document.aq_parent.absolute_url(),
translated_id,
])
re.sub(re.escape(r'^' + section_url) + r'(/|$)', translated_section_url + r'\1', actual_url)
translated_url = actual_url.replace(section_url, translated_section_url, 1)
if actual_url != translated_url:
query_string = self.REQUEST.get('QUERY_STRING', '')
if query_string:
translated_url += '?' + query_string
self.REQUEST.RESPONSE.redirect(translated_url, status=301)
return document
def _getTranslatedPathDict(self):
return getattr(self, INTERNAL_TRANSLATED_PATH_DICT_NAME, {})
security.declareProtected(Permissions.ModifyPortalContent, 'updateTranslatedWebSectionList')
def updateTranslatedPathDict(self, recursive=False):
translated_path_dict = {}
available_language_list = self.getAvailableLanguageList()
for section in self.objectValues(portal_type='Web Section'):
section_id = section.getId()
translated_section_id_dict = dict((k[1], v[1]) for k, v in section._getTranslationDict().items() \
if k[0] == 'translatable_id' and v[0] == section_id)
for language in available_language_list:
translated_section_id = translated_section_id_dict.get(language, section_id)
checkValidId(self, translated_section_id, allow_dup=True)
key = (translated_section_id, language)
if key in translated_path_dict:
raise ValueError, '%r is used in several sections : %r' % (key, (translated_path_dict[key], section_id))
translated_path_dict[key] = section_id
if translated_path_dict != self._getTranslatedPathDict():
setattr(self, INTERNAL_TRANSLATED_PATH_DICT_NAME, translated_path_dict)
self._p_changed = True
if recursive:
for section in self.objectValues(portal_type='Web Section'):
section.updateTranslatedPathDict(recursive=recursive)
security.declarePrivate( 'manage_beforeDelete' )
def manage_beforeDelete(self, item, container):
if item is self:
......@@ -397,6 +455,16 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin):
return result
security.declarePublic('absolute_translated_url')
def absolute_translated_url(self, relative=0):
"""Return the absolute translated URL of the object."""
parent = aq_parent(aq_inner(self))
parent_url = getattr(parent, 'absolute_translated_url', parent.absolute_url)(relative=relative)
original_document = self.getOriginalDocument()
return self._add_trailing_slash(
self._add_trailing_slash(parent_url) + \
getattr(original_document, 'getTranslatedTranslatableId', original_document.getId)())
security.declareProtected(Permissions.View, 'getSiteMapTree')
def getSiteMapTree(self, **kw):
"""
......
......@@ -119,6 +119,19 @@ class WebSite(WebSection):
raise Redirect(redirect_url)
return super(WebSite, self).__before_publishing_traverse__(self2, request)
security.declarePublic('absolute_translated_url')
def absolute_translated_url(self, relative=0):
"""Return the absolute translated URL of the object."""
language = self.getPortalObject().Localizer.get_selected_language()
language_list = self.getAvailableLanguageList()
if language in language_list and self.isStaticLanguageSelection():
url = self.getOriginalDocument().absolute_url(relative=relative)
if language != self.getDefaultAvailableLanguage():
return '/'.join([url, language])
else:
return url
return self.absolute_url(relative=relative)
security.declareProtected(Permissions.AccessContentsInformation, 'getPermanentURLList')
def getPermanentURLList(self, document):
"""
......
......@@ -13,6 +13,7 @@
</portal_type>
<portal_type id="Web Section">
<item>SortIndex</item>
<item>TranslatablePath</item>
<item>WebSectionUpgradeConstraint</item>
</portal_type>
<portal_type id="Web Site">
......
......@@ -29,7 +29,7 @@
</chain>
<chain>
<type>Web Section</type>
<workflow>category_publication_workflow, edit_workflow</workflow>
<workflow>category_publication_workflow, edit_workflow, web_section_interaction_workflow</workflow>
</chain>
<chain>
<type>Web Site</type>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Property Sheet" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>TranslatablePath</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Property Sheet</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/string</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>translatable_id_property</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
<item>
<key> <string>storage_id</string> </key>
<value> <string>id</string> </value>
</item>
<item>
<key> <string>translatable</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>translation_domain</string> </key>
<value> <string>content_translation</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -1579,6 +1579,67 @@ Hé Hé Hé!""", page.asText().strip())
check(document2, date2)
check(document2, date3)
def test_translatable_path(self):
portal = self.portal
website = self.setupWebSite(
static_language_selection=1,
)
section1 = self.setupWebSection(
id='aaa',
)
page1 = portal.web_page_module.newContent(
portal_type='Web Page',
reference='page1',
language='en',
text_content='page1 in English',
)
page1.publish()
page1_fr = portal.web_page_module.newContent(
portal_type='Web Page',
reference='page1',
language='fr',
text_content='page1 in French',
)
page1_fr.publish()
section1.setAggregateValue(page1)
section2 = self.setupWebSection(
id='bbb',
)
page2 = portal.web_page_module.newContent(
portal_type='Web Page',
reference='page2',
language='en',
text_content='page2',
)
page2.publish()
page2_fr = portal.web_page_module.newContent(
portal_type='Web Page',
reference='page2',
language='fr',
text_content='page2 in French',
)
page2_fr.publish()
section2.setAggregateValue(page2)
section2.setFrTranslatedTranslatableId('aaa')
# Only setting /fr/aaa => /bbb will conflict with /aaa having no translated path yet.
self.assertRaises(ValueError, self.commit)
# After setting /fr/ccc => /aaa as well, we have no conflict.
section1.setFrTranslatedTranslatableId('ccc')
self.tic()
website_path = website.absolute_url_path()
# /fr/ccc/ is /aaa/
response = self.publish(website_path + '/fr/ccc/')
self.assertEqual(response.status, 200)
self.assertIn('page1 in French', response.getBody())
# /fr/aaa/ is /bbb/
response = self.publish(website_path + '/fr/aaa/')
self.assertEqual(response.status, 200)
self.assertIn('page2 in French', response.getBody())
# /fr/bbb/ should be redirected to /fr/aaa/
response = self.publish(website_path + '/fr/bbb/')
self.assertEqual(response.status, 301)
self.assertEqual(response.getHeader('Location'), website.absolute_url() + '/fr/aaa/')
class TestERP5WebWithSimpleSecurity(ERP5TypeTestCase):
"""
Test for erp5_web with simple security.
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Interaction Workflow" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>web_section_interaction_workflow</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>web_section_interaction_workflow</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Interaction Workflow</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Interaction Workflow Definition</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Interaction Workflow Interaction" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>before_commit_script/portal_workflow/web_section_interaction_workflow/script_updateTranslatedPathDict</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>interaction_updateTranslatedPathDict</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Interaction Workflow Interaction</string> </value>
</item>
<item>
<key> <string>portal_type_filter</string> </key>
<value>
<tuple>
<string>Web Section</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type_group_filter</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>temporary_document_disallowed</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>trigger_method_id</string> </key>
<value>
<tuple>
<string>set.*TranslatedTranslatableId</string>
</tuple>
</value>
</item>
<item>
<key> <string>trigger_once_per_transaction</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>trigger_type</string> </key>
<value> <int>2</int> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
section = state_change['object']
section.getParentValue().updateTranslatedPathDict()
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Workflow Script" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>state_change</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>script_updateTranslatedPathDict</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Workflow Script</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value>
<none/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -5,6 +5,7 @@ Static Web Site | WebSitePreference
Web Page | Reference
Web Page | SortIndex
Web Section | SortIndex
Web Section | TranslatablePath
Web Section | WebSectionUpgradeConstraint
Web Site | WebSectionUpgradeConstraint
Web Site | WebSitePreference
\ No newline at end of file
......@@ -13,5 +13,6 @@ Web Page | processing_status_workflow
Web Page | publication_workflow
Web Section | category_publication_workflow
Web Section | edit_workflow
Web Section | web_section_interaction_workflow
Web Site | category_publication_workflow
Web Site | edit_workflow
\ No newline at end of file
WebSectionUpgradeConstraint
\ No newline at end of file
WebSectionUpgradeConstraint
TranslatablePath
\ No newline at end of file
category_publication_workflow
publication_workflow
\ No newline at end of file
publication_workflow
web_section_interaction_workflow
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment