diff -r 000000000000 -r 6f99128c6d48 src/pyams_portal/template.py --- /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 +# 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__)