|
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 } |