src/pyams_portal/zmi/layout.py
changeset 5 670b7956c689
child 17 3f879876f7a6
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/layout.py	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,519 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__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, IPortalPortletsConfiguration, IPortalContext, IPortalPage, \
+    MANAGE_TEMPLATE_PERMISSION
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu, IMenuHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu, IContentManagementMenu
+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.zmi.template import PortalTemplateHeaderAdapter
+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
+from pyams_utils.registry import query_utility
+from pyams_utils.traversing import get_parent
+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, Interface
+
+from pyams_portal import _
+
+
+@adapter_config(context=(IPortalTemplate, IContentManagementMenu), provides=IMenuHeader)
+class PortalTemplateMenuHeader(object):
+    """Portal template menu header"""
+
+    def __init__(self, context, menu):
+        self.context = context
+        self.menu = menu
+
+    @property
+    def header(self):
+        return _("Template management")
+
+
+@viewlet_config(name='template-properties.menu', context=IPortalTemplate, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, 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=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/layout.pt', layer=IAdminLayer)
+@implementer(IInnerPage)
+class PortalTemplateLayoutView(AdminView):
+    """Portal template main layout configuration view"""
+
+    @property
+    def title(self):
+        container = get_parent(self.context, IPortalTemplateContainer)
+        if container is None:
+            context = get_parent(self.context, IPortalContext)
+            page = IPortalPage(context)
+            if page.use_local_template:
+                return _("Local template configuration")
+            else:
+                translate = self.request.localizer.translate
+                return translate(_("Shared template configuration ({0})")).format(page.template.name)
+        else:
+            return _("Template configuration")
+
+    def get_template(self):
+        return self.context
+
+    def get_context(self):
+        return self.context
+
+    @property
+    def can_change(self):
+        return self.request.has_permission(MANAGE_TEMPLATE_PERMISSION)
+
+    @reify
+    def template_configuration(self):
+        return IPortalTemplateConfiguration(self.get_template())
+
+    @reify
+    def portlet_configuration(self):
+        return IPortalPortletsConfiguration(self.get_context())
+
+    @property
+    def selected_portlets(self):
+        container = query_utility(IPortalTemplateContainer)
+        configuration = IPortalTemplateContainerConfiguration(container)
+        return filter(lambda x: x is not None,
+                      [query_utility(IPortlet, name=portlet_name)
+                       for portlet_name in configuration.toolbar_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, portlet_id):
+        settings = self.portlet_configuration.get_portlet_configuration(portlet_id).settings
+        previewer = self.request.registry.queryMultiAdapter((self.get_context(), self.request, self, settings),
+                                                            IPortletPreviewer)
+        if previewer is not None:
+            previewer.update()
+            return previewer.render()
+        else:
+            return ''
+
+
+@adapter_config(context=(IPortalTemplate, IAdminLayer, Interface), provides=IPageHeader)
+class PortalTemplateLayoutHeaderAdapter(PortalTemplateHeaderAdapter):
+    """Portal template configuration header adapter"""
+
+    back_url = '/admin.html#portal-templates.html'
+    back_target = None
+
+
+#
+# Rows views
+#
+
+@viewlet_config(name='add-template-row.menu', context=IPortalTemplate, layer=IAdminLayer,
+                view=PortalTemplateLayoutView, manager=IToolbarAddingMenu,
+                permission=MANAGE_TEMPLATE_PERMISSION, 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=MANAGE_TEMPLATE_PERMISSION, 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=MANAGE_TEMPLATE_PERMISSION, 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=MANAGE_TEMPLATE_PERMISSION, 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=PortalTemplateLayoutView, manager=IToolbarAddingMenu,
+                permission=MANAGE_TEMPLATE_PERMISSION, 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=MANAGE_TEMPLATE_PERMISSION)
+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')
+        if self.widgets['row_id'].value:
+            self.widgets['row_id'].mode = HIDDEN_MODE
+
+    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=MANAGE_TEMPLATE_PERMISSION, 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=MANAGE_TEMPLATE_PERMISSION, 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=VIEW_SYSTEM_PERMISSION, 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=MANAGE_TEMPLATE_PERMISSION, 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=VIEW_SYSTEM_PERMISSION)
+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")
+    fields = field.Fields(ISlotConfiguration).omit('portlet_ids')
+
+    label_css_class = 'control-label col-md-5'
+    input_css_class = 'col-md-7'
+
+    ajax_handler = 'slot-properties.json'
+    edit_permission = MANAGE_TEMPLATE_PERMISSION
+
+    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=MANAGE_TEMPLATE_PERMISSION, 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=MANAGE_TEMPLATE_PERMISSION, 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=PortalTemplateLayoutView, manager=IToolbarAddingMenu,
+                permission=MANAGE_TEMPLATE_PERMISSION, weight=10)
+class PortalTemplateAddMenuDivider(ToolbarMenuDivider):
+    """Portal template menu divider"""
+
+
+@viewlet_config(name='add-template-portlet.menu', context=IPortalTemplate, layer=IAdminLayer,
+                view=PortalTemplateLayoutView, manager=IToolbarAddingMenu,
+                permission=MANAGE_TEMPLATE_PERMISSION, 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=MANAGE_TEMPLATE_PERMISSION)
+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=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+class PortalTemplatePortletAJAXAddForm(AJAXAddForm, PortalTemplatePortletAddForm):
+    """Portal template portlet add form, AJAX handler"""
+
+    def get_ajax_output(self, changes):
+        configuration = IPortalPortletsConfiguration(self.context)
+        settings = configuration.get_portlet_configuration(changes['portlet_id']).settings
+        previewer = self.request.registry.queryMultiAdapter((self.context, self.request, self, settings),
+                                                            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=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def drag_template_portlet(request):
+    """Drag portlet icon to slot"""
+    tmpl_config = IPortalTemplateConfiguration(request.context)
+    portlets_config = IPortalPortletsConfiguration(request.context)
+    portlet_name = request.params.get('portlet_name')
+    slot_name = request.params.get('slot_name')
+    changes = tmpl_config.add_portlet(portlet_name, slot_name)
+    settings = portlets_config.get_portlet_configuration(changes['portlet_id']).settings
+    previewer = request.registry.queryMultiAdapter((request.context, request, request, settings),
+                                                   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=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_template_portlet_order(request):
+    """Set template portlet order"""
+    order = json.loads(request.params.get('order'))
+    order['from'] = int(order['from'])
+    order['to']['portlet_ids'] = list(map(int, order['to']['portlet_ids']))
+    IPortalTemplateConfiguration(request.context).set_portlet_order(order)
+    return {'status': 'success'}
+
+
+@view_config(name='portlet-properties.html', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION)
+class PortalTemplatePortletEditForm(AdminDialogEditForm):
+    """Portal template portlet edit form"""
+
+    dialog_class = 'modal-large'
+
+    def __call__(self):
+        request = self.request
+        request.registry.notify(PageletCreatedEvent(self))
+        portlet_id = int(request.params.get('form.widgets.portlet_id'))
+        portlet_config = IPortalPortletsConfiguration(self.context).get_portlet_configuration(portlet_id)
+        if portlet_config is None:
+            raise NotFound()
+        editor = self.request.registry.queryMultiAdapter((portlet_config.editor_settings, request),
+                                                         IPagelet, name='properties.html')
+        if editor is None:
+            raise NotFound()
+        request.registry.notify(PageletCreatedEvent(editor))
+        editor.ajax_handler = 'portlet-properties.json'
+        editor.update()
+        return editor()
+
+
+@view_config(name='portlet-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, 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))
+        # load portlet config
+        portlet_id = int(request.params.get('form.widgets.portlet_id'))
+        portlet_config = IPortalPortletsConfiguration(self.context).get_portlet_configuration(portlet_id)
+        if portlet_config is None:
+            raise NotFound()
+        # check inheritance
+        old_override = portlet_config.inherit_parent
+        new_override = request.params.get('form.widgets.override_parent')
+        if new_override:
+            portlet_config.inherit_parent = False
+        else:
+            portlet_config.inherit_parent = True
+        changed_override = portlet_config.inherit_parent != old_override
+        # update settings
+        editor = request.registry.queryMultiAdapter((portlet_config.editor_settings, request),
+                                                    IPagelet, name='properties.json')
+        if editor is None:
+            raise NotFound()
+        changes = editor()
+        translate = self.request.localizer.translate
+        if changed_override or 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.settings),
+                                                           IPortletPreviewer)
+            if previewer is not None:
+                previewer.update()
+                changes.update({'status': 'success',
+                                'message': translate(self.successMessage),
+                                'callback': 'PyAMS_portal.template.editPortletCallback',
+                                'options': {'portlet_id': portlet_id,
+                                            'preview': previewer.render()}})
+        return changes
+
+
+@view_config(name='delete-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def delete_template_portlet(request):
+    """Delete template portlet"""
+    config = IPortalTemplateConfiguration(request.context)
+    config.delete_portlet(int(request.params.get('portlet_id')))
+    return {'status': 'success'}