|
1 # |
|
2 # Copyright (c) 2008-2015 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_skin.page import DefaultPageHeaderAdapter |
|
13 |
|
14 __docformat__ = 'restructuredtext' |
|
15 |
|
16 |
|
17 # import standard library |
|
18 import json |
|
19 |
|
20 # import interfaces |
|
21 from pyams_pagelet.interfaces import IPagelet, PageletCreatedEvent |
|
22 from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, ISlot, \ |
|
23 IPortletAddingInfo, IPortlet, ISlotConfiguration, IPortletPreviewer, IPortalTemplateContainer, \ |
|
24 IPortalTemplateContainerConfiguration |
|
25 from pyams_skin.interfaces import IInnerPage, IPageHeader, IContentTitle |
|
26 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu |
|
27 from pyams_skin.layer import IPyAMSLayer |
|
28 from pyams_workflow.interfaces import IWorkflowState, IWorkflowVersions |
|
29 from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu |
|
30 from pyams_zmi.layer import IAdminLayer |
|
31 from transaction.interfaces import ITransactionManager |
|
32 from z3c.form.interfaces import IDataExtractedEvent, HIDDEN_MODE |
|
33 |
|
34 # import packages |
|
35 from pyams_form.form import AJAXAddForm, AJAXEditForm |
|
36 from pyams_pagelet.pagelet import pagelet_config |
|
37 from pyams_portal.workflow import STATUS_LABELS, STATUS_IDS, PUBLISHED, ARCHIVED |
|
38 from pyams_skin.viewlet.menu import MenuItem |
|
39 from pyams_skin.viewlet.toolbar import JsToolbarMenuItem, ToolbarMenuDivider, ToolbarMenuItem |
|
40 from pyams_template.template import template_config |
|
41 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter |
|
42 from pyams_utils.registry import query_utility |
|
43 from pyams_viewlet.manager import viewletmanager_config |
|
44 from pyams_viewlet.viewlet import viewlet_config |
|
45 from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm |
|
46 from pyams_zmi.view import AdminView |
|
47 from pyramid.decorator import reify |
|
48 from pyramid.events import subscriber |
|
49 from pyramid.exceptions import NotFound |
|
50 from pyramid.view import view_config |
|
51 from z3c.form import field |
|
52 from zope.interface import implementer, Invalid |
|
53 |
|
54 from pyams_portal import _ |
|
55 |
|
56 |
|
57 @viewlet_config(name='template-properties.menu', context=IPortalTemplate, layer=IAdminLayer, |
|
58 manager=ISiteManagementMenu, permission='system.view', weight=1) |
|
59 @viewletmanager_config(name='template-properties.menu', layer=IAdminLayer, provides=IPropertiesMenu) |
|
60 @implementer(IPropertiesMenu) |
|
61 class PortalTemplatePropertiesMenu(MenuItem): |
|
62 """Portal template properties menu""" |
|
63 |
|
64 label = _("Properties") |
|
65 icon_class = 'fa-twitch' |
|
66 url = '#properties.html' |
|
67 |
|
68 |
|
69 @pagelet_config(name='properties.html', context=IPortalTemplate, layer=IPyAMSLayer, permission='system.view') |
|
70 @template_config(template='templates/config.pt', layer=IAdminLayer) |
|
71 @implementer(IInnerPage) |
|
72 class PortalTemplateConfigView(AdminView): |
|
73 """Portal template configuration view""" |
|
74 |
|
75 title = _("Shared portal template configuration") |
|
76 |
|
77 def get_context(self): |
|
78 return self.context |
|
79 |
|
80 @property |
|
81 def can_change(self): |
|
82 return self.request.has_permission('portal.templates.manage') and \ |
|
83 IWorkflowState(self.get_context()).state not in (PUBLISHED, ARCHIVED) |
|
84 |
|
85 @reify |
|
86 def configuration(self): |
|
87 return IPortalTemplateConfiguration(self.get_context()) |
|
88 |
|
89 @property |
|
90 def selected_portlets(self): |
|
91 container = query_utility(IPortalTemplateContainer) |
|
92 configuration = IPortalTemplateContainerConfiguration(container) |
|
93 return [query_utility(IPortlet, name=portlet_name) for portlet_name in configuration.selected_portlets or ()] |
|
94 |
|
95 def get_portlet(self, name): |
|
96 return self.request.registry.getUtility(IPortlet, name=name) |
|
97 |
|
98 def get_portlet_label(self, name): |
|
99 return self.request.localizer.translate(self.get_portlet(name).label) |
|
100 |
|
101 def get_portlet_preview(self, slot_name, position): |
|
102 portlet_config = self.configuration.get_portlet_configuration(slot_name, position) |
|
103 previewer = self.request.registry.queryMultiAdapter((self.get_context(), self.request, self, portlet_config), |
|
104 IPortletPreviewer) |
|
105 if previewer is not None: |
|
106 previewer.update() |
|
107 return previewer.render() |
|
108 else: |
|
109 return '' |
|
110 |
|
111 |
|
112 @adapter_config(context=(IPortalTemplate, IAdminLayer, PortalTemplateConfigView), provides=IPageHeader) |
|
113 class PortalTemplateConfigHeaderAdapter(DefaultPageHeaderAdapter): |
|
114 """Portal template configuration header adapter""" |
|
115 |
|
116 back_url = '/admin.html#portal-templates.html' |
|
117 back_target = None |
|
118 |
|
119 icon_class = 'fa fa-fw fa-columns' |
|
120 subtitle = _("Portlets configuration") |
|
121 |
|
122 |
|
123 # |
|
124 # Rows views |
|
125 # |
|
126 |
|
127 @viewlet_config(name='add-template-row.menu', context=IPortalTemplate, layer=IAdminLayer, |
|
128 view=PortalTemplateConfigView, manager=IToolbarAddingMenu, |
|
129 permission='portal.templates.manage', weight=1) |
|
130 class PortalTemplateRowAddMenu(JsToolbarMenuItem): |
|
131 """Portal template row add menu""" |
|
132 |
|
133 label = _("Add row...") |
|
134 label_css_class = 'fa fa-fw fa-indent' |
|
135 url = 'PyAMS_portal.template.addRow' |
|
136 |
|
137 |
|
138 @view_config(name='add-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
139 permission='portal.templates.manage', renderer='json', xhr=True) |
|
140 def add_template_row(request): |
|
141 """Add template raw""" |
|
142 config = IPortalTemplateConfiguration(request.context) |
|
143 return {'row_id': config.add_row()} |
|
144 |
|
145 |
|
146 @view_config(name='set-template-row-order.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
147 permission='portal.templates.manage', renderer='json', xhr=True) |
|
148 def set_template_row_order(request): |
|
149 """Set template rows order""" |
|
150 config = IPortalTemplateConfiguration(request.context) |
|
151 row_ids = map(int, json.loads(request.params.get('rows'))) |
|
152 config.set_row_order(row_ids) |
|
153 return {'status': 'success'} |
|
154 |
|
155 |
|
156 @view_config(name='delete-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
157 permission='portal.templates.manage', renderer='json', xhr=True) |
|
158 def delete_template_row(request): |
|
159 """Delete template row""" |
|
160 config = IPortalTemplateConfiguration(request.context) |
|
161 config.delete_row(int(request.params.get('row_id'))) |
|
162 return {'status': 'success'} |
|
163 |
|
164 |
|
165 # |
|
166 # Slots views |
|
167 # |
|
168 |
|
169 @viewlet_config(name='add-template-slot.menu', context=IPortalTemplate, layer=IAdminLayer, |
|
170 view=PortalTemplateConfigView, manager=IToolbarAddingMenu, |
|
171 permission='portal.templates.manage', weight=2) |
|
172 class PortalTemplateSlotAddMenu(ToolbarMenuItem): |
|
173 """Portal template slot add menu""" |
|
174 |
|
175 label = _("Add slot...") |
|
176 label_css_class = 'fa fa-fw fa-columns' |
|
177 url = 'add-template-slot.html' |
|
178 modal_target = True |
|
179 |
|
180 |
|
181 @pagelet_config(name='add-template-slot.html', context=IPortalTemplate, layer=IPyAMSLayer, |
|
182 permission='portal.templates.manage') |
|
183 class PortalTemplateSlotAddForm(AdminDialogAddForm): |
|
184 """Portal template slot add form""" |
|
185 |
|
186 @property |
|
187 def title(self): |
|
188 translate = self.request.localizer.translate |
|
189 return translate(_("« {0} » portal template")).format(self.context.name) |
|
190 |
|
191 legend = _("Add slot") |
|
192 icon_css_class = 'fa fa-fw fa-columns' |
|
193 |
|
194 fields = field.Fields(ISlot) |
|
195 ajax_handler = 'add-template-slot.json' |
|
196 edit_permission = None |
|
197 |
|
198 def updateWidgets(self, prefix=None): |
|
199 super(PortalTemplateSlotAddForm, self).updateWidgets() |
|
200 self.widgets['row_id'].value = self.request.params.get('form.widgets.row_id') |
|
201 |
|
202 def createAndAdd(self, data): |
|
203 config = IPortalTemplateConfiguration(self.context) |
|
204 return config.add_slot(data.get('name'), data.get('row_id')) |
|
205 |
|
206 |
|
207 @subscriber(IDataExtractedEvent, form_selector=PortalTemplateSlotAddForm) |
|
208 def handle_new_slot_data_extraction(event): |
|
209 """Handle new slot form data extraction""" |
|
210 config = IPortalTemplateConfiguration(event.form.context) |
|
211 name = event.data.get('name') |
|
212 if name in config.slot_names: |
|
213 event.form.widgets.errors += (Invalid(_("Specified name is already used!")),) |
|
214 |
|
215 |
|
216 @view_config(name='add-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
217 permission='portal.templates.manage', renderer='json', xhr=True) |
|
218 class PortalTemplateSlotAJAXAddForm(AJAXAddForm, PortalTemplateSlotAddForm): |
|
219 """Portal template slot add form, AJAX handler""" |
|
220 |
|
221 def get_ajax_output(self, changes): |
|
222 return {'status': 'callback', |
|
223 'callback': 'PyAMS_portal.template.addSlotCallback', |
|
224 'options': {'row_id': changes[0], |
|
225 'slot_name': changes[1]}} |
|
226 |
|
227 |
|
228 @view_config(name='set-template-slot-order.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
229 permission='portal.templates.manage', renderer='json', xhr=True) |
|
230 def set_template_slot_order(request): |
|
231 """Set template slots order""" |
|
232 config = IPortalTemplateConfiguration(request.context) |
|
233 order = json.loads(request.params.get('order')) |
|
234 for key in order.copy().keys(): |
|
235 order[int(key)] = order.pop(key) |
|
236 config.set_slot_order(order) |
|
237 return {'status': 'success'} |
|
238 |
|
239 |
|
240 @view_config(name='get-slots-width.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
241 permission='system.view', renderer='json', xhr=True) |
|
242 def get_template_slots_width(request): |
|
243 """Get template slots width""" |
|
244 config = IPortalTemplateConfiguration(request.context) |
|
245 return config.get_slots_width(request.params.get('device')) |
|
246 |
|
247 |
|
248 @view_config(name='set-slot-width.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
249 permission='portal.templates.manage', renderer='json', xhr=True) |
|
250 def set_template_slot_width(request): |
|
251 """Set template slot width""" |
|
252 config = IPortalTemplateConfiguration(request.context) |
|
253 config.set_slot_width(request.params.get('slot_name'), |
|
254 request.params.get('device'), |
|
255 int(request.params.get('width'))) |
|
256 return config.get_slots_width(request.params.get('device')) |
|
257 |
|
258 |
|
259 @pagelet_config(name='slot-properties.html', context=IPortalTemplate, layer=IPyAMSLayer, permission='system.view') |
|
260 class PortalTemplateSlotPropertiesEditForm(AdminDialogEditForm): |
|
261 """Slot properties edit form""" |
|
262 |
|
263 @property |
|
264 def title(self): |
|
265 translate = self.request.localizer.translate |
|
266 return translate(_("« {0} » portal template - {1} slot")).format(self.context.name, |
|
267 self.getContent().slot_name) |
|
268 |
|
269 legend = _("Edit slot properties") |
|
270 |
|
271 label_css_class = 'control-label col-md-5' |
|
272 input_css_class = 'col-md-7' |
|
273 |
|
274 @property |
|
275 def fields(self): |
|
276 fields = field.Fields(ISlotConfiguration) |
|
277 if not self.getContent().can_inherit: |
|
278 fields = fields.omit('inherit_parent') |
|
279 return fields |
|
280 |
|
281 ajax_handler = 'slot-properties.json' |
|
282 edit_permission = 'portal.templates.manage' |
|
283 |
|
284 def __init__(self, context, request): |
|
285 super(PortalTemplateSlotPropertiesEditForm, self).__init__(context, request) |
|
286 self.config = IPortalTemplateConfiguration(context) |
|
287 |
|
288 def getContent(self): |
|
289 slot_name = self.request.params.get('form.widgets.slot_name') |
|
290 return self.config.slot_config[slot_name] |
|
291 |
|
292 def updateWidgets(self, prefix=None): |
|
293 super(PortalTemplateSlotPropertiesEditForm, self).updateWidgets(prefix) |
|
294 self.widgets['slot_name'].mode = HIDDEN_MODE |
|
295 |
|
296 |
|
297 @view_config(name='slot-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
298 permission='portal.templates.manage', renderer='json', xhr=True) |
|
299 class PortalTemplateSlotPropertiesAJAXEditForm(AJAXEditForm, PortalTemplateSlotPropertiesEditForm): |
|
300 """Slot properties edit form, AJAX renderer""" |
|
301 |
|
302 def get_ajax_output(self, changes): |
|
303 if changes: |
|
304 slot_name = self.widgets['slot_name'].value |
|
305 slot_config = self.config.slot_config[slot_name] |
|
306 return {'status': 'success', |
|
307 'callback': 'PyAMS_portal.template.editSlotCallback', |
|
308 'options': {'slot_name': slot_name, |
|
309 'width': slot_config.get_width()}} |
|
310 else: |
|
311 return super(PortalTemplateSlotPropertiesAJAXEditForm, self).get_ajax_output(changes) |
|
312 |
|
313 |
|
314 @view_config(name='delete-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
315 permission='portal.templates.manage', renderer='json', xhr=True) |
|
316 def delete_template_slot(request): |
|
317 """Delete template slot""" |
|
318 config = IPortalTemplateConfiguration(request.context) |
|
319 config.delete_slot(request.params.get('slot_name')) |
|
320 return {'status': 'success'} |
|
321 |
|
322 |
|
323 # |
|
324 # Portlet views |
|
325 # |
|
326 |
|
327 @viewlet_config(name='add-template-portlet.divider', context=IPortalTemplate, layer=IAdminLayer, |
|
328 view=PortalTemplateConfigView, manager=IToolbarAddingMenu, |
|
329 permission='portal.templates.manage', weight=10) |
|
330 class PortalTemplateAddMenuDivider(ToolbarMenuDivider): |
|
331 """Portal template menu divider""" |
|
332 |
|
333 |
|
334 @viewlet_config(name='add-template-portlet.menu', context=IPortalTemplate, layer=IAdminLayer, |
|
335 view=PortalTemplateConfigView, manager=IToolbarAddingMenu, |
|
336 permission='portal.templates.manage', weight=20) |
|
337 class PortalTemplatePortletAddMenu(ToolbarMenuItem): |
|
338 """Portal template portlet add menu""" |
|
339 |
|
340 label = _("Add portlet...") |
|
341 label_css_class = 'fa fa-fw fa-columns' |
|
342 url = 'add-template-portlet.html' |
|
343 modal_target = True |
|
344 |
|
345 |
|
346 @pagelet_config(name='add-template-portlet.html', context=IPortalTemplate, layer=IPyAMSLayer, |
|
347 permission='portal.templates.manage') |
|
348 class PortalTemplatePortletAddForm(AdminDialogAddForm): |
|
349 """Portal template portlet add form""" |
|
350 |
|
351 @property |
|
352 def title(self): |
|
353 translate = self.request.localizer.translate |
|
354 return translate(_("« {0} » portal template")).format(self.context.name) |
|
355 |
|
356 legend = _("Add portlet") |
|
357 icon_css_class = 'fa fa-fw fa-columns' |
|
358 |
|
359 fields = field.Fields(IPortletAddingInfo) |
|
360 ajax_handler = 'add-template-portlet.json' |
|
361 edit_permission = None |
|
362 |
|
363 def createAndAdd(self, data): |
|
364 config = IPortalTemplateConfiguration(self.context) |
|
365 return config.add_portlet(data.get('portlet_name'), data.get('slot_name')) |
|
366 |
|
367 |
|
368 @view_config(name='add-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
369 permission='portal.templates.manage', renderer='json', xhr=True) |
|
370 class PortalTemplatePortletAJAXAddForm(AJAXAddForm, PortalTemplatePortletAddForm): |
|
371 """Portal template portlet add form, AJAX handler""" |
|
372 |
|
373 def get_ajax_output(self, changes): |
|
374 config = IPortalTemplateConfiguration(self.context) |
|
375 portlet_config = config.get_portlet_configuration(changes['slot_name'], changes['position']) |
|
376 previewer = self.request.registry.queryMultiAdapter((self.context, self.request, self, portlet_config), |
|
377 IPortletPreviewer) |
|
378 if previewer is not None: |
|
379 previewer.update() |
|
380 changes['preview'] = previewer.render() |
|
381 return {'status': 'callback', |
|
382 'callback': 'PyAMS_portal.template.addPortletCallback', |
|
383 'options': changes} |
|
384 |
|
385 |
|
386 @view_config(name='drag-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
387 permission='portal.templates.manage', renderer='json', xhr=True) |
|
388 def drag_template_portlet(request): |
|
389 """Drag portlet icon to slot""" |
|
390 config = IPortalTemplateConfiguration(request.context) |
|
391 portlet_name = request.params.get('portlet_name') |
|
392 slot_name = request.params.get('slot_name') |
|
393 changes = config.add_portlet(portlet_name, slot_name) |
|
394 portlet_config = config.get_portlet_configuration(changes['slot_name'], changes['position']) |
|
395 previewer = request.registry.queryMultiAdapter((request.context, request, request, portlet_config), |
|
396 IPortletPreviewer) |
|
397 if previewer is not None: |
|
398 previewer.update() |
|
399 changes['preview'] = previewer.render() |
|
400 return {'status': 'callback', |
|
401 'close_form': False, |
|
402 'callback': 'PyAMS_portal.template.addPortletCallback', |
|
403 'options': changes} |
|
404 |
|
405 |
|
406 @view_config(name='set-template-portlet-order.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
407 permission='portal.templates.manage', renderer='json', xhr=True) |
|
408 def set_template_portlet_order(request): |
|
409 """Set template portlet order""" |
|
410 config = IPortalTemplateConfiguration(request.context) |
|
411 order = json.loads(request.params.get('order')) |
|
412 order['from']['position'] = int(order['from']['position']) |
|
413 order['to']['positions'] = list(map(int, order['to']['positions'])) |
|
414 config.set_portlet_order(order) |
|
415 return {'status': 'success'} |
|
416 |
|
417 |
|
418 @view_config(name='portlet-properties.html', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
419 permission='system.view') |
|
420 class PortalTemplatePortletEditForm(AdminDialogEditForm): |
|
421 """Portal template portlet edit form""" |
|
422 |
|
423 dialog_class = 'modal-large' |
|
424 |
|
425 def __call__(self): |
|
426 request = self.request |
|
427 request.registry.notify(PageletCreatedEvent(self)) |
|
428 slot_name = request.params.get('form.widgets.slot_name') |
|
429 position = int(request.params.get('form.widgets.position')) |
|
430 config = IPortalTemplateConfiguration(self.context) |
|
431 portlet_config = config.get_portlet_configuration(slot_name, position) |
|
432 if portlet_config is None: |
|
433 raise NotFound() |
|
434 editor = self.request.registry.queryMultiAdapter((portlet_config, request), |
|
435 IPagelet, name='properties.html') |
|
436 if editor is None: |
|
437 raise NotFound() |
|
438 editor.ajax_handler = 'portlet-properties.json' |
|
439 editor.update() |
|
440 return editor() |
|
441 |
|
442 |
|
443 @view_config(name='portlet-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
444 permission='portal.templates.manage', renderer='json', xhr=True) |
|
445 class PortalTemplatePortletAJAXEditForm(AJAXEditForm, PortalTemplatePortletEditForm): |
|
446 """Portal template portlet edit form, AJAX renderer""" |
|
447 |
|
448 def __call__(self): |
|
449 request = self.request |
|
450 request.registry.notify(PageletCreatedEvent(self)) |
|
451 slot_name = request.params.get('form.widgets.slot_name') |
|
452 position = int(request.params.get('form.widgets.position')) |
|
453 config = IPortalTemplateConfiguration(self.context) |
|
454 portlet_config = config.get_portlet_configuration(slot_name, position) |
|
455 if portlet_config is None: |
|
456 raise NotFound() |
|
457 editor = request.registry.queryMultiAdapter((portlet_config, request), |
|
458 IPagelet, name='properties.json') |
|
459 if editor is None: |
|
460 raise NotFound() |
|
461 changes = editor() |
|
462 if changes: |
|
463 # we commit before loading previewer to avoid BLOBs "uncommited changes" error |
|
464 ITransactionManager(self.context).commit() |
|
465 previewer = request.registry.queryMultiAdapter((self.context, request, self, portlet_config), |
|
466 IPortletPreviewer) |
|
467 if previewer is not None: |
|
468 previewer.update() |
|
469 changes.update({'status': 'callback', |
|
470 'callback': 'PyAMS_portal.template.editPortletCallback', |
|
471 'options': {'slot_name': slot_name, |
|
472 'position': position, |
|
473 'preview': previewer.render()}}) |
|
474 return changes |
|
475 |
|
476 |
|
477 @view_config(name='delete-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer, |
|
478 permission='portal.templates.manage', renderer='json', xhr=True) |
|
479 def delete_template_portlet(request): |
|
480 """Delete template portlet""" |
|
481 config = IPortalTemplateConfiguration(request.context) |
|
482 config.delete_portlet(request.params.get('slot_name'), int(request.params.get('position'))) |
|
483 return {'status': 'success'} |