src/pyams_content/features/redirect/zmi/container.py
changeset 864 209432f09f9f
child 869 ae7f0471e337
equal deleted inserted replaced
863:edcf61caaf3b 864:209432f09f9f
       
     1 #
       
     2 # Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 from pyams_form.form import ajax_config, AJAXAddForm
       
    13 from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager
       
    14 from pyams_form.schema import CloseButton
       
    15 from pyams_skin.help import ContentHelp
       
    16 from pyams_skin.interfaces.viewlet import IToolbarViewletManager
       
    17 from pyams_skin.skin import apply_skin
       
    18 from pyams_skin.viewlet.toolbar import ToolbarAction
       
    19 from pyams_template.template import template_config
       
    20 from pyams_utils.request import copy_request
       
    21 from pyams_zmi.form import AdminDialogAddForm
       
    22 
       
    23 __docformat__ = 'restructuredtext'
       
    24 
       
    25 
       
    26 # import standard library
       
    27 import json
       
    28 
       
    29 # import interfaces
       
    30 from pyams_content.features.redirect.interfaces import IRedirectionManagerTarget, IRedirectionManager
       
    31 from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION
       
    32 from pyams_i18n.interfaces import II18n
       
    33 from pyams_sequence.interfaces import ISequentialIdInfo
       
    34 from pyams_skin.interfaces import IPageHeader, IUserSkinnable, IContentHelp
       
    35 from pyams_skin.layer import IPyAMSLayer
       
    36 from pyams_zmi.interfaces.menu import ISiteManagementMenu
       
    37 from pyams_zmi.layer import IAdminLayer
       
    38 from z3c.table.interfaces import IValues, IColumn
       
    39 
       
    40 # import packages
       
    41 from pyams_content.skin import pyams_content
       
    42 from pyams_pagelet.pagelet import pagelet_config
       
    43 from pyams_skin.page import DefaultPageHeaderAdapter
       
    44 from pyams_skin.table import BaseTable, SorterColumn, VisibilitySwitcherColumn, TrashColumn, I18nColumn
       
    45 from pyams_skin.viewlet.menu import MenuItem
       
    46 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
       
    47 from pyams_utils.fanstatic import get_resource_path
       
    48 from pyams_utils.url import absolute_url
       
    49 from pyams_viewlet.viewlet import viewlet_config, Viewlet
       
    50 from pyams_zmi.view import ContainerAdminView
       
    51 from pyramid.decorator import reify
       
    52 from pyramid.exceptions import NotFound
       
    53 from pyramid.view import view_config
       
    54 from z3c.form import field, button
       
    55 from z3c.table.column import GetAttrColumn
       
    56 from zope.interface import Interface
       
    57 from zope.schema import TextLine, Bool
       
    58 
       
    59 from pyams_content import _
       
    60 
       
    61 
       
    62 @viewlet_config(name='redirections.menu', context=IRedirectionManagerTarget, layer=IPyAMSLayer,
       
    63                 manager=ISiteManagementMenu, permission=MANAGE_SITE_ROOT_PERMISSION, weight=35)
       
    64 class RedirectionMenu(MenuItem):
       
    65     """Redirection manager menu"""
       
    66 
       
    67     label = _("Redirections")
       
    68     icon_class = 'fa-map-signs'
       
    69     url = '#redirections.html'
       
    70 
       
    71 
       
    72 class RedirectionsContainerTable(BaseTable):
       
    73     """Redirections container table"""
       
    74 
       
    75     prefix = 'redirections'
       
    76 
       
    77     hide_header = True
       
    78     sortOn = None
       
    79 
       
    80     cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight table-dnd'}
       
    81 
       
    82     @property
       
    83     def data_attributes(self):
       
    84         attributes = super(RedirectionsContainerTable, self).data_attributes
       
    85         attributes.setdefault('table', {}).update({
       
    86             'data-ams-plugins': 'pyams_content',
       
    87             'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
       
    88             'data-ams-location': absolute_url(IRedirectionManager(self.context), self.request),
       
    89             'data-ams-tablednd-drag-handle': 'td.sorter',
       
    90             'data-ams-tablednd-drop-target': 'set-rules-order.json',
       
    91             'data-ams-active-icon-on': 'fa fa-fw fa-check-square-o',
       
    92             'data-ams-active-icon-off': 'fa fa-fw fa-square-o txt-color-silver opacity-75',
       
    93             'data-ams-chained-icon-on': 'fa fa-fw fa-chain',
       
    94             'data-ams-chained-icon-off': 'fa fa-fw fa-chain txt-color-silver opacity-50'
       
    95         })
       
    96         attributes.setdefault('td', {}).update({
       
    97             'data-ams-attribute-switcher': self.get_switcher_target,
       
    98             'data-ams-switcher-attribute-name': self.get_switcher_attribute
       
    99         })
       
   100         return attributes
       
   101 
       
   102     @staticmethod
       
   103     def get_switcher_target(element, column):
       
   104         if column.__name__ == 'enable-disable':
       
   105             return 'switch-rule-activity.json'
       
   106         elif column.__name__ == 'chain-unchain':
       
   107             return 'switch-rule-chain.json'
       
   108 
       
   109     @staticmethod
       
   110     def get_switcher_attribute(element, column):
       
   111         if column.__name__ == 'enable-disable':
       
   112             return 'active'
       
   113         elif column.__name__ == 'chain-unchain':
       
   114             return 'chained'
       
   115 
       
   116     @reify
       
   117     def values(self):
       
   118         return list(super(RedirectionsContainerTable, self).values)
       
   119 
       
   120     def render(self):
       
   121         if not self.values:
       
   122             translate = self.request.localizer.translate
       
   123             return translate(_("No currently defined redirection rule."))
       
   124         return super(RedirectionsContainerTable, self).render()
       
   125 
       
   126 
       
   127 @adapter_config(context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), provides=IValues)
       
   128 class RedirectionsContainerValues(ContextRequestViewAdapter):
       
   129     """Redirections container values"""
       
   130 
       
   131     @property
       
   132     def values(self):
       
   133         return IRedirectionManager(self.context).values()
       
   134 
       
   135 
       
   136 @adapter_config(name='sorter', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable),
       
   137                 provides=IColumn)
       
   138 class RedirectionsContainerSorterColumn(SorterColumn):
       
   139     """Redirections container sorter column"""
       
   140 
       
   141 
       
   142 @view_config(name='set-rules-order.json', context=IRedirectionManager, request_type=IPyAMSLayer,
       
   143              permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
       
   144 def set_rules_order(request):
       
   145     """Update redirection rules order"""
       
   146     order = list(map(str, json.loads(request.params.get('names'))))
       
   147     request.context.updateOrder(order)
       
   148     return {'status': 'success'}
       
   149 
       
   150 
       
   151 @adapter_config(name='enable-disable', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable),
       
   152                 provides=IColumn)
       
   153 class RedirectionsContainerShowHideColumn(VisibilitySwitcherColumn):
       
   154     """Redirections container activity switcher column"""
       
   155 
       
   156     switch_attribute = 'active'
       
   157     visible_icon_class = 'fa fa-fw fa-check-square-o'
       
   158     hidden_icon_class = 'fa fa-fw fa-square-o txt-color-silver opacity-75'
       
   159 
       
   160     icon_hint = _("Enable/disable rule")
       
   161 
       
   162     url = 'MyAMS.container.switchElementAttribute'
       
   163     weight = 6
       
   164 
       
   165 
       
   166 @view_config(name='switch-rule-activity.json', context=IRedirectionManager, request_type=IPyAMSLayer,
       
   167              permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
       
   168 def switch_rule_activity(request):
       
   169     """Switch rule activity"""
       
   170     container = IRedirectionManager(request.context)
       
   171     rule = container.get(str(request.params.get('object_name')))
       
   172     if rule is None:
       
   173         raise NotFound()
       
   174     rule.active = not rule.active
       
   175     return {'on': rule.active}
       
   176 
       
   177 
       
   178 @adapter_config(name='chain-unchain', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable),
       
   179                 provides=IColumn)
       
   180 class RedirectionsContainerChainedColumn(VisibilitySwitcherColumn):
       
   181     """Redirections container chained switcher column"""
       
   182 
       
   183     switch_attribute = 'chained'
       
   184     visible_icon_class = 'fa fa-fw fa-chain'
       
   185     hidden_icon_class = 'fa fa-fw fa-chain txt-color-silver opacity-50'
       
   186 
       
   187     icon_hint = _("Chain/unchain rule")
       
   188 
       
   189     url = 'MyAMS.container.switchElementAttribute'
       
   190     weight = 7
       
   191 
       
   192 
       
   193 @view_config(name='switch-rule-chain.json', context=IRedirectionManager, request_type=IPyAMSLayer,
       
   194              permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
       
   195 def switch_rule_chain(request):
       
   196     """Switch rule chain"""
       
   197     container = IRedirectionManager(request.context)
       
   198     rule = container.get(str(request.params.get('object_name')))
       
   199     if rule is None:
       
   200         raise NotFound()
       
   201     rule.chained = not rule.chained
       
   202     return {'on': rule.chained}
       
   203 
       
   204 
       
   205 @adapter_config(name='name', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable),
       
   206                 provides=IColumn)
       
   207 class RedirectionsContainerNameColumn(I18nColumn, GetAttrColumn):
       
   208     """Redirections container name column"""
       
   209 
       
   210     _header = _("URL pattern")
       
   211     attrName = 'url_pattern'
       
   212     weight = 10
       
   213 
       
   214 
       
   215 @adapter_config(name='target', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable),
       
   216                 provides=IColumn)
       
   217 class RedirectionsContainerTargetColumn(I18nColumn, GetAttrColumn):
       
   218     """Redirections container target column"""
       
   219 
       
   220     _header = _("Target")
       
   221     attrName = 'target_url'
       
   222     weight = 20
       
   223 
       
   224     def getValue(self, obj):
       
   225         if obj.reference:
       
   226             target = obj.target
       
   227             return '{0} ({1})'.format(II18n(target).query_attribute('title', request=self.request),
       
   228                                       ISequentialIdInfo(target).get_short_oid())
       
   229         else:
       
   230             return super(RedirectionsContainerTargetColumn, self).getValue(obj)
       
   231 
       
   232 
       
   233 @adapter_config(name='trash', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable),
       
   234                 provides=IColumn)
       
   235 class RedirectionsContainerTrashColumn(TrashColumn):
       
   236     """Redirections container trash column"""
       
   237 
       
   238     permission = MANAGE_SITE_ROOT_PERMISSION
       
   239 
       
   240 
       
   241 @pagelet_config(name='redirections.html', context=IRedirectionManagerTarget, layer=IPyAMSLayer,
       
   242                 permission=MANAGE_SITE_ROOT_PERMISSION)
       
   243 class RedirectionsContainerView(ContainerAdminView):
       
   244     """Redirections container view"""
       
   245 
       
   246     title = _("Redirections list")
       
   247     table_class = RedirectionsContainerTable
       
   248 
       
   249 
       
   250 @adapter_config(context=(IRedirectionManagerTarget, IAdminLayer, RedirectionsContainerView), provides=IPageHeader)
       
   251 class RedirectionsContainerViewHeaderAdapter(DefaultPageHeaderAdapter):
       
   252     """Redirections container view header adapter"""
       
   253 
       
   254     icon_class = 'fa fa-fw fa-map-signs'
       
   255 
       
   256 
       
   257 @adapter_config(context=(IRedirectionManagerTarget, IAdminLayer, RedirectionsContainerView), provides=IContentHelp)
       
   258 class RedirectionsContainerHelpAdapter(ContentHelp):
       
   259     """Redirections container help adapter"""
       
   260 
       
   261     header = _("Redirection rules")
       
   262     message = _("""Redirection rules are use to handle redirections responses when a request generates 
       
   263 a famous « 404 NotFound » error.
       
   264 
       
   265 Redirections are particularly useful when you are migrating from a previous site and don't want to lose 
       
   266 your SEO.
       
   267 
       
   268 You can define a set of rules which will be applied to every \"NotFound\" request; rules are based on 
       
   269 regular expressions which are applied to input URL: if the rule is \"matching\", the target URL is rewritten
       
   270 and a \"Redirect\" response is send.
       
   271 
       
   272 You can chain rules together: when a rule is chained, it's rewritten URL is passed as input URL to the 
       
   273 next rule, until a matching rule is found.
       
   274 """)
       
   275     message_format = 'rest'
       
   276 
       
   277 
       
   278 #
       
   279 # Redirections container test form
       
   280 #
       
   281 
       
   282 @viewlet_config(name='test.action', context=IRedirectionManagerTarget, layer=IAdminLayer,
       
   283                 view=RedirectionsContainerView, manager=IToolbarViewletManager,
       
   284                 permission=MANAGE_SITE_ROOT_PERMISSION, weight=75)
       
   285 class RedirectionsContainerTestAction(ToolbarAction):
       
   286     """redirections container test action"""
       
   287 
       
   288     label = _("Test")
       
   289 
       
   290     group_css_class = 'btn-group margin-left-5'
       
   291     label_css_class = 'fa fa-fw fa-magic'
       
   292     css_class = 'btn btn-xs btn-default'
       
   293 
       
   294     url = 'test-redirection-rules.html'
       
   295     modal_target = True
       
   296 
       
   297 
       
   298 class IRedirectionsContainerTestFields(Interface):
       
   299     """Redirections container test fields"""
       
   300 
       
   301     source_url = TextLine(title=_("Test URL"),
       
   302                           required=True)
       
   303 
       
   304     check_inactive_rules = Bool(title=_("Check inactive rules?"),
       
   305                                 description=_("If 'yes', inactive rules will also be tested"),
       
   306                                 required=True,
       
   307                                 default=False)
       
   308 
       
   309 
       
   310 class IRedirectionsContainerTestButtons(Interface):
       
   311     """Redirections container test form buttons"""
       
   312 
       
   313     close = CloseButton(name='close', title=_("Close"))
       
   314     test = button.Button(name='test', title=_("Test rules"))
       
   315 
       
   316 
       
   317 @pagelet_config(name='test-redirection-rules.html', context=IRedirectionManagerTarget, layer=IPyAMSLayer,
       
   318                 permission=MANAGE_SITE_ROOT_PERMISSION)
       
   319 class RedirectionsContainerTestForm(AdminDialogAddForm):
       
   320     """Redirections container test form"""
       
   321 
       
   322     dialog_class = 'modal-max'
       
   323     legend = _("Test redirection rules")
       
   324     icon_css_class = 'fa fa-fw fa-magic'
       
   325 
       
   326     prefix = 'rules_test_form.'
       
   327     fields = field.Fields(IRedirectionsContainerTestFields)
       
   328     buttons = button.Buttons(IRedirectionsContainerTestButtons)
       
   329     ajax_handler = 'test-redirection-rules.json'
       
   330     edit_permission = MANAGE_SITE_ROOT_PERMISSION
       
   331 
       
   332     @property
       
   333     def form_target(self):
       
   334         return '#{0}_test_result'.format(self.id)
       
   335 
       
   336     def updateActions(self):
       
   337         super(RedirectionsContainerTestForm, self).updateActions()
       
   338         if 'test' in self.actions:
       
   339             self.actions['test'].addClass('btn-primary')
       
   340 
       
   341     def createAndAdd(self, data):
       
   342         data = data.get(self, data)
       
   343         request = copy_request(self.request)
       
   344         apply_skin(request, IUserSkinnable(self.context).get_skin())
       
   345         return IRedirectionManager(self.context).test_rules(data['source_url'], request, data['check_inactive_rules'])
       
   346 
       
   347 
       
   348 @viewlet_config(name='test-indexer-process.suffix', layer=IAdminLayer, manager=IWidgetsSuffixViewletsManager,
       
   349                 view=RedirectionsContainerTestForm, weight=50)
       
   350 @template_config(template='templates/manager-test.pt')
       
   351 class RedirectionsContainerTestSuffix(Viewlet):
       
   352     """Redirections container test form suffix"""
       
   353 
       
   354 
       
   355 @view_config(name='test-redirection-rules.json', context=IRedirectionManagerTarget, request_type=IPyAMSLayer,
       
   356              permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
       
   357 class RedirectionsContainerAJAXTestForm(AJAXAddForm, RedirectionsContainerTestForm):
       
   358     """Redirections container test form, JSON renderer"""
       
   359 
       
   360     def get_ajax_output(self, changes):
       
   361         message = []
       
   362         translate = self.request.localizer.translate
       
   363         for rule, source_url, target_url in changes:
       
   364             if not message:
       
   365                 message.append('{:<40}  {:<40}  =>  {:<40}'.format(translate(_("URL pattern")),
       
   366                                                                    translate(_("Input URL")),
       
   367                                                                    translate(_("Output URL"))))
       
   368                 message.append('{:<40}  {:<40}  =>  {:<40}'.format('-' * 40, '-' * 40, '-' * 40))
       
   369             message.append('{:<40}  {:<40}  =>  {:<40}'.format(rule.url_pattern, source_url, target_url))
       
   370         if not message:
       
   371             message.append(translate(_("No matching rule!")))
       
   372         return {
       
   373             'status': 'success',
       
   374             'content': {'html': '\n'.join(message)},
       
   375             'close_form': False
       
   376         }