src/pyams_portal/template.py
changeset 0 6f99128c6d48
child 5 670b7956c689
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/template.py	Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,441 @@
+#
+# 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 interfaces
+from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, IPortalContext, IPortalPage, \
+    IPortletConfiguration, IPortlet, IPortalTemplateContainer, IPortalWfTemplate, IPortalTemplateContainerConfiguration
+from pyams_workflow.interfaces import IWorkflowVersions
+from zope.annotation.interfaces import IAnnotations
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from persistent.list import PersistentList
+from persistent.mapping import PersistentMapping
+from pyams_portal.slot import SlotConfiguration
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import get_local_registry
+from pyams_utils.request import check_request
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.componentvocabulary.vocabulary import UtilityVocabulary
+from zope.container.contained import Contained
+from zope.container.folder import Folder
+from zope.copy import clone
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location.location import locate
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+
+@implementer(IPortalTemplateContainer)
+class PortalTemplateContainer(Folder):
+    """Portal template container"""
+
+
+@implementer(IPortalTemplateContainerConfiguration)
+class PortalTemplateContainerConfiguration(Persistent, Contained):
+    """Portal template container configuration"""
+
+    selected_portlets = FieldProperty(IPortalTemplateContainerConfiguration['selected_portlets'])
+
+
+PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY = 'pyams_portal.container.configuration'
+
+
+@adapter_config(context=IPortalTemplateContainer, provides=IPortalTemplateContainerConfiguration)
+def PortalTemplateContainerConfigurationFactory(context):
+    """Portal template container configuration factory"""
+    annotations = IAnnotations(context)
+    config = annotations.get(PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY)
+    if config is None:
+        config = annotations[PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY] = PortalTemplateContainerConfiguration()
+        get_current_registry().notify(ObjectCreatedEvent(config))
+        locate(config, context)
+    return config
+
+
+@implementer(IPortalTemplate)
+class PortalTemplate(Persistent, Contained):
+    """Portal template persistent class"""
+
+    name = FieldProperty(IPortalTemplate['name'])
+
+
+@implementer(IPortalWfTemplate)
+class PortalWfTemplate(Persistent, Contained):
+    """Portal template workflow manager class"""
+
+    content_class = PortalTemplate
+    workflow_name = 'PyAMS portal template workflow'
+    view_permission = None
+
+
+@subscriber(IObjectAddedEvent, context_selector=IPortalWfTemplate)
+def handle_added_template(event):
+    """Register shared template"""
+    registry = get_local_registry()
+    if (registry is not None) and IPortalTemplateContainer.providedBy(event.newParent):
+        registry.registerUtility(event.object, IPortalWfTemplate, name=event.object.__name__)
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IPortalWfTemplate)
+def handle_removed_template(event):
+    """Unregister removed template"""
+    registry = get_local_registry()
+    if (registry is not None) and IPortalTemplateContainer.providedBy(event.oldParent):
+        registry.unregisterUtility(event.object, IPortalWfTemplate, name=event.object.__name__)
+
+
+class PortalTemplatesVocabulary(UtilityVocabulary):
+    """Portal templates vocabulary"""
+
+    interface = IPortalWfTemplate
+    nameOnly = True
+
+getVocabularyRegistry().register('PyAMS portal templates', PortalTemplatesVocabulary)
+
+
+@implementer(IPortalTemplateConfiguration)
+class PortalTemplateConfiguration(Persistent, Contained):
+    """Portal template configuration"""
+
+    rows = FieldProperty(IPortalTemplateConfiguration['rows'])
+    _slot_names = FieldProperty(IPortalTemplateConfiguration['slot_names'])
+    _slot_order = FieldProperty(IPortalTemplateConfiguration['slot_order'])
+    _slots = FieldProperty(IPortalTemplateConfiguration['slots'])
+    slot_config = FieldProperty(IPortalTemplateConfiguration['slot_config'])
+    portlet_config = FieldProperty(IPortalTemplateConfiguration['portlet_config'])
+
+    def __init__(self):
+        self._slot_names = PersistentList()
+        self._slot_order = PersistentMapping()
+        self._slot_order[0] = PersistentList()
+        self._slots = PersistentMapping()
+        self._slots[0] = PersistentMapping()
+        self.slot_config = PersistentMapping()
+        self.portlet_config = PersistentMapping()
+
+    def add_row(self):
+        """Add new row and return last row index (0 based)"""
+        self.rows += 1
+        last_index = self.rows - 1
+        self.slot_order[last_index] = PersistentList()
+        self.slots[last_index] = PersistentMapping()
+        return last_index
+
+    def set_row_order(self, order):
+        """Change template row order"""
+        if not isinstance(order, (list, tuple)):
+            order = list(order)
+        old_slot_order = self.slot_order
+        old_slots = self.slots
+        assert len(order) == self.rows
+        new_slot_order = PersistentMapping()
+        new_slots = PersistentMapping()
+        for index, row_id in enumerate(order):
+            new_slot_order[index] = old_slot_order.get(row_id) or PersistentList()
+            new_slots[index] = old_slots.get(row_id) or PersistentMapping()
+        if self.slot_order != new_slot_order:
+            self.slot_order = new_slot_order
+            self.slots = new_slots
+
+    def delete_row(self, row_id):
+        """Delete template row"""
+        assert row_id in self.slots
+        for slot_name in self.slots.get(row_id, {}).keys():
+            if slot_name in self.slot_names:
+                self.slot_names.remove(slot_name)
+            if slot_name in self.slot_config:
+                del self.slot_config[slot_name]
+            if slot_name in self.portlet_config:
+                del self.portlet_config[slot_name]
+        for index in range(row_id, self.rows-1):
+            self.slot_order[index] = self.slot_order[index+1]
+            self.slots[index] = self.slots[index+1]
+        if self.rows > 0:
+            del self.slot_order[self.rows-1]
+            del self.slots[self.rows-1]
+        self.rows -= 1
+
+    @property
+    def slot_names(self):
+        if IPortalTemplate.providedBy(self.__parent__):
+            return self._slot_names
+        else:
+            return IPortalTemplateConfiguration(self.__parent__).slot_names
+
+    @slot_names.setter
+    def slot_names(self, value):
+        self._slot_names = value
+
+    @property
+    def slot_order(self):
+        if IPortalTemplate.providedBy(self.__parent__):
+            return self._slot_order
+        else:
+            return IPortalTemplateConfiguration(self.__parent__).slot_order
+
+    @slot_order.setter
+    def slot_order(self, value):
+        self._slot_order = value
+
+    @property
+    def slots(self):
+        if IPortalTemplate.providedBy(self.__parent__):
+            return self._slots
+        else:
+            return IPortalTemplateConfiguration(self.__parent__).slots
+
+    @slots.setter
+    def slots(self, value):
+        self._slots = value
+
+    def add_slot(self, slot_name, row_id=None):
+        assert slot_name not in self.slot_names
+        self.slot_names.append(slot_name)
+        if row_id is None:
+            row_id = 0
+        # init slots order
+        if row_id not in self.slot_order:
+            self.slot_order[row_id] = PersistentList()
+        self.slot_order[row_id].append(slot_name)
+        # init slots portlets
+        if row_id not in self.slots:
+            self.slots[row_id] = PersistentMapping()
+        self.slots[row_id][slot_name] = PersistentList()
+        # init slots configuration
+        slot = self.slot_config[slot_name] = SlotConfiguration(slot_name)
+        locate(slot, self.__parent__)
+        return row_id, slot_name
+
+    def set_slot_order(self, order):
+        """Set slots order"""
+        old_slot_order = self.slot_order
+        old_slots = self.slots
+        new_slot_order = PersistentMapping()
+        new_slots = PersistentMapping()
+        for row_id in sorted(map(int, order.keys())):
+            new_slot_order[row_id] = PersistentList(order[row_id])
+            new_slots[row_id] = PersistentMapping()
+            for slot_name in order[row_id]:
+                old_row_id = self.get_slot_row(slot_name)
+                new_slots[row_id][slot_name] = old_slots[old_row_id][slot_name]
+        if new_slot_order != old_slot_order:
+            self.slot_order = new_slot_order
+            self.slots = new_slots
+
+    def get_slot_row(self, slot_name):
+        for row_id in self.slot_order:
+            if slot_name in self.slot_order[row_id]:
+                return row_id
+
+    def get_slots(self, row_id):
+        """Get ordered slots list"""
+        return self.slot_order.get(row_id, [])
+
+    def get_slots_width(self, device=None):
+        """Get slots width"""
+        result = {}
+        for slot_name, config in self.slot_config.items():
+            result[slot_name] = config.get_width(device)
+        return result
+
+    def set_slot_width(self, slot_name, device, width):
+        """Set slot width"""
+        self.slot_config[slot_name].set_width(width, device)
+
+    def get_slot_configuration(self, slot_name):
+        """Get slot configuration"""
+        if slot_name not in self.slot_names:
+            return None
+        config = self.slot_config.get(slot_name)
+        if config is None:
+            if IPortalTemplate.providedBy(self.__parent__):
+                config = SlotConfiguration()
+            else:
+                config = clone(IPortalTemplateConfiguration(self.__parent__).get_slot_configuration(slot_name))
+                config.inherit_parent = True
+            self.slot_config[slot_name] = config
+            locate(config, self.__parent__)
+        return config
+
+    def delete_slot(self, slot_name):
+        """Delete slot and associated portlets"""
+        assert slot_name in self.slot_names
+        row_id = self.get_slot_row(slot_name)
+        del self.portlet_config[slot_name]
+        del self.slot_config[slot_name]
+        del self.slots[row_id][slot_name]
+        self.slot_order[row_id].remove(slot_name)
+        self.slot_names.remove(slot_name)
+
+    def add_portlet(self, portlet_name, slot_name):
+        """Add portlet to given slot"""
+        assert slot_name in self.slot_names
+        row_id = self.get_slot_row(slot_name)
+        if slot_name not in self.slots.get(row_id):
+            self.slots[row_id][slot_name] = PersistentList()
+        self.slots[row_id][slot_name].append(portlet_name)
+        if slot_name not in self.portlet_config:
+            self.portlet_config[slot_name] = PersistentMapping()
+        position = len(self.slots[row_id][slot_name]) - 1
+        portlet = get_current_registry().getUtility(IPortlet, name=portlet_name)
+        config = IPortletConfiguration(portlet)
+        config.slot_name = slot_name
+        config.position = position
+        locate(config, self.__parent__, '++portlet++{0}::{1}'.format(slot_name, position))
+        self.portlet_config[slot_name][position] = config
+        return {'portlet_name': portlet_name,
+                'slot_name': slot_name,
+                'position': position,
+                'label': check_request().localizer.translate(portlet.label)}
+
+    def set_portlet_order(self, order):
+        """Set portlet order"""
+        source = order['from']
+        source_slot = source['slot']
+        source_row = self.get_slot_row(source_slot)
+        target = order['to']
+        target_slot = target['slot']
+        target_row = self.get_slot_row(target_slot)
+        portlet_config = self.portlet_config
+        old_config = portlet_config[source_slot].pop(source['position'])
+        target_config = PersistentMapping()
+        for index, (slot_name, portlet_name, position) in enumerate(zip(target['slots'], target['names'],
+                                                                        target['positions'])):
+            if (slot_name == source_slot) and (position == source['position']):
+                target_config[index] = old_config
+            else:
+                target_config[index] = portlet_config[slot_name][position]
+            target_config[index].slot_name = target_slot
+            target_config[index].position = index
+            locate(target_config[index], self.__parent__, '++portlet++{0}::{1}'.format(slot_name, index))
+        portlet_config[target_slot] = target_config
+        # re-order source portlets
+        config = portlet_config[source_slot]
+        for index, key in enumerate(sorted(config)):
+            if index != key:
+                config[index] = config.pop(key)
+                config[index].position = index
+                locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(source_slot, index))
+        # re-order target portlets
+        if target_slot != source_slot:
+            config = portlet_config[target_slot]
+            for index, key in enumerate(sorted(config)):
+                config[index] = config.pop(key)
+                config[index].position = index
+                locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(target_slot, index))
+        self.portlet_config = portlet_config
+        del self.slots[source_row][source_slot][source['position']]
+        self.slots[target_row][target_slot] = PersistentList(target['names'])
+
+    def get_portlet_configuration(self, slot_name, position):
+        """Get portlet configuration"""
+        if slot_name not in self.slot_names:
+            return None
+        config = self.portlet_config.get(slot_name, {}).get(position)
+        if config is None:
+            if IPortalTemplate.providedBy(self.__parent__):
+                portlet_name = self.slots[slot_name][position]
+                portlet = get_current_registry().queryUtility(IPortlet, name=portlet_name)
+                config = IPortletConfiguration(portlet)
+            else:
+                config = clone(IPortalTemplateConfiguration(self.__parent__).get_portlet_configuration(slot_name,
+                                                                                                       position))
+                config.inherit_parent = True
+            self.portlet_config[slot_name][position] = config
+            locate(config, self.__parent__)
+        return config
+
+    def delete_portlet(self, slot_name, position):
+        """Delete portlet"""
+        assert slot_name in self.slot_names
+        row_id = self.get_slot_row(slot_name)
+        config = self.portlet_config[slot_name]
+        del config[position]
+        if len(config) and (position < max(tuple(config.keys()))):
+            for index, key in enumerate(sorted(config)):
+                config[index] = config.pop(key)
+                config[index].position = index
+                locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(slot_name, index))
+        del self.slots[row_id][slot_name][position]
+
+
+class PortalTemplateSlotsVocabulary(SimpleVocabulary):
+    """Portal template slots vocabulary"""
+
+    def __init__(self, context):
+        config = IPortalTemplateConfiguration(context)
+        terms = [SimpleTerm(slot_name) for slot_name in sorted(config.slot_names)]
+        super(PortalTemplateSlotsVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS template slots', PortalTemplateSlotsVocabulary)
+
+
+@adapter_config(name='portlet', context=IPortalTemplate, provides=ITraversable)
+class PortalTemplatePortletTraverser(ContextAdapter):
+    """++portlet++ namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        config = IPortalTemplateConfiguration(self.context)
+        if name:
+            slot_name, position = name.split('::')
+            return config.get_portlet_configuration(slot_name, int(position))
+        else:
+            return config
+
+
+TEMPLATE_CONFIGURATION_KEY = 'pyams_portal.template'
+
+
+@adapter_config(context=IPortalTemplate, provides=IPortalTemplateConfiguration)
+def PortalTemplateConfigurationFactory(context):
+    """Portal template configuration factory"""
+    annotations = IAnnotations(context)
+    config = annotations.get(TEMPLATE_CONFIGURATION_KEY)
+    if config is None:
+        config = annotations[TEMPLATE_CONFIGURATION_KEY] = PortalTemplateConfiguration()
+        get_current_registry().notify(ObjectCreatedEvent(config))
+        locate(config, context)
+    return config
+
+
+@adapter_config(context=IPortalContext, provides=IPortalTemplateConfiguration)
+def PortalContextConfigurationFactory(context):
+    """Portal context configuration factory"""
+    page = IPortalPage(context)
+    if page.use_local_template:
+        template = IWorkflowVersions(page.template).get_last_versions()[0]
+        config = IPortalTemplateConfiguration(template)
+    else:
+        annotations = IAnnotations(context)
+        config = annotations.get(TEMPLATE_CONFIGURATION_KEY)
+        if config is None:
+            # we clone template configuration
+            config = annotations[TEMPLATE_CONFIGURATION_KEY] = clone(IPortalTemplateConfiguration(page.template))
+            get_current_registry().notify(ObjectCreatedEvent(config))
+            locate(config, context)
+    return config
+
+
+@adapter_config(context=IPortletConfiguration, provides=IPortalTemplateConfiguration)
+def PortalPortletConfigurationFactory(context):
+    """Portal portlet configuration factory"""
+    return IPortalTemplateConfiguration(context.__parent__)