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 @@ ...@@ -27,15 +27,17 @@
# #
############################################################################## ##############################################################################
import re
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type import Permissions, PropertySheet
from erp5.component.document.Domain import Domain from erp5.component.document.Domain import Domain
from Products.ERP5.Document.WebSection import WebSectionTraversalHook from Products.ERP5.Document.WebSection import WebSectionTraversalHook
from erp5.component.mixin.DocumentExtensibleTraversableMixin import DocumentExtensibleTraversableMixin 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 Products.ERP5Type.UnrestrictedMethod import unrestricted_apply
from AccessControl import Unauthorized from AccessControl import Unauthorized
from OFS.Traversable import NotFound from OFS.Traversable import NotFound
from OFS.ObjectManager import checkValidId
from ZPublisher import BeforeTraverse from ZPublisher import BeforeTraverse
from Products.CMFCore.utils import _checkConditionalGET, _setCacheHeaders, _ViewEmulator from Products.CMFCore.utils import _checkConditionalGET, _setCacheHeaders, _ViewEmulator
...@@ -45,6 +47,7 @@ from Products.ERP5Type.Cache import getReadOnlyTransactionCache ...@@ -45,6 +47,7 @@ from Products.ERP5Type.Cache import getReadOnlyTransactionCache
WEBSECTION_KEY = 'web_section_value' WEBSECTION_KEY = 'web_section_value'
MARKER = [] MARKER = []
WEB_SECTION_PORTAL_TYPE_TUPLE = ('Web Section', 'Web Site') WEB_SECTION_PORTAL_TYPE_TUPLE = ('Web Section', 'Web Site')
INTERNAL_TRANSLATED_PATH_DICT_NAME = '__translated_path_dict'
class WebSection(Domain, DocumentExtensibleTraversableMixin): class WebSection(Domain, DocumentExtensibleTraversableMixin):
""" """
...@@ -102,6 +105,18 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin): ...@@ -102,6 +105,18 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin):
If no subobject is found through Folder API If no subobject is found through Folder API
then try to lookup the object by invoking getDocumentValue 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 # Register current web site physical path for later URL generation
if request.get(self.web_section_key, MARKER) is MARKER: if request.get(self.web_section_key, MARKER) is MARKER:
request[self.web_section_key] = self.getPhysicalPath() request[self.web_section_key] = self.getPhysicalPath()
...@@ -128,8 +143,51 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin): ...@@ -128,8 +143,51 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin):
# if no document found, fallback on default page template # if no document found, fallback on default page template
document = DocumentExtensibleTraversableMixin.__bobo_traverse__(self, request, document = DocumentExtensibleTraversableMixin.__bobo_traverse__(self, request,
'404.error.page') '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 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' ) security.declarePrivate( 'manage_beforeDelete' )
def manage_beforeDelete(self, item, container): def manage_beforeDelete(self, item, container):
if item is self: if item is self:
...@@ -397,6 +455,16 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin): ...@@ -397,6 +455,16 @@ class WebSection(Domain, DocumentExtensibleTraversableMixin):
return result 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') security.declareProtected(Permissions.View, 'getSiteMapTree')
def getSiteMapTree(self, **kw): def getSiteMapTree(self, **kw):
""" """
......
...@@ -119,6 +119,19 @@ class WebSite(WebSection): ...@@ -119,6 +119,19 @@ class WebSite(WebSection):
raise Redirect(redirect_url) raise Redirect(redirect_url)
return super(WebSite, self).__before_publishing_traverse__(self2, request) 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') security.declareProtected(Permissions.AccessContentsInformation, 'getPermanentURLList')
def getPermanentURLList(self, document): def getPermanentURLList(self, document):
""" """
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
</portal_type> </portal_type>
<portal_type id="Web Section"> <portal_type id="Web Section">
<item>SortIndex</item> <item>SortIndex</item>
<item>TranslatablePath</item>
<item>WebSectionUpgradeConstraint</item> <item>WebSectionUpgradeConstraint</item>
</portal_type> </portal_type>
<portal_type id="Web Site"> <portal_type id="Web Site">
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
</chain> </chain>
<chain> <chain>
<type>Web Section</type> <type>Web Section</type>
<workflow>category_publication_workflow, edit_workflow</workflow> <workflow>category_publication_workflow, edit_workflow, web_section_interaction_workflow</workflow>
</chain> </chain>
<chain> <chain>
<type>Web Site</type> <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()) ...@@ -1579,6 +1579,67 @@ Hé Hé Hé!""", page.asText().strip())
check(document2, date2) check(document2, date2)
check(document2, date3) 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): class TestERP5WebWithSimpleSecurity(ERP5TypeTestCase):
""" """
Test for erp5_web with simple security. 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 ...@@ -5,6 +5,7 @@ Static Web Site | WebSitePreference
Web Page | Reference Web Page | Reference
Web Page | SortIndex Web Page | SortIndex
Web Section | SortIndex Web Section | SortIndex
Web Section | TranslatablePath
Web Section | WebSectionUpgradeConstraint Web Section | WebSectionUpgradeConstraint
Web Site | WebSectionUpgradeConstraint Web Site | WebSectionUpgradeConstraint
Web Site | WebSitePreference Web Site | WebSitePreference
\ No newline at end of file
...@@ -13,5 +13,6 @@ Web Page | processing_status_workflow ...@@ -13,5 +13,6 @@ Web Page | processing_status_workflow
Web Page | publication_workflow Web Page | publication_workflow
Web Section | category_publication_workflow Web Section | category_publication_workflow
Web Section | edit_workflow Web Section | edit_workflow
Web Section | web_section_interaction_workflow
Web Site | category_publication_workflow Web Site | category_publication_workflow
Web Site | edit_workflow Web Site | edit_workflow
\ No newline at end of file
WebSectionUpgradeConstraint WebSectionUpgradeConstraint
TranslatablePath
\ No newline at end of file
category_publication_workflow category_publication_workflow
publication_workflow 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