# HG changeset patch # User Thierry Florac # Date 1532009730 -7200 # Node ID 209432f09f9f47c515540fee662081f378db1a25 # Parent edcf61caaf3bebf071316bf5122ba0f95566709c Added redirections manager diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/features/redirect/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/__init__.py Thu Jul 19 16:15:30 2018 +0200 @@ -0,0 +1,88 @@ +# +# Copyright (c) 2008-2018 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import re + +# import interfaces +from pyams_content.features.redirect.interfaces import IRedirectionRule +from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION +from pyams_form.interfaces.form import IFormContextPermissionChecker + +# import packages +from persistent import Persistent +from pyams_sequence.reference import get_reference_target +from pyams_utils.adapter import adapter_config, ContextAdapter +from pyams_utils.url import canonical_url +from pyams_utils.zodb import volatile_property +from zope.container.contained import Contained +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +@implementer(IRedirectionRule) +class RedirectionRule(Persistent, Contained): + """Redirection rule persistent class""" + + active = FieldProperty(IRedirectionRule['active']) + chained = FieldProperty(IRedirectionRule['chained']) + permanent = FieldProperty(IRedirectionRule['permanent']) + _url_pattern = FieldProperty(IRedirectionRule['url_pattern']) + reference = FieldProperty(IRedirectionRule['reference']) + target_url = FieldProperty(IRedirectionRule['target_url']) + + @property + def url_pattern(self): + return self._url_pattern + + @url_pattern.setter + def url_pattern(self, value): + if value != self._url_pattern: + self._url_pattern = value + del self.pattern + + @volatile_property + def target(self): + return get_reference_target(self.reference) + + def get_target(self, state=None): + if not state: + return self.target + else: + return get_reference_target(self.reference, state) + + @volatile_property + def pattern(self): + return re.compile(self.url_pattern) + + def match(self, source_url): + return self.pattern.match(source_url) + + def rewrite(self, source_url, request): + target_url = None + if self.reference: + target = self.target + if target is not None: + target_url = canonical_url(target, request) + else: + target_url = self.pattern.sub(self.target_url, source_url) + return target_url + + +@adapter_config(context=IRedirectionRule, provides=IFormContextPermissionChecker) +class RedirectionRulePermissionChecker(ContextAdapter): + """Redirection rule permission checker""" + + edit_permission = MANAGE_SITE_ROOT_PERMISSION diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/features/redirect/container.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/container.py Thu Jul 19 16:15:30 2018 +0200 @@ -0,0 +1,103 @@ +# +# Copyright (c) 2008-2018 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_content.features.redirect.interfaces import IRedirectionManager, IRedirectionRule, IRedirectionManagerTarget, \ + REDIRECT_MANAGER_KEY +from zope.location.interfaces import ISublocations +from zope.traversing.interfaces import ITraversable + +# import packages +from pyams_catalog.utils import index_object +from pyams_utils.adapter import adapter_config, get_annotation_adapter, ContextAdapter +from pyramid.response import Response +from zope.container.ordered import OrderedContainer +from zope.interface import implementer +from zope.location.location import locate + +from pyams_content import _ + + +@implementer(IRedirectionManager) +class RedirectManager(OrderedContainer): + """Redirect manager""" + + last_id = 1 + + def append(self, value, notify=True): + key = str(self.last_id) + if not notify: + # pre-locate item to avoid multiple notifications + locate(value, self, key) + self[key] = value + self.last_id += 1 + if not notify: + # make sure that item is correctly indexed + index_object(value) + + def get_active_items(self): + yield from filter(lambda x: IRedirectionRule(x).active, self.values()) + + def get_response(self, request): + target_url = request.path_qs + for rule in self.get_active_items(): + match = rule.match(target_url) + if match: + target_url = rule.rewrite(target_url, request) + if not rule.chained: + response = Response() + response.status_code = 301 if rule.permanent else 302 + response.location = target_url + return response + + def test_rules(self, source_url, request, check_inactive_rules=False): + if check_inactive_rules: + rules = self.values() + else: + rules = self.get_active_items() + for rule in rules: + match = rule.match(source_url) + if match: + target_url = rule.rewrite(source_url, request) + yield rule, source_url, target_url + if not rule.chained: + raise StopIteration + source_url = target_url + else: + yield rule, source_url, request.localizer.translate(_("not matching")) + + +@adapter_config(context=IRedirectionManagerTarget, provides=IRedirectionManager) +def redirection_manager_factory(context): + """Redirection manager factory""" + return get_annotation_adapter(context, REDIRECT_MANAGER_KEY, RedirectManager, name='++redirect++') + + +@adapter_config(name='redirect', context=IRedirectionManagerTarget, provides=ITraversable) +class RedirectionManagerNamespace(ContextAdapter): + """Redirection manager ++redirect++ namespace""" + + def traverse(self, name, furtherpath=None): + return IRedirectionManager(self.context) + + +@adapter_config(name='redirect', context=IRedirectionManagerTarget, provides=ISublocations) +class RedirectManagerSublocations(ContextAdapter): + """redirection manager sub-locations adapter""" + + def sublocations(self): + return IRedirectionManager(self.context).values() diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/features/redirect/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/interfaces/__init__.py Thu Jul 19 16:15:30 2018 +0200 @@ -0,0 +1,102 @@ +# +# Copyright (c) 2008-2018 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_content.interfaces.container import IOrderedContainer +from pyams_sequence.interfaces import IInternalReference + +# import packages +from pyams_sequence.schema import InternalReferenceField +from zope.container.constraints import contains, containers +from zope.interface import Interface, Attribute, invariant, Invalid +from zope.schema import Bool, TextLine, Choice + +from pyams_content import _ + + +REDIRECT_MANAGER_KEY = 'pyams_content.redirect' + + +class IRedirectionRule(IInternalReference): + """Redirection rule interface""" + + containers('.IRedirectManager') + + active = Bool(title=_("Active rule?"), + description=_("If 'no', selected rule is inactive"), + required=True, + default=False) + + chained = Bool(title=_("Chained rule?"), + description=_("If 'no', and if this rule is matching received request URL, the rule " + "returns a redirection response; otherwise, the rule just rewrites the " + "input URL which is forwarded to the next rule"), + required=True, + default=False) + + permanent = Bool(title=_("Permanent redirect?"), + description=_("Define if this redirection should be permanent or temporary"), + required=True, + default=True) + + url_pattern = TextLine(title=_("URL pattern"), + description=_("Regexp pattern of matching URLs for this redirection rule"), + required=True) + + pattern = Attribute("Compiled URL pattern") + + reference = InternalReferenceField(title=_("Internal redirection target"), + description=_("Internal redirection reference. You can search a reference using " + "'+' followed by internal number, of by entering text matching " + "content title."), + required=False) + + target_url = TextLine(title=_("Target URL"), + description=_("URL to which source URL should be redirected"), + required=False) + + @invariant + def check_reference_and_target(self): + if self.reference and self.target_url: + raise Invalid(_("You can only provide an internal reference OR a target URL")) + elif not (self.reference or self.target_url): + raise Invalid(_("You must provide an internal reference OR a target URL")) + + def match(self, source_url): + """Return regexp URL match on given URL""" + + def rewrite(self, source_url, request): + """Rewrite given source URL""" + + +class IRedirectionManager(IOrderedContainer): + """Redirection manager""" + + contains(IRedirectionRule) + + def get_active_items(self): + """Get iterator over active items""" + + def get_response(self, request): + """Get new response for given request""" + + def test_rules(self, source_url, request, check_inactive_rules=False): + """Test rules against given URL""" + + +class IRedirectionManagerTarget(Interface): + """Redirection manager target marker interface""" diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/features/redirect/tween.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/tween.py Thu Jul 19 16:15:30 2018 +0200 @@ -0,0 +1,49 @@ +# +# Copyright (c) 2008-2018 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_content.features.redirect.interfaces import IRedirectionManager + +# import packages +from pyramid.exceptions import NotFound +from pyramid.httpexceptions import HTTPNotFound + + +def redirect_tween_factory(handler, registry): + """Redirect tween factory + + This tween is used to handle NotFound errors: when a request which raises + a NotFound error is served, we look info redirects configuration to check if + given URL is matching any defined redirection, in which case another HTTPRedirect + response is returned with a new location; another content using a proxy request + can also be returned. + """ + + def redirect_tween(request): + try: + response = handler(request) + except (NotFound, HTTPNotFound): + manager = IRedirectionManager(request.root, None) + if manager is not None: + response = manager.get_response(request) + if response is not None: + return response + raise + else: + return response + + return redirect_tween diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/features/redirect/zmi/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/zmi/__init__.py Thu Jul 19 16:15:30 2018 +0200 @@ -0,0 +1,126 @@ +# +# Copyright (c) 2008-2018 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +from pyams_form.help import FormHelp +from pyams_form.interfaces.form import IFormHelp +from pyams_utils.adapter import adapter_config +from pyams_zmi.layer import IAdminLayer + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_content.features.redirect.interfaces import IRedirectionRule, IRedirectionManagerTarget, IRedirectionManager +from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION +from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager +from pyams_skin.layer import IPyAMSLayer + +# import packages +from pyams_content.features.redirect import RedirectionRule +from pyams_content.features.redirect.zmi.container import RedirectionsContainerView, RedirectionsContainerTable +from pyams_form.form import ajax_config, AJAXAddForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.event import get_json_table_row_refresh_event +from pyams_skin.viewlet.toolbar import ToolbarAction +from pyams_utils.traversing import get_parent +from pyams_utils.url import absolute_url +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm +from z3c.form import field + +from pyams_content import _ + + +@viewlet_config(name='add-rule.action', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + view=RedirectionsContainerView, manager=IWidgetTitleViewletManager, + permission=MANAGE_SITE_ROOT_PERMISSION, weight=1) +class RedirectionRuleAddAction(ToolbarAction): + """Redirection rule add action""" + + label = _("Add rule") + label_css_class = 'fa fa-fw fa-plus' + url = 'add-rule.html' + modal_target = True + + +@pagelet_config(name='add-rule.html', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +@ajax_config(name='add-rule.json', context=IRedirectionManagerTarget, layer=IPyAMSLayer, base=AJAXAddForm) +class RedirectionRuleAddForm(AdminDialogAddForm): + """Redirection rule add form""" + + dialog_class = 'modal-large' + legend = _("Add new redirection rule") + icon_css_class = 'fa fa-fw fa-map-signs' + + fields = field.Fields(IRedirectionRule).omit('__parent__', '__name__', 'active', 'chained') + edit_permission = MANAGE_SITE_ROOT_PERMISSION + + def create(self, data): + return RedirectionRule() + + def add(self, object): + IRedirectionManager(self.context).append(object) + + def nextURL(self): + return absolute_url(self.context, self.request, 'redirections.html') + + +@pagelet_config(name='properties.html', context=IRedirectionRule, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +@ajax_config(name='properties.json', context=IRedirectionRule, layer=IPyAMSLayer) +class RedirectionRulePropertiesEditForm(AdminDialogEditForm): + """Redirection rule properties edit form""" + + dialog_class = 'modal-large' + prefix = 'rule_properties.' + + legend = _("Edit redirection rule properties") + icon_css_class = 'fa fa-fw fa-map-signs' + + fields = field.Fields(IRedirectionRule).omit('__parent__', '__name__', 'active', 'chained') + edit_permission = MANAGE_SITE_ROOT_PERMISSION + + def get_ajax_output(self, changes): + output = super(self.__class__, self).get_ajax_output(changes) + updated = changes.get(IRedirectionRule, ()) + if updated: + target = get_parent(self.context, IRedirectionManagerTarget) + output.setdefault('events', []).append( + get_json_table_row_refresh_event(target, self.request, RedirectionsContainerTable, self.context)) + return output + + +@adapter_config(context=(IRedirectionManagerTarget, IAdminLayer, RedirectionRuleAddForm), provides=IFormHelp) +@adapter_config(context=(IRedirectionRule, IAdminLayer, RedirectionRulePropertiesEditForm), provides=IFormHelp) +class RedirectionRuleFormHelp(FormHelp): + """Redirection rule form help""" + + message = _("""URL pattern and target URL are defined by *regular expressions* (see |regexp|). + +In URL pattern, you can use any valid regular expression element, notably: + +- « .* » to match any list of characters + +- « ( ) » to "memorize" parts of the URL which can be replaced into target URL + +- special characters (like "+") must be escaped with an « \\\\ ». + +In target URL, memorized parts can be reused using « \\\\1 », « \\\\2 » and so on, where given number is +the order of the matching pattern element. + +.. |regexp| raw:: html + + Python Regular Expressions +""") + message_format = 'rest' diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/features/redirect/zmi/container.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/zmi/container.py Thu Jul 19 16:15:30 2018 +0200 @@ -0,0 +1,376 @@ +# +# Copyright (c) 2008-2018 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +from pyams_form.form import ajax_config, AJAXAddForm +from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager +from pyams_form.schema import CloseButton +from pyams_skin.help import ContentHelp +from pyams_skin.interfaces.viewlet import IToolbarViewletManager +from pyams_skin.skin import apply_skin +from pyams_skin.viewlet.toolbar import ToolbarAction +from pyams_template.template import template_config +from pyams_utils.request import copy_request +from pyams_zmi.form import AdminDialogAddForm + +__docformat__ = 'restructuredtext' + + +# import standard library +import json + +# import interfaces +from pyams_content.features.redirect.interfaces import IRedirectionManagerTarget, IRedirectionManager +from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION +from pyams_i18n.interfaces import II18n +from pyams_sequence.interfaces import ISequentialIdInfo +from pyams_skin.interfaces import IPageHeader, IUserSkinnable, IContentHelp +from pyams_skin.layer import IPyAMSLayer +from pyams_zmi.interfaces.menu import ISiteManagementMenu +from pyams_zmi.layer import IAdminLayer +from z3c.table.interfaces import IValues, IColumn + +# import packages +from pyams_content.skin import pyams_content +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.page import DefaultPageHeaderAdapter +from pyams_skin.table import BaseTable, SorterColumn, VisibilitySwitcherColumn, TrashColumn, I18nColumn +from pyams_skin.viewlet.menu import MenuItem +from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter +from pyams_utils.fanstatic import get_resource_path +from pyams_utils.url import absolute_url +from pyams_viewlet.viewlet import viewlet_config, Viewlet +from pyams_zmi.view import ContainerAdminView +from pyramid.decorator import reify +from pyramid.exceptions import NotFound +from pyramid.view import view_config +from z3c.form import field, button +from z3c.table.column import GetAttrColumn +from zope.interface import Interface +from zope.schema import TextLine, Bool + +from pyams_content import _ + + +@viewlet_config(name='redirections.menu', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + manager=ISiteManagementMenu, permission=MANAGE_SITE_ROOT_PERMISSION, weight=35) +class RedirectionMenu(MenuItem): + """Redirection manager menu""" + + label = _("Redirections") + icon_class = 'fa-map-signs' + url = '#redirections.html' + + +class RedirectionsContainerTable(BaseTable): + """Redirections container table""" + + prefix = 'redirections' + + hide_header = True + sortOn = None + + cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight table-dnd'} + + @property + def data_attributes(self): + attributes = super(RedirectionsContainerTable, self).data_attributes + attributes.setdefault('table', {}).update({ + 'data-ams-plugins': 'pyams_content', + 'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content), + 'data-ams-location': absolute_url(IRedirectionManager(self.context), self.request), + 'data-ams-tablednd-drag-handle': 'td.sorter', + 'data-ams-tablednd-drop-target': 'set-rules-order.json', + 'data-ams-active-icon-on': 'fa fa-fw fa-check-square-o', + 'data-ams-active-icon-off': 'fa fa-fw fa-square-o txt-color-silver opacity-75', + 'data-ams-chained-icon-on': 'fa fa-fw fa-chain', + 'data-ams-chained-icon-off': 'fa fa-fw fa-chain txt-color-silver opacity-50' + }) + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target, + 'data-ams-switcher-attribute-name': self.get_switcher_attribute + }) + return attributes + + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'enable-disable': + return 'switch-rule-activity.json' + elif column.__name__ == 'chain-unchain': + return 'switch-rule-chain.json' + + @staticmethod + def get_switcher_attribute(element, column): + if column.__name__ == 'enable-disable': + return 'active' + elif column.__name__ == 'chain-unchain': + return 'chained' + + @reify + def values(self): + return list(super(RedirectionsContainerTable, self).values) + + def render(self): + if not self.values: + translate = self.request.localizer.translate + return translate(_("No currently defined redirection rule.")) + return super(RedirectionsContainerTable, self).render() + + +@adapter_config(context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), provides=IValues) +class RedirectionsContainerValues(ContextRequestViewAdapter): + """Redirections container values""" + + @property + def values(self): + return IRedirectionManager(self.context).values() + + +@adapter_config(name='sorter', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerSorterColumn(SorterColumn): + """Redirections container sorter column""" + + +@view_config(name='set-rules-order.json', context=IRedirectionManager, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +def set_rules_order(request): + """Update redirection rules order""" + order = list(map(str, json.loads(request.params.get('names')))) + request.context.updateOrder(order) + return {'status': 'success'} + + +@adapter_config(name='enable-disable', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerShowHideColumn(VisibilitySwitcherColumn): + """Redirections container activity switcher column""" + + switch_attribute = 'active' + visible_icon_class = 'fa fa-fw fa-check-square-o' + hidden_icon_class = 'fa fa-fw fa-square-o txt-color-silver opacity-75' + + icon_hint = _("Enable/disable rule") + + url = 'MyAMS.container.switchElementAttribute' + weight = 6 + + +@view_config(name='switch-rule-activity.json', context=IRedirectionManager, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +def switch_rule_activity(request): + """Switch rule activity""" + container = IRedirectionManager(request.context) + rule = container.get(str(request.params.get('object_name'))) + if rule is None: + raise NotFound() + rule.active = not rule.active + return {'on': rule.active} + + +@adapter_config(name='chain-unchain', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerChainedColumn(VisibilitySwitcherColumn): + """Redirections container chained switcher column""" + + switch_attribute = 'chained' + visible_icon_class = 'fa fa-fw fa-chain' + hidden_icon_class = 'fa fa-fw fa-chain txt-color-silver opacity-50' + + icon_hint = _("Chain/unchain rule") + + url = 'MyAMS.container.switchElementAttribute' + weight = 7 + + +@view_config(name='switch-rule-chain.json', context=IRedirectionManager, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +def switch_rule_chain(request): + """Switch rule chain""" + container = IRedirectionManager(request.context) + rule = container.get(str(request.params.get('object_name'))) + if rule is None: + raise NotFound() + rule.chained = not rule.chained + return {'on': rule.chained} + + +@adapter_config(name='name', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerNameColumn(I18nColumn, GetAttrColumn): + """Redirections container name column""" + + _header = _("URL pattern") + attrName = 'url_pattern' + weight = 10 + + +@adapter_config(name='target', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerTargetColumn(I18nColumn, GetAttrColumn): + """Redirections container target column""" + + _header = _("Target") + attrName = 'target_url' + weight = 20 + + def getValue(self, obj): + if obj.reference: + target = obj.target + return '{0} ({1})'.format(II18n(target).query_attribute('title', request=self.request), + ISequentialIdInfo(target).get_short_oid()) + else: + return super(RedirectionsContainerTargetColumn, self).getValue(obj) + + +@adapter_config(name='trash', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerTrashColumn(TrashColumn): + """Redirections container trash column""" + + permission = MANAGE_SITE_ROOT_PERMISSION + + +@pagelet_config(name='redirections.html', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +class RedirectionsContainerView(ContainerAdminView): + """Redirections container view""" + + title = _("Redirections list") + table_class = RedirectionsContainerTable + + +@adapter_config(context=(IRedirectionManagerTarget, IAdminLayer, RedirectionsContainerView), provides=IPageHeader) +class RedirectionsContainerViewHeaderAdapter(DefaultPageHeaderAdapter): + """Redirections container view header adapter""" + + icon_class = 'fa fa-fw fa-map-signs' + + +@adapter_config(context=(IRedirectionManagerTarget, IAdminLayer, RedirectionsContainerView), provides=IContentHelp) +class RedirectionsContainerHelpAdapter(ContentHelp): + """Redirections container help adapter""" + + header = _("Redirection rules") + message = _("""Redirection rules are use to handle redirections responses when a request generates +a famous « 404 NotFound » error. + +Redirections are particularly useful when you are migrating from a previous site and don't want to lose +your SEO. + +You can define a set of rules which will be applied to every \"NotFound\" request; rules are based on +regular expressions which are applied to input URL: if the rule is \"matching\", the target URL is rewritten +and a \"Redirect\" response is send. + +You can chain rules together: when a rule is chained, it's rewritten URL is passed as input URL to the +next rule, until a matching rule is found. +""") + message_format = 'rest' + + +# +# Redirections container test form +# + +@viewlet_config(name='test.action', context=IRedirectionManagerTarget, layer=IAdminLayer, + view=RedirectionsContainerView, manager=IToolbarViewletManager, + permission=MANAGE_SITE_ROOT_PERMISSION, weight=75) +class RedirectionsContainerTestAction(ToolbarAction): + """redirections container test action""" + + label = _("Test") + + group_css_class = 'btn-group margin-left-5' + label_css_class = 'fa fa-fw fa-magic' + css_class = 'btn btn-xs btn-default' + + url = 'test-redirection-rules.html' + modal_target = True + + +class IRedirectionsContainerTestFields(Interface): + """Redirections container test fields""" + + source_url = TextLine(title=_("Test URL"), + required=True) + + check_inactive_rules = Bool(title=_("Check inactive rules?"), + description=_("If 'yes', inactive rules will also be tested"), + required=True, + default=False) + + +class IRedirectionsContainerTestButtons(Interface): + """Redirections container test form buttons""" + + close = CloseButton(name='close', title=_("Close")) + test = button.Button(name='test', title=_("Test rules")) + + +@pagelet_config(name='test-redirection-rules.html', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +class RedirectionsContainerTestForm(AdminDialogAddForm): + """Redirections container test form""" + + dialog_class = 'modal-max' + legend = _("Test redirection rules") + icon_css_class = 'fa fa-fw fa-magic' + + prefix = 'rules_test_form.' + fields = field.Fields(IRedirectionsContainerTestFields) + buttons = button.Buttons(IRedirectionsContainerTestButtons) + ajax_handler = 'test-redirection-rules.json' + edit_permission = MANAGE_SITE_ROOT_PERMISSION + + @property + def form_target(self): + return '#{0}_test_result'.format(self.id) + + def updateActions(self): + super(RedirectionsContainerTestForm, self).updateActions() + if 'test' in self.actions: + self.actions['test'].addClass('btn-primary') + + def createAndAdd(self, data): + data = data.get(self, data) + request = copy_request(self.request) + apply_skin(request, IUserSkinnable(self.context).get_skin()) + return IRedirectionManager(self.context).test_rules(data['source_url'], request, data['check_inactive_rules']) + + +@viewlet_config(name='test-indexer-process.suffix', layer=IAdminLayer, manager=IWidgetsSuffixViewletsManager, + view=RedirectionsContainerTestForm, weight=50) +@template_config(template='templates/manager-test.pt') +class RedirectionsContainerTestSuffix(Viewlet): + """Redirections container test form suffix""" + + +@view_config(name='test-redirection-rules.json', context=IRedirectionManagerTarget, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +class RedirectionsContainerAJAXTestForm(AJAXAddForm, RedirectionsContainerTestForm): + """Redirections container test form, JSON renderer""" + + def get_ajax_output(self, changes): + message = [] + translate = self.request.localizer.translate + for rule, source_url, target_url in changes: + if not message: + message.append('{:<40} {:<40} => {:<40}'.format(translate(_("URL pattern")), + translate(_("Input URL")), + translate(_("Output URL")))) + message.append('{:<40} {:<40} => {:<40}'.format('-' * 40, '-' * 40, '-' * 40)) + message.append('{:<40} {:<40} => {:<40}'.format(rule.url_pattern, source_url, target_url)) + if not message: + message.append(translate(_("No matching rule!"))) + return { + 'status': 'success', + 'content': {'html': '\n'.join(message)}, + 'close_form': False + } diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/features/redirect/zmi/templates/manager-test.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/zmi/templates/manager-test.pt Thu Jul 19 16:15:30 2018 +0200 @@ -0,0 +1,4 @@ +
+

+
diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/include.py --- a/src/pyams_content/include.py Thu Jul 19 10:38:08 2018 +0200 +++ b/src/pyams_content/include.py Thu Jul 19 16:15:30 2018 +0200 @@ -18,6 +18,7 @@ # import interfaces # import packages +from pyramid.tweens import MAIN def include_package(config): @@ -26,6 +27,9 @@ # add translations config.add_translation_dirs('pyams_content:locales') + # add custom twwen + config.add_tween('pyams_content.features.redirect.tween.redirect_tween_factory', over=MAIN) + # add custom routes config.add_route('oid_access', '/+/{oid}*view') diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo Binary file src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo has changed diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po --- a/src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po Thu Jul 19 10:38:08 2018 +0200 +++ b/src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po Thu Jul 19 16:15:30 2018 +0200 @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2018-07-18 14:46+0200\n" +"POT-Creation-Date: 2018-07-19 15:57+0200\n" "PO-Revision-Date: 2015-09-10 10:42+0200\n" "Last-Translator: Thierry Florac \n" "Language-Team: French\n" @@ -1389,27 +1389,27 @@ msgid "Content collections" msgstr "Collections associées au contenu" -#: src/pyams_content/component/theme/zmi/manager.py:51 +#: src/pyams_content/component/theme/zmi/manager.py:58 msgid "Tags settings..." msgstr "Paramétrage des tags" -#: src/pyams_content/component/theme/zmi/manager.py:65 +#: src/pyams_content/component/theme/zmi/manager.py:72 msgid "Selected tags" msgstr "Tags sélectionnés" -#: src/pyams_content/component/theme/zmi/manager.py:101 +#: src/pyams_content/component/theme/zmi/manager.py:108 msgid "Themes settings..." msgstr "Paramétrage des thèmes" -#: src/pyams_content/component/theme/zmi/manager.py:115 +#: src/pyams_content/component/theme/zmi/manager.py:122 msgid "Selected themes" msgstr "Thèmes sélectionnés" -#: src/pyams_content/component/theme/zmi/manager.py:151 +#: src/pyams_content/component/theme/zmi/manager.py:158 msgid "Collections settings..." msgstr "Paramétrage des collections" -#: src/pyams_content/component/theme/zmi/manager.py:165 +#: src/pyams_content/component/theme/zmi/manager.py:172 msgid "Selected collections" msgstr "Collections sélectionnées" @@ -1556,6 +1556,7 @@ #: src/pyams_content/component/links/interfaces/__init__.py:61 #: src/pyams_content/shared/logo/interfaces/__init__.py:56 +#: src/pyams_content/features/redirect/interfaces/__init__.py:68 msgid "Target URL" msgstr "URL cible" @@ -4411,7 +4412,7 @@ msgid "Default length used for inner tables and dashboards" msgstr "Longueur par défaut des tableaux internes et des tableaux de bord" -#: src/pyams_content/root/__init__.py:68 +#: src/pyams_content/root/__init__.py:70 msgid "Site root" msgstr "Racine du site" @@ -5079,6 +5080,247 @@ msgid "No currently defined alert." msgstr "Aucune alerte n'est définie actuellement." +#: src/pyams_content/features/redirect/container.py:81 +msgid "not matching" +msgstr "pas de correspondance" + +#: src/pyams_content/features/redirect/zmi/__init__.py:50 +msgid "Add rule" +msgstr "Ajouter une règle" + +#: src/pyams_content/features/redirect/zmi/__init__.py:63 +msgid "Add new redirection rule" +msgstr "Ajout d'une règle de redirection" + +#: src/pyams_content/features/redirect/zmi/__init__.py:88 +msgid "Edit redirection rule properties" +msgstr "Propriétés de la règle de redirection" + +#: src/pyams_content/features/redirect/zmi/__init__.py:109 +msgid "" +"URL pattern and target URL are defined by *regular expressions* (see |" +"regexp|).\n" +" \n" +"In URL pattern, you can use any valid regular expression element, notably:\n" +"\n" +"- « .* » to match any list of characters \n" +"\n" +"- « ( ) » to \"memorize\" parts of the URL which can be replaced into target " +"URL\n" +"\n" +"- special characters (like \"+\") must be escaped with an « \\\\ ».\n" +"\n" +"In target URL, memorized parts can be reused using « \\\\1 », « \\\\2 » and " +"so on, where given number is\n" +"the order of the matching pattern element.\n" +"\n" +".. |regexp| raw:: html\n" +"\n" +" Python Regular Expressions\n" +msgstr "" +"Le schéma d'URL et l'URL cible sont définis en tant que « expressions " +"rationelles » (voir |regexp|).\n" +"\n" +"Dans le schéma d'URL utilisé pour identifier les requêtes en entrée, vous " +"pouvez utiliser tout élément d'une expression rationnelle valide, " +"notamment :\n" +"\n" +"- « .* » pour rechercher n'importe quelle suite de caractères\n" +"\n" +"- « ^ » et « $ » pour identifier le début ou la fin de l'URL\n" +"\n" +"- « ( ) » pour \"mémoriser\" certains éléments de l'URL qui pourront être " +"repris dans l'URL cible\n" +"\n" +"- les caractères spéciaux (comme les \"+\") doivent être protégés par un " +"caractère « \\\\ ».\n" +"\n" +"For exemple : dans le schéma « ^/.*?oid=([a-z0-9]+)$ », toute URL contenant " +"un paramètre \"oid\" composé de minuscules et/ou de chiffres sera mémorisé " +"pour pouvoir être réutilisé dans l'URL cible.\n" +"\n" +"Dans l'URL cible, les éléments mémorisés peuvent être réutilisés en " +"utilisant une expression comme « \\\\1 », « \\\\2 » (et ainsi de suite), le " +"chiffre indiquant la position de l'élément dans la liste des éléments " +"mémorisés.\n" +"\n" +".. |regexp| raw:: html\n" +"\n" +" Expressions rationnelles en Python\n" + +#: src/pyams_content/features/redirect/zmi/container.py:67 +msgid "Redirections" +msgstr "Redirections" + +#: src/pyams_content/features/redirect/zmi/container.py:160 +msgid "Enable/disable rule" +msgstr "Activer/désactiver la règle" + +#: src/pyams_content/features/redirect/zmi/container.py:187 +msgid "Chain/unchain rule" +msgstr "Enchaîner la règle avec la suivante" + +#: src/pyams_content/features/redirect/zmi/container.py:210 +#: src/pyams_content/features/redirect/zmi/container.py:365 +#: src/pyams_content/features/redirect/interfaces/__init__.py:56 +msgid "URL pattern" +msgstr "Schéma d'URL" + +#: src/pyams_content/features/redirect/zmi/container.py:220 +msgid "Target" +msgstr "Cible" + +#: src/pyams_content/features/redirect/zmi/container.py:246 +msgid "Redirections list" +msgstr "Liste des règles de redirection" + +#: src/pyams_content/features/redirect/zmi/container.py:261 +msgid "Redirection rules" +msgstr "Règles de redirection" + +#: src/pyams_content/features/redirect/zmi/container.py:262 +msgid "" +"Redirection rules are use to handle redirections responses when a request " +"generates \n" +"a famous « 404 NotFound » error.\n" +"\n" +"Redirections are particularly useful when you are migrating from a previous " +"site and don't want to lose \n" +"your SEO.\n" +"\n" +"You can define a set of rules which will be applied to every \"NotFound\" " +"request; rules are based on \n" +"regular expressions which are applied to input URL: if the rule is \"matching" +"\", the target URL is rewritten\n" +"and a \"Redirect\" response is send.\n" +"\n" +"You can chain rules together: when a rule is chained, it's rewritten URL is " +"passed as input URL to the \n" +"next rule, until a matching rule is found.\n" +msgstr "" +"Les règles de redirection sont utilisées pour transmettre des réponses de redirection " +"au lieu de la fameuse erreur « 404 - Page non trouvée ».\n" +"\n" +"La gestion des redirections est particulièrement importante en phase de migration d'un site web, " +"pour éviter les liens cassés, ne pas perdre votre référencement et faciliter la mise à jour " +"des moteurs de recherche.\n" +"\n" +"Vous pouvez définir un ensemble de règles qui seront appliquées dès lors qu'une requête " +"adressée au serveur génère une erreur de page non trouvée ; les règles sont basées sur des " +"expressions rationnelles que l'on applique à l'URL de la requête reçue : si la règle correspond, " +"l'URL est réécrite et une réponse de redirection vers cette nouvelle URL est renvoyée.\n" +"\n" +"Vous pouvez également enchaîner les règles : lorsqu'une règle est \"chaînée\", la nouvelle URL " +"qu'elle génère est passée aux règles suivantes, jusqu'à ce qu'une règle s'applique à cette " +"nouvelle URL.\n" + +#: src/pyams_content/features/redirect/zmi/container.py:288 +msgid "Test" +msgstr "Tester !" + +#: src/pyams_content/features/redirect/zmi/container.py:323 +msgid "Test redirection rules" +msgstr "Test des règles de redirection" + +#: src/pyams_content/features/redirect/zmi/container.py:301 +msgid "Test URL" +msgstr "URL à tester" + +#: src/pyams_content/features/redirect/zmi/container.py:304 +msgid "Check inactive rules?" +msgstr "Tester les règles inactive ?" + +#: src/pyams_content/features/redirect/zmi/container.py:305 +msgid "If 'yes', inactive rules will also be tested" +msgstr "Si 'oui', les règles inactives seront également testées" + +#: src/pyams_content/features/redirect/zmi/container.py:313 +msgid "Close" +msgstr "Fermer" + +#: src/pyams_content/features/redirect/zmi/container.py:314 +msgid "Test rules" +msgstr "Tester cette URL" + +#: src/pyams_content/features/redirect/zmi/container.py:123 +msgid "No currently defined redirection rule." +msgstr "Aucune règle de redirection n'est définie actuellement." + +#: src/pyams_content/features/redirect/zmi/container.py:371 +msgid "No matching rule!" +msgstr "Aucune règle ne correspond !" + +#: src/pyams_content/features/redirect/zmi/container.py:366 +msgid "Input URL" +msgstr "URL en entrée" + +#: src/pyams_content/features/redirect/zmi/container.py:367 +msgid "Output URL" +msgstr "URL générée" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:39 +msgid "Active rule?" +msgstr "Règle active ?" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:40 +msgid "If 'no', selected rule is inactive" +msgstr "Si 'non', la règle est inactive" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:44 +msgid "Chained rule?" +msgstr "Règle chaînée ?" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:45 +msgid "" +"If 'no', and if this rule is matching received request URL, the rule returns " +"a redirection response; otherwise, the rule just rewrites the input URL " +"which is forwarded to the next rule" +msgstr "" +"Si 'non', et si cette règle correspond à l'URL reçue en entrée, une réponde " +"de redirection est renvoyée directement ; dans le cas contraire, l'URL " +"générée par cette règle est passée en entrée de la règle suivante" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:51 +msgid "Permanent redirect?" +msgstr "Redirection permanente ?" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:52 +msgid "Define if this redirection should be permanent or temporary" +msgstr "" +"Indique si cette redirection doit être considérée comme permanente ou " +"temporaire" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:57 +msgid "Regexp pattern of matching URLs for this redirection rule" +msgstr "Modèle de l'URL d'origine de cette règle de redirection" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:62 +msgid "Internal redirection target" +msgstr "Redirection interne" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:63 +msgid "" +"Internal redirection reference. You can search a reference using '+' " +"followed by internal number, of by entering text matching content title." +msgstr "" +"Référence interne vers une cible de redirection. Vous pouvez la rechercher " +"par des mots de son titre, ou par son numéro interne (précédé d'un '+')" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:69 +msgid "URL to which source URL should be redirected" +msgstr "URL vers laquelle l'URL d'origine doit être redirigée" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:75 +msgid "You can only provide an internal reference OR a target URL" +msgstr "" +"Vous ne pouvez fournir qu'une référence interne OU une URL de redirection !" + +#: src/pyams_content/features/redirect/interfaces/__init__.py:77 +msgid "You must provide an internal reference OR a target URL" +msgstr "Vous devez fournir une référence interne OU une URL de redirection !" + #: src/pyams_content/features/menu/zmi/__init__.py:81 msgid "Add menu..." msgstr "Ajouter un menu" @@ -5405,6 +5647,21 @@ msgid "Hidden header" msgstr "Ne pas afficher d'en-tête de pages" +#~ msgid "Rewrite to another internal reference or URL" +#~ msgstr "Rediriger vers une autre référence ou URL interne" + +#~ msgid "Redirect to another external URL" +#~ msgstr "Rediriger vers une URL externe" + +#~ msgid "Return content in proxy mode without redirection" +#~ msgstr "Charger le contenu ciblé sans redirection" + +#~ msgid "Redirect mode" +#~ msgstr "Mode de redirection" + +#~ msgid "Mode of redirection for this URL pattern" +#~ msgstr "Mode de redirection utilisé par cette règle" + #~ msgid "Subtitle" #~ msgstr "Sous-titre" diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/locales/pyams_content.pot --- a/src/pyams_content/locales/pyams_content.pot Thu Jul 19 10:38:08 2018 +0200 +++ b/src/pyams_content/locales/pyams_content.pot Thu Jul 19 16:15:30 2018 +0200 @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2018-07-18 14:46+0200\n" +"POT-Creation-Date: 2018-07-19 15:57+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" @@ -1337,27 +1337,27 @@ msgid "Content collections" msgstr "" -#: ./src/pyams_content/component/theme/zmi/manager.py:51 +#: ./src/pyams_content/component/theme/zmi/manager.py:58 msgid "Tags settings..." msgstr "" -#: ./src/pyams_content/component/theme/zmi/manager.py:65 +#: ./src/pyams_content/component/theme/zmi/manager.py:72 msgid "Selected tags" msgstr "" -#: ./src/pyams_content/component/theme/zmi/manager.py:101 +#: ./src/pyams_content/component/theme/zmi/manager.py:108 msgid "Themes settings..." msgstr "" -#: ./src/pyams_content/component/theme/zmi/manager.py:115 +#: ./src/pyams_content/component/theme/zmi/manager.py:122 msgid "Selected themes" msgstr "" -#: ./src/pyams_content/component/theme/zmi/manager.py:151 +#: ./src/pyams_content/component/theme/zmi/manager.py:158 msgid "Collections settings..." msgstr "" -#: ./src/pyams_content/component/theme/zmi/manager.py:165 +#: ./src/pyams_content/component/theme/zmi/manager.py:172 msgid "Selected collections" msgstr "" @@ -1500,6 +1500,7 @@ #: ./src/pyams_content/component/links/interfaces/__init__.py:61 #: ./src/pyams_content/shared/logo/interfaces/__init__.py:56 +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:68 msgid "Target URL" msgstr "" @@ -4131,7 +4132,7 @@ msgid "Default length used for inner tables and dashboards" msgstr "" -#: ./src/pyams_content/root/__init__.py:68 +#: ./src/pyams_content/root/__init__.py:70 msgid "Site root" msgstr "" @@ -4781,6 +4782,185 @@ msgid "No currently defined alert." msgstr "" +#: ./src/pyams_content/features/redirect/container.py:81 +msgid "not matching" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/__init__.py:50 +msgid "Add rule" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/__init__.py:63 +msgid "Add new redirection rule" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/__init__.py:88 +msgid "Edit redirection rule properties" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/__init__.py:109 +msgid "" +"URL pattern and target URL are defined by *regular expressions* (see |regexp|).\n" +" \n" +"In URL pattern, you can use any valid regular expression element, notably:\n" +"\n" +"- « .* » to match any list of characters \n" +"\n" +"- « ( ) » to \"memorize\" parts of the URL which can be replaced into target URL\n" +"\n" +"- special characters (like \"+\") must be escaped with an « \\\\ ».\n" +"\n" +"In target URL, memorized parts can be reused using « \\\\1 », « \\\\2 » and so on, where given number is\n" +"the order of the matching pattern element.\n" +"\n" +".. |regexp| raw:: html\n" +"\n" +" Python Regular Expressions\n" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:67 +msgid "Redirections" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:160 +msgid "Enable/disable rule" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:187 +msgid "Chain/unchain rule" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:210 +#: ./src/pyams_content/features/redirect/zmi/container.py:365 +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:56 +msgid "URL pattern" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:220 +msgid "Target" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:246 +msgid "Redirections list" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:261 +msgid "Redirection rules" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:262 +msgid "" +"Redirection rules are use to handle redirections responses when a request generates \n" +"a famous « 404 NotFound » error.\n" +"\n" +"Redirections are particularly useful when you are migrating from a previous site and don't want to lose \n" +"your SEO.\n" +"\n" +"You can define a set of rules which will be applied to every \"NotFound\" request; rules are based on \n" +"regular expressions which are applied to input URL: if the rule is \"matching\", the target URL is rewritten\n" +"and a \"Redirect\" response is send.\n" +"\n" +"You can chain rules together: when a rule is chained, it's rewritten URL is passed as input URL to the \n" +"next rule, until a matching rule is found.\n" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:288 +msgid "Test" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:323 +msgid "Test redirection rules" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:301 +msgid "Test URL" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:304 +msgid "Check inactive rules?" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:305 +msgid "If 'yes', inactive rules will also be tested" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:313 +msgid "Close" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:314 +msgid "Test rules" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:123 +msgid "No currently defined redirection rule." +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:371 +msgid "No matching rule!" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:366 +msgid "Input URL" +msgstr "" + +#: ./src/pyams_content/features/redirect/zmi/container.py:367 +msgid "Output URL" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:39 +msgid "Active rule?" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:40 +msgid "If 'no', selected rule is inactive" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:44 +msgid "Chained rule?" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:45 +msgid "" +"If 'no', and if this rule is matching received request URL, the rule returns " +"a redirection response; otherwise, the rule just rewrites the input URL which" +" is forwarded to the next rule" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:51 +msgid "Permanent redirect?" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:52 +msgid "Define if this redirection should be permanent or temporary" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:57 +msgid "Regexp pattern of matching URLs for this redirection rule" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:62 +msgid "Internal redirection target" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:63 +msgid "" +"Internal redirection reference. You can search a reference using '+' followed" +" by internal number, of by entering text matching content title." +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:69 +msgid "URL to which source URL should be redirected" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:75 +msgid "You can only provide an internal reference OR a target URL" +msgstr "" + +#: ./src/pyams_content/features/redirect/interfaces/__init__.py:77 +msgid "You must provide an internal reference OR a target URL" +msgstr "" + #: ./src/pyams_content/features/menu/zmi/__init__.py:81 msgid "Add menu..." msgstr "" diff -r edcf61caaf3b -r 209432f09f9f src/pyams_content/root/__init__.py --- a/src/pyams_content/root/__init__.py Thu Jul 19 10:38:08 2018 +0200 +++ b/src/pyams_content/root/__init__.py Thu Jul 19 16:15:30 2018 +0200 @@ -24,6 +24,7 @@ from pyams_content.features.footer.interfaces import IFooterTarget from pyams_content.features.header.interfaces import IHeaderTarget from pyams_content.features.preview.interfaces import IPreviewTarget +from pyams_content.features.redirect.interfaces import IRedirectionManagerTarget from pyams_content.interfaces import WEBMASTER_ROLE, OPERATOR_ROLE from pyams_content.root.interfaces import ISiteRootRoles, ISiteRootConfiguration, ISiteRoot, \ ISiteRootToolsConfiguration, ISiteRootBackOfficeConfiguration @@ -52,7 +53,8 @@ @implementer(IDefaultProtectionPolicy, ISiteRoot, ISiteRootRoles, IPortalContext, ITagsManagerTarget, - IIllustrationTarget, IHeaderTarget, IFooterTarget, IAlertTarget, IPreviewTarget) + IIllustrationTarget, IHeaderTarget, IFooterTarget, IAlertTarget, IRedirectionManagerTarget, + IPreviewTarget) class SiteRoot(ProtectedObject, BaseSiteRoot, UserSkinnableContent): """Main site root"""