diff -r 000000000000 -r 6f99128c6d48 src/pyams_portal/zmi/template/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_portal/zmi/template/config.py Wed Jun 17 09:58:33 2015 +0200 @@ -0,0 +1,483 @@ +# +# Copyright (c) 2008-2015 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_skin.page import DefaultPageHeaderAdapter + +__docformat__ = 'restructuredtext' + + +# import standard library +import json + +# import interfaces +from pyams_pagelet.interfaces import IPagelet, PageletCreatedEvent +from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, ISlot, \ + IPortletAddingInfo, IPortlet, ISlotConfiguration, IPortletPreviewer, IPortalTemplateContainer, \ + IPortalTemplateContainerConfiguration +from pyams_skin.interfaces import IInnerPage, IPageHeader, IContentTitle +from pyams_skin.interfaces.viewlet import IToolbarAddingMenu +from pyams_skin.layer import IPyAMSLayer +from pyams_workflow.interfaces import IWorkflowState, IWorkflowVersions +from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu +from pyams_zmi.layer import IAdminLayer +from transaction.interfaces import ITransactionManager +from z3c.form.interfaces import IDataExtractedEvent, HIDDEN_MODE + +# import packages +from pyams_form.form import AJAXAddForm, AJAXEditForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_portal.workflow import STATUS_LABELS, STATUS_IDS, PUBLISHED, ARCHIVED +from pyams_skin.viewlet.menu import MenuItem +from pyams_skin.viewlet.toolbar import JsToolbarMenuItem, ToolbarMenuDivider, ToolbarMenuItem +from pyams_template.template import template_config +from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter +from pyams_utils.registry import query_utility +from pyams_viewlet.manager import viewletmanager_config +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm +from pyams_zmi.view import AdminView +from pyramid.decorator import reify +from pyramid.events import subscriber +from pyramid.exceptions import NotFound +from pyramid.view import view_config +from z3c.form import field +from zope.interface import implementer, Invalid + +from pyams_portal import _ + + +@viewlet_config(name='template-properties.menu', context=IPortalTemplate, layer=IAdminLayer, + manager=ISiteManagementMenu, permission='system.view', weight=1) +@viewletmanager_config(name='template-properties.menu', layer=IAdminLayer, provides=IPropertiesMenu) +@implementer(IPropertiesMenu) +class PortalTemplatePropertiesMenu(MenuItem): + """Portal template properties menu""" + + label = _("Properties") + icon_class = 'fa-twitch' + url = '#properties.html' + + +@pagelet_config(name='properties.html', context=IPortalTemplate, layer=IPyAMSLayer, permission='system.view') +@template_config(template='templates/config.pt', layer=IAdminLayer) +@implementer(IInnerPage) +class PortalTemplateConfigView(AdminView): + """Portal template configuration view""" + + title = _("Shared portal template configuration") + + def get_context(self): + return self.context + + @property + def can_change(self): + return self.request.has_permission('portal.templates.manage') and \ + IWorkflowState(self.get_context()).state not in (PUBLISHED, ARCHIVED) + + @reify + def configuration(self): + return IPortalTemplateConfiguration(self.get_context()) + + @property + def selected_portlets(self): + container = query_utility(IPortalTemplateContainer) + configuration = IPortalTemplateContainerConfiguration(container) + return [query_utility(IPortlet, name=portlet_name) for portlet_name in configuration.selected_portlets or ()] + + def get_portlet(self, name): + return self.request.registry.getUtility(IPortlet, name=name) + + def get_portlet_label(self, name): + return self.request.localizer.translate(self.get_portlet(name).label) + + def get_portlet_preview(self, slot_name, position): + portlet_config = self.configuration.get_portlet_configuration(slot_name, position) + previewer = self.request.registry.queryMultiAdapter((self.get_context(), self.request, self, portlet_config), + IPortletPreviewer) + if previewer is not None: + previewer.update() + return previewer.render() + else: + return '' + + +@adapter_config(context=(IPortalTemplate, IAdminLayer, PortalTemplateConfigView), provides=IPageHeader) +class PortalTemplateConfigHeaderAdapter(DefaultPageHeaderAdapter): + """Portal template configuration header adapter""" + + back_url = '/admin.html#portal-templates.html' + back_target = None + + icon_class = 'fa fa-fw fa-columns' + subtitle = _("Portlets configuration") + + +# +# Rows views +# + +@viewlet_config(name='add-template-row.menu', context=IPortalTemplate, layer=IAdminLayer, + view=PortalTemplateConfigView, manager=IToolbarAddingMenu, + permission='portal.templates.manage', weight=1) +class PortalTemplateRowAddMenu(JsToolbarMenuItem): + """Portal template row add menu""" + + label = _("Add row...") + label_css_class = 'fa fa-fw fa-indent' + url = 'PyAMS_portal.template.addRow' + + +@view_config(name='add-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def add_template_row(request): + """Add template raw""" + config = IPortalTemplateConfiguration(request.context) + return {'row_id': config.add_row()} + + +@view_config(name='set-template-row-order.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def set_template_row_order(request): + """Set template rows order""" + config = IPortalTemplateConfiguration(request.context) + row_ids = map(int, json.loads(request.params.get('rows'))) + config.set_row_order(row_ids) + return {'status': 'success'} + + +@view_config(name='delete-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def delete_template_row(request): + """Delete template row""" + config = IPortalTemplateConfiguration(request.context) + config.delete_row(int(request.params.get('row_id'))) + return {'status': 'success'} + + +# +# Slots views +# + +@viewlet_config(name='add-template-slot.menu', context=IPortalTemplate, layer=IAdminLayer, + view=PortalTemplateConfigView, manager=IToolbarAddingMenu, + permission='portal.templates.manage', weight=2) +class PortalTemplateSlotAddMenu(ToolbarMenuItem): + """Portal template slot add menu""" + + label = _("Add slot...") + label_css_class = 'fa fa-fw fa-columns' + url = 'add-template-slot.html' + modal_target = True + + +@pagelet_config(name='add-template-slot.html', context=IPortalTemplate, layer=IPyAMSLayer, + permission='portal.templates.manage') +class PortalTemplateSlotAddForm(AdminDialogAddForm): + """Portal template slot add form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("« {0} » portal template")).format(self.context.name) + + legend = _("Add slot") + icon_css_class = 'fa fa-fw fa-columns' + + fields = field.Fields(ISlot) + ajax_handler = 'add-template-slot.json' + edit_permission = None + + def updateWidgets(self, prefix=None): + super(PortalTemplateSlotAddForm, self).updateWidgets() + self.widgets['row_id'].value = self.request.params.get('form.widgets.row_id') + + def createAndAdd(self, data): + config = IPortalTemplateConfiguration(self.context) + return config.add_slot(data.get('name'), data.get('row_id')) + + +@subscriber(IDataExtractedEvent, form_selector=PortalTemplateSlotAddForm) +def handle_new_slot_data_extraction(event): + """Handle new slot form data extraction""" + config = IPortalTemplateConfiguration(event.form.context) + name = event.data.get('name') + if name in config.slot_names: + event.form.widgets.errors += (Invalid(_("Specified name is already used!")),) + + +@view_config(name='add-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +class PortalTemplateSlotAJAXAddForm(AJAXAddForm, PortalTemplateSlotAddForm): + """Portal template slot add form, AJAX handler""" + + def get_ajax_output(self, changes): + return {'status': 'callback', + 'callback': 'PyAMS_portal.template.addSlotCallback', + 'options': {'row_id': changes[0], + 'slot_name': changes[1]}} + + +@view_config(name='set-template-slot-order.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def set_template_slot_order(request): + """Set template slots order""" + config = IPortalTemplateConfiguration(request.context) + order = json.loads(request.params.get('order')) + for key in order.copy().keys(): + order[int(key)] = order.pop(key) + config.set_slot_order(order) + return {'status': 'success'} + + +@view_config(name='get-slots-width.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='system.view', renderer='json', xhr=True) +def get_template_slots_width(request): + """Get template slots width""" + config = IPortalTemplateConfiguration(request.context) + return config.get_slots_width(request.params.get('device')) + + +@view_config(name='set-slot-width.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def set_template_slot_width(request): + """Set template slot width""" + config = IPortalTemplateConfiguration(request.context) + config.set_slot_width(request.params.get('slot_name'), + request.params.get('device'), + int(request.params.get('width'))) + return config.get_slots_width(request.params.get('device')) + + +@pagelet_config(name='slot-properties.html', context=IPortalTemplate, layer=IPyAMSLayer, permission='system.view') +class PortalTemplateSlotPropertiesEditForm(AdminDialogEditForm): + """Slot properties edit form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("« {0} » portal template - {1} slot")).format(self.context.name, + self.getContent().slot_name) + + legend = _("Edit slot properties") + + label_css_class = 'control-label col-md-5' + input_css_class = 'col-md-7' + + @property + def fields(self): + fields = field.Fields(ISlotConfiguration) + if not self.getContent().can_inherit: + fields = fields.omit('inherit_parent') + return fields + + ajax_handler = 'slot-properties.json' + edit_permission = 'portal.templates.manage' + + def __init__(self, context, request): + super(PortalTemplateSlotPropertiesEditForm, self).__init__(context, request) + self.config = IPortalTemplateConfiguration(context) + + def getContent(self): + slot_name = self.request.params.get('form.widgets.slot_name') + return self.config.slot_config[slot_name] + + def updateWidgets(self, prefix=None): + super(PortalTemplateSlotPropertiesEditForm, self).updateWidgets(prefix) + self.widgets['slot_name'].mode = HIDDEN_MODE + + +@view_config(name='slot-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +class PortalTemplateSlotPropertiesAJAXEditForm(AJAXEditForm, PortalTemplateSlotPropertiesEditForm): + """Slot properties edit form, AJAX renderer""" + + def get_ajax_output(self, changes): + if changes: + slot_name = self.widgets['slot_name'].value + slot_config = self.config.slot_config[slot_name] + return {'status': 'success', + 'callback': 'PyAMS_portal.template.editSlotCallback', + 'options': {'slot_name': slot_name, + 'width': slot_config.get_width()}} + else: + return super(PortalTemplateSlotPropertiesAJAXEditForm, self).get_ajax_output(changes) + + +@view_config(name='delete-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def delete_template_slot(request): + """Delete template slot""" + config = IPortalTemplateConfiguration(request.context) + config.delete_slot(request.params.get('slot_name')) + return {'status': 'success'} + + +# +# Portlet views +# + +@viewlet_config(name='add-template-portlet.divider', context=IPortalTemplate, layer=IAdminLayer, + view=PortalTemplateConfigView, manager=IToolbarAddingMenu, + permission='portal.templates.manage', weight=10) +class PortalTemplateAddMenuDivider(ToolbarMenuDivider): + """Portal template menu divider""" + + +@viewlet_config(name='add-template-portlet.menu', context=IPortalTemplate, layer=IAdminLayer, + view=PortalTemplateConfigView, manager=IToolbarAddingMenu, + permission='portal.templates.manage', weight=20) +class PortalTemplatePortletAddMenu(ToolbarMenuItem): + """Portal template portlet add menu""" + + label = _("Add portlet...") + label_css_class = 'fa fa-fw fa-columns' + url = 'add-template-portlet.html' + modal_target = True + + +@pagelet_config(name='add-template-portlet.html', context=IPortalTemplate, layer=IPyAMSLayer, + permission='portal.templates.manage') +class PortalTemplatePortletAddForm(AdminDialogAddForm): + """Portal template portlet add form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("« {0} » portal template")).format(self.context.name) + + legend = _("Add portlet") + icon_css_class = 'fa fa-fw fa-columns' + + fields = field.Fields(IPortletAddingInfo) + ajax_handler = 'add-template-portlet.json' + edit_permission = None + + def createAndAdd(self, data): + config = IPortalTemplateConfiguration(self.context) + return config.add_portlet(data.get('portlet_name'), data.get('slot_name')) + + +@view_config(name='add-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +class PortalTemplatePortletAJAXAddForm(AJAXAddForm, PortalTemplatePortletAddForm): + """Portal template portlet add form, AJAX handler""" + + def get_ajax_output(self, changes): + config = IPortalTemplateConfiguration(self.context) + portlet_config = config.get_portlet_configuration(changes['slot_name'], changes['position']) + previewer = self.request.registry.queryMultiAdapter((self.context, self.request, self, portlet_config), + IPortletPreviewer) + if previewer is not None: + previewer.update() + changes['preview'] = previewer.render() + return {'status': 'callback', + 'callback': 'PyAMS_portal.template.addPortletCallback', + 'options': changes} + + +@view_config(name='drag-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def drag_template_portlet(request): + """Drag portlet icon to slot""" + config = IPortalTemplateConfiguration(request.context) + portlet_name = request.params.get('portlet_name') + slot_name = request.params.get('slot_name') + changes = config.add_portlet(portlet_name, slot_name) + portlet_config = config.get_portlet_configuration(changes['slot_name'], changes['position']) + previewer = request.registry.queryMultiAdapter((request.context, request, request, portlet_config), + IPortletPreviewer) + if previewer is not None: + previewer.update() + changes['preview'] = previewer.render() + return {'status': 'callback', + 'close_form': False, + 'callback': 'PyAMS_portal.template.addPortletCallback', + 'options': changes} + + +@view_config(name='set-template-portlet-order.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def set_template_portlet_order(request): + """Set template portlet order""" + config = IPortalTemplateConfiguration(request.context) + order = json.loads(request.params.get('order')) + order['from']['position'] = int(order['from']['position']) + order['to']['positions'] = list(map(int, order['to']['positions'])) + config.set_portlet_order(order) + return {'status': 'success'} + + +@view_config(name='portlet-properties.html', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='system.view') +class PortalTemplatePortletEditForm(AdminDialogEditForm): + """Portal template portlet edit form""" + + dialog_class = 'modal-large' + + def __call__(self): + request = self.request + request.registry.notify(PageletCreatedEvent(self)) + slot_name = request.params.get('form.widgets.slot_name') + position = int(request.params.get('form.widgets.position')) + config = IPortalTemplateConfiguration(self.context) + portlet_config = config.get_portlet_configuration(slot_name, position) + if portlet_config is None: + raise NotFound() + editor = self.request.registry.queryMultiAdapter((portlet_config, request), + IPagelet, name='properties.html') + if editor is None: + raise NotFound() + editor.ajax_handler = 'portlet-properties.json' + editor.update() + return editor() + + +@view_config(name='portlet-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +class PortalTemplatePortletAJAXEditForm(AJAXEditForm, PortalTemplatePortletEditForm): + """Portal template portlet edit form, AJAX renderer""" + + def __call__(self): + request = self.request + request.registry.notify(PageletCreatedEvent(self)) + slot_name = request.params.get('form.widgets.slot_name') + position = int(request.params.get('form.widgets.position')) + config = IPortalTemplateConfiguration(self.context) + portlet_config = config.get_portlet_configuration(slot_name, position) + if portlet_config is None: + raise NotFound() + editor = request.registry.queryMultiAdapter((portlet_config, request), + IPagelet, name='properties.json') + if editor is None: + raise NotFound() + changes = editor() + if changes: + # we commit before loading previewer to avoid BLOBs "uncommited changes" error + ITransactionManager(self.context).commit() + previewer = request.registry.queryMultiAdapter((self.context, request, self, portlet_config), + IPortletPreviewer) + if previewer is not None: + previewer.update() + changes.update({'status': 'callback', + 'callback': 'PyAMS_portal.template.editPortletCallback', + 'options': {'slot_name': slot_name, + 'position': position, + 'preview': previewer.render()}}) + return changes + + +@view_config(name='delete-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer, + permission='portal.templates.manage', renderer='json', xhr=True) +def delete_template_portlet(request): + """Delete template portlet""" + config = IPortalTemplateConfiguration(request.context) + config.delete_portlet(request.params.get('slot_name'), int(request.params.get('position'))) + return {'status': 'success'}