src/pyams_content/features/redirect/zmi/container.py
changeset 864 209432f09f9f
child 869 ae7f0471e337
--- /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 <tflorac AT ulthar.net>
+# 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
+        }