src/pyams_portal/template.py
changeset 0 6f99128c6d48
child 5 670b7956c689
equal deleted inserted replaced
-1:000000000000 0:6f99128c6d48
       
     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 
       
    13 __docformat__ = 'restructuredtext'
       
    14 
       
    15 
       
    16 # import standard library
       
    17 
       
    18 # import interfaces
       
    19 from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, IPortalContext, IPortalPage, \
       
    20     IPortletConfiguration, IPortlet, IPortalTemplateContainer, IPortalWfTemplate, IPortalTemplateContainerConfiguration
       
    21 from pyams_workflow.interfaces import IWorkflowVersions
       
    22 from zope.annotation.interfaces import IAnnotations
       
    23 from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent
       
    24 from zope.traversing.interfaces import ITraversable
       
    25 
       
    26 # import packages
       
    27 from persistent import Persistent
       
    28 from persistent.list import PersistentList
       
    29 from persistent.mapping import PersistentMapping
       
    30 from pyams_portal.slot import SlotConfiguration
       
    31 from pyams_utils.adapter import adapter_config, ContextAdapter
       
    32 from pyams_utils.registry import get_local_registry
       
    33 from pyams_utils.request import check_request
       
    34 from pyramid.events import subscriber
       
    35 from pyramid.threadlocal import get_current_registry
       
    36 from zope.componentvocabulary.vocabulary import UtilityVocabulary
       
    37 from zope.container.contained import Contained
       
    38 from zope.container.folder import Folder
       
    39 from zope.copy import clone
       
    40 from zope.interface import implementer
       
    41 from zope.lifecycleevent import ObjectCreatedEvent
       
    42 from zope.location.location import locate
       
    43 from zope.schema.fieldproperty import FieldProperty
       
    44 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
       
    45 
       
    46 
       
    47 @implementer(IPortalTemplateContainer)
       
    48 class PortalTemplateContainer(Folder):
       
    49     """Portal template container"""
       
    50 
       
    51 
       
    52 @implementer(IPortalTemplateContainerConfiguration)
       
    53 class PortalTemplateContainerConfiguration(Persistent, Contained):
       
    54     """Portal template container configuration"""
       
    55 
       
    56     selected_portlets = FieldProperty(IPortalTemplateContainerConfiguration['selected_portlets'])
       
    57 
       
    58 
       
    59 PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY = 'pyams_portal.container.configuration'
       
    60 
       
    61 
       
    62 @adapter_config(context=IPortalTemplateContainer, provides=IPortalTemplateContainerConfiguration)
       
    63 def PortalTemplateContainerConfigurationFactory(context):
       
    64     """Portal template container configuration factory"""
       
    65     annotations = IAnnotations(context)
       
    66     config = annotations.get(PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY)
       
    67     if config is None:
       
    68         config = annotations[PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY] = PortalTemplateContainerConfiguration()
       
    69         get_current_registry().notify(ObjectCreatedEvent(config))
       
    70         locate(config, context)
       
    71     return config
       
    72 
       
    73 
       
    74 @implementer(IPortalTemplate)
       
    75 class PortalTemplate(Persistent, Contained):
       
    76     """Portal template persistent class"""
       
    77 
       
    78     name = FieldProperty(IPortalTemplate['name'])
       
    79 
       
    80 
       
    81 @implementer(IPortalWfTemplate)
       
    82 class PortalWfTemplate(Persistent, Contained):
       
    83     """Portal template workflow manager class"""
       
    84 
       
    85     content_class = PortalTemplate
       
    86     workflow_name = 'PyAMS portal template workflow'
       
    87     view_permission = None
       
    88 
       
    89 
       
    90 @subscriber(IObjectAddedEvent, context_selector=IPortalWfTemplate)
       
    91 def handle_added_template(event):
       
    92     """Register shared template"""
       
    93     registry = get_local_registry()
       
    94     if (registry is not None) and IPortalTemplateContainer.providedBy(event.newParent):
       
    95         registry.registerUtility(event.object, IPortalWfTemplate, name=event.object.__name__)
       
    96 
       
    97 
       
    98 @subscriber(IObjectRemovedEvent, context_selector=IPortalWfTemplate)
       
    99 def handle_removed_template(event):
       
   100     """Unregister removed template"""
       
   101     registry = get_local_registry()
       
   102     if (registry is not None) and IPortalTemplateContainer.providedBy(event.oldParent):
       
   103         registry.unregisterUtility(event.object, IPortalWfTemplate, name=event.object.__name__)
       
   104 
       
   105 
       
   106 class PortalTemplatesVocabulary(UtilityVocabulary):
       
   107     """Portal templates vocabulary"""
       
   108 
       
   109     interface = IPortalWfTemplate
       
   110     nameOnly = True
       
   111 
       
   112 getVocabularyRegistry().register('PyAMS portal templates', PortalTemplatesVocabulary)
       
   113 
       
   114 
       
   115 @implementer(IPortalTemplateConfiguration)
       
   116 class PortalTemplateConfiguration(Persistent, Contained):
       
   117     """Portal template configuration"""
       
   118 
       
   119     rows = FieldProperty(IPortalTemplateConfiguration['rows'])
       
   120     _slot_names = FieldProperty(IPortalTemplateConfiguration['slot_names'])
       
   121     _slot_order = FieldProperty(IPortalTemplateConfiguration['slot_order'])
       
   122     _slots = FieldProperty(IPortalTemplateConfiguration['slots'])
       
   123     slot_config = FieldProperty(IPortalTemplateConfiguration['slot_config'])
       
   124     portlet_config = FieldProperty(IPortalTemplateConfiguration['portlet_config'])
       
   125 
       
   126     def __init__(self):
       
   127         self._slot_names = PersistentList()
       
   128         self._slot_order = PersistentMapping()
       
   129         self._slot_order[0] = PersistentList()
       
   130         self._slots = PersistentMapping()
       
   131         self._slots[0] = PersistentMapping()
       
   132         self.slot_config = PersistentMapping()
       
   133         self.portlet_config = PersistentMapping()
       
   134 
       
   135     def add_row(self):
       
   136         """Add new row and return last row index (0 based)"""
       
   137         self.rows += 1
       
   138         last_index = self.rows - 1
       
   139         self.slot_order[last_index] = PersistentList()
       
   140         self.slots[last_index] = PersistentMapping()
       
   141         return last_index
       
   142 
       
   143     def set_row_order(self, order):
       
   144         """Change template row order"""
       
   145         if not isinstance(order, (list, tuple)):
       
   146             order = list(order)
       
   147         old_slot_order = self.slot_order
       
   148         old_slots = self.slots
       
   149         assert len(order) == self.rows
       
   150         new_slot_order = PersistentMapping()
       
   151         new_slots = PersistentMapping()
       
   152         for index, row_id in enumerate(order):
       
   153             new_slot_order[index] = old_slot_order.get(row_id) or PersistentList()
       
   154             new_slots[index] = old_slots.get(row_id) or PersistentMapping()
       
   155         if self.slot_order != new_slot_order:
       
   156             self.slot_order = new_slot_order
       
   157             self.slots = new_slots
       
   158 
       
   159     def delete_row(self, row_id):
       
   160         """Delete template row"""
       
   161         assert row_id in self.slots
       
   162         for slot_name in self.slots.get(row_id, {}).keys():
       
   163             if slot_name in self.slot_names:
       
   164                 self.slot_names.remove(slot_name)
       
   165             if slot_name in self.slot_config:
       
   166                 del self.slot_config[slot_name]
       
   167             if slot_name in self.portlet_config:
       
   168                 del self.portlet_config[slot_name]
       
   169         for index in range(row_id, self.rows-1):
       
   170             self.slot_order[index] = self.slot_order[index+1]
       
   171             self.slots[index] = self.slots[index+1]
       
   172         if self.rows > 0:
       
   173             del self.slot_order[self.rows-1]
       
   174             del self.slots[self.rows-1]
       
   175         self.rows -= 1
       
   176 
       
   177     @property
       
   178     def slot_names(self):
       
   179         if IPortalTemplate.providedBy(self.__parent__):
       
   180             return self._slot_names
       
   181         else:
       
   182             return IPortalTemplateConfiguration(self.__parent__).slot_names
       
   183 
       
   184     @slot_names.setter
       
   185     def slot_names(self, value):
       
   186         self._slot_names = value
       
   187 
       
   188     @property
       
   189     def slot_order(self):
       
   190         if IPortalTemplate.providedBy(self.__parent__):
       
   191             return self._slot_order
       
   192         else:
       
   193             return IPortalTemplateConfiguration(self.__parent__).slot_order
       
   194 
       
   195     @slot_order.setter
       
   196     def slot_order(self, value):
       
   197         self._slot_order = value
       
   198 
       
   199     @property
       
   200     def slots(self):
       
   201         if IPortalTemplate.providedBy(self.__parent__):
       
   202             return self._slots
       
   203         else:
       
   204             return IPortalTemplateConfiguration(self.__parent__).slots
       
   205 
       
   206     @slots.setter
       
   207     def slots(self, value):
       
   208         self._slots = value
       
   209 
       
   210     def add_slot(self, slot_name, row_id=None):
       
   211         assert slot_name not in self.slot_names
       
   212         self.slot_names.append(slot_name)
       
   213         if row_id is None:
       
   214             row_id = 0
       
   215         # init slots order
       
   216         if row_id not in self.slot_order:
       
   217             self.slot_order[row_id] = PersistentList()
       
   218         self.slot_order[row_id].append(slot_name)
       
   219         # init slots portlets
       
   220         if row_id not in self.slots:
       
   221             self.slots[row_id] = PersistentMapping()
       
   222         self.slots[row_id][slot_name] = PersistentList()
       
   223         # init slots configuration
       
   224         slot = self.slot_config[slot_name] = SlotConfiguration(slot_name)
       
   225         locate(slot, self.__parent__)
       
   226         return row_id, slot_name
       
   227 
       
   228     def set_slot_order(self, order):
       
   229         """Set slots order"""
       
   230         old_slot_order = self.slot_order
       
   231         old_slots = self.slots
       
   232         new_slot_order = PersistentMapping()
       
   233         new_slots = PersistentMapping()
       
   234         for row_id in sorted(map(int, order.keys())):
       
   235             new_slot_order[row_id] = PersistentList(order[row_id])
       
   236             new_slots[row_id] = PersistentMapping()
       
   237             for slot_name in order[row_id]:
       
   238                 old_row_id = self.get_slot_row(slot_name)
       
   239                 new_slots[row_id][slot_name] = old_slots[old_row_id][slot_name]
       
   240         if new_slot_order != old_slot_order:
       
   241             self.slot_order = new_slot_order
       
   242             self.slots = new_slots
       
   243 
       
   244     def get_slot_row(self, slot_name):
       
   245         for row_id in self.slot_order:
       
   246             if slot_name in self.slot_order[row_id]:
       
   247                 return row_id
       
   248 
       
   249     def get_slots(self, row_id):
       
   250         """Get ordered slots list"""
       
   251         return self.slot_order.get(row_id, [])
       
   252 
       
   253     def get_slots_width(self, device=None):
       
   254         """Get slots width"""
       
   255         result = {}
       
   256         for slot_name, config in self.slot_config.items():
       
   257             result[slot_name] = config.get_width(device)
       
   258         return result
       
   259 
       
   260     def set_slot_width(self, slot_name, device, width):
       
   261         """Set slot width"""
       
   262         self.slot_config[slot_name].set_width(width, device)
       
   263 
       
   264     def get_slot_configuration(self, slot_name):
       
   265         """Get slot configuration"""
       
   266         if slot_name not in self.slot_names:
       
   267             return None
       
   268         config = self.slot_config.get(slot_name)
       
   269         if config is None:
       
   270             if IPortalTemplate.providedBy(self.__parent__):
       
   271                 config = SlotConfiguration()
       
   272             else:
       
   273                 config = clone(IPortalTemplateConfiguration(self.__parent__).get_slot_configuration(slot_name))
       
   274                 config.inherit_parent = True
       
   275             self.slot_config[slot_name] = config
       
   276             locate(config, self.__parent__)
       
   277         return config
       
   278 
       
   279     def delete_slot(self, slot_name):
       
   280         """Delete slot and associated portlets"""
       
   281         assert slot_name in self.slot_names
       
   282         row_id = self.get_slot_row(slot_name)
       
   283         del self.portlet_config[slot_name]
       
   284         del self.slot_config[slot_name]
       
   285         del self.slots[row_id][slot_name]
       
   286         self.slot_order[row_id].remove(slot_name)
       
   287         self.slot_names.remove(slot_name)
       
   288 
       
   289     def add_portlet(self, portlet_name, slot_name):
       
   290         """Add portlet to given slot"""
       
   291         assert slot_name in self.slot_names
       
   292         row_id = self.get_slot_row(slot_name)
       
   293         if slot_name not in self.slots.get(row_id):
       
   294             self.slots[row_id][slot_name] = PersistentList()
       
   295         self.slots[row_id][slot_name].append(portlet_name)
       
   296         if slot_name not in self.portlet_config:
       
   297             self.portlet_config[slot_name] = PersistentMapping()
       
   298         position = len(self.slots[row_id][slot_name]) - 1
       
   299         portlet = get_current_registry().getUtility(IPortlet, name=portlet_name)
       
   300         config = IPortletConfiguration(portlet)
       
   301         config.slot_name = slot_name
       
   302         config.position = position
       
   303         locate(config, self.__parent__, '++portlet++{0}::{1}'.format(slot_name, position))
       
   304         self.portlet_config[slot_name][position] = config
       
   305         return {'portlet_name': portlet_name,
       
   306                 'slot_name': slot_name,
       
   307                 'position': position,
       
   308                 'label': check_request().localizer.translate(portlet.label)}
       
   309 
       
   310     def set_portlet_order(self, order):
       
   311         """Set portlet order"""
       
   312         source = order['from']
       
   313         source_slot = source['slot']
       
   314         source_row = self.get_slot_row(source_slot)
       
   315         target = order['to']
       
   316         target_slot = target['slot']
       
   317         target_row = self.get_slot_row(target_slot)
       
   318         portlet_config = self.portlet_config
       
   319         old_config = portlet_config[source_slot].pop(source['position'])
       
   320         target_config = PersistentMapping()
       
   321         for index, (slot_name, portlet_name, position) in enumerate(zip(target['slots'], target['names'],
       
   322                                                                         target['positions'])):
       
   323             if (slot_name == source_slot) and (position == source['position']):
       
   324                 target_config[index] = old_config
       
   325             else:
       
   326                 target_config[index] = portlet_config[slot_name][position]
       
   327             target_config[index].slot_name = target_slot
       
   328             target_config[index].position = index
       
   329             locate(target_config[index], self.__parent__, '++portlet++{0}::{1}'.format(slot_name, index))
       
   330         portlet_config[target_slot] = target_config
       
   331         # re-order source portlets
       
   332         config = portlet_config[source_slot]
       
   333         for index, key in enumerate(sorted(config)):
       
   334             if index != key:
       
   335                 config[index] = config.pop(key)
       
   336                 config[index].position = index
       
   337                 locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(source_slot, index))
       
   338         # re-order target portlets
       
   339         if target_slot != source_slot:
       
   340             config = portlet_config[target_slot]
       
   341             for index, key in enumerate(sorted(config)):
       
   342                 config[index] = config.pop(key)
       
   343                 config[index].position = index
       
   344                 locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(target_slot, index))
       
   345         self.portlet_config = portlet_config
       
   346         del self.slots[source_row][source_slot][source['position']]
       
   347         self.slots[target_row][target_slot] = PersistentList(target['names'])
       
   348 
       
   349     def get_portlet_configuration(self, slot_name, position):
       
   350         """Get portlet configuration"""
       
   351         if slot_name not in self.slot_names:
       
   352             return None
       
   353         config = self.portlet_config.get(slot_name, {}).get(position)
       
   354         if config is None:
       
   355             if IPortalTemplate.providedBy(self.__parent__):
       
   356                 portlet_name = self.slots[slot_name][position]
       
   357                 portlet = get_current_registry().queryUtility(IPortlet, name=portlet_name)
       
   358                 config = IPortletConfiguration(portlet)
       
   359             else:
       
   360                 config = clone(IPortalTemplateConfiguration(self.__parent__).get_portlet_configuration(slot_name,
       
   361                                                                                                        position))
       
   362                 config.inherit_parent = True
       
   363             self.portlet_config[slot_name][position] = config
       
   364             locate(config, self.__parent__)
       
   365         return config
       
   366 
       
   367     def delete_portlet(self, slot_name, position):
       
   368         """Delete portlet"""
       
   369         assert slot_name in self.slot_names
       
   370         row_id = self.get_slot_row(slot_name)
       
   371         config = self.portlet_config[slot_name]
       
   372         del config[position]
       
   373         if len(config) and (position < max(tuple(config.keys()))):
       
   374             for index, key in enumerate(sorted(config)):
       
   375                 config[index] = config.pop(key)
       
   376                 config[index].position = index
       
   377                 locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(slot_name, index))
       
   378         del self.slots[row_id][slot_name][position]
       
   379 
       
   380 
       
   381 class PortalTemplateSlotsVocabulary(SimpleVocabulary):
       
   382     """Portal template slots vocabulary"""
       
   383 
       
   384     def __init__(self, context):
       
   385         config = IPortalTemplateConfiguration(context)
       
   386         terms = [SimpleTerm(slot_name) for slot_name in sorted(config.slot_names)]
       
   387         super(PortalTemplateSlotsVocabulary, self).__init__(terms)
       
   388 
       
   389 getVocabularyRegistry().register('PyAMS template slots', PortalTemplateSlotsVocabulary)
       
   390 
       
   391 
       
   392 @adapter_config(name='portlet', context=IPortalTemplate, provides=ITraversable)
       
   393 class PortalTemplatePortletTraverser(ContextAdapter):
       
   394     """++portlet++ namespace traverser"""
       
   395 
       
   396     def traverse(self, name, furtherpath=None):
       
   397         config = IPortalTemplateConfiguration(self.context)
       
   398         if name:
       
   399             slot_name, position = name.split('::')
       
   400             return config.get_portlet_configuration(slot_name, int(position))
       
   401         else:
       
   402             return config
       
   403 
       
   404 
       
   405 TEMPLATE_CONFIGURATION_KEY = 'pyams_portal.template'
       
   406 
       
   407 
       
   408 @adapter_config(context=IPortalTemplate, provides=IPortalTemplateConfiguration)
       
   409 def PortalTemplateConfigurationFactory(context):
       
   410     """Portal template configuration factory"""
       
   411     annotations = IAnnotations(context)
       
   412     config = annotations.get(TEMPLATE_CONFIGURATION_KEY)
       
   413     if config is None:
       
   414         config = annotations[TEMPLATE_CONFIGURATION_KEY] = PortalTemplateConfiguration()
       
   415         get_current_registry().notify(ObjectCreatedEvent(config))
       
   416         locate(config, context)
       
   417     return config
       
   418 
       
   419 
       
   420 @adapter_config(context=IPortalContext, provides=IPortalTemplateConfiguration)
       
   421 def PortalContextConfigurationFactory(context):
       
   422     """Portal context configuration factory"""
       
   423     page = IPortalPage(context)
       
   424     if page.use_local_template:
       
   425         template = IWorkflowVersions(page.template).get_last_versions()[0]
       
   426         config = IPortalTemplateConfiguration(template)
       
   427     else:
       
   428         annotations = IAnnotations(context)
       
   429         config = annotations.get(TEMPLATE_CONFIGURATION_KEY)
       
   430         if config is None:
       
   431             # we clone template configuration
       
   432             config = annotations[TEMPLATE_CONFIGURATION_KEY] = clone(IPortalTemplateConfiguration(page.template))
       
   433             get_current_registry().notify(ObjectCreatedEvent(config))
       
   434             locate(config, context)
       
   435     return config
       
   436 
       
   437 
       
   438 @adapter_config(context=IPortletConfiguration, provides=IPortalTemplateConfiguration)
       
   439 def PortalPortletConfigurationFactory(context):
       
   440     """Portal portlet configuration factory"""
       
   441     return IPortalTemplateConfiguration(context.__parent__)