|
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__) |