Conservation de l'état de déroulement de l'arborescence (seulement pour le déroulement).
[MosaicDocument.git] / MosaicBlock.py
1 # -*- coding: utf-8 -*-
2 # (c) 2003 Centre de Recherche en Informatique ENSMP Fontainebleau <http://cri.ensmp.fr>
3 # (c) 2003 Benoît PIN <mailto:pin@cri.ensmp.fr>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License version 2 as published
7 # by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
17 # 02111-1307, USA.
18
19
20 from Products.CMFCore.PortalFolder import PortalFolder
21 from Globals import InitializeClass
22 from AccessControl import ClassSecurityInfo
23 from AccessControl.Permission import Permission
24 from Products.CMFCore.utils import getToolByName
25 from Products.CMFCore.permissions import View, ModifyPortalContent, AccessContentsInformation
26 from random import randrange
27 from DateTime import DateTime
28 from types import InstanceType, StringType, DictType
29 from MosaicBlockInformation import RuleError
30
31 from OFS.Moniker import Moniker, loadMoniker
32 from OFS.CopySupport import _cb_encode, _cb_decode#, cookie_path
33 from MosaicBlockInformation import RuleError
34
35
36 class MosaicBlock(PortalFolder) :
37 """ Block class for 'Mosaic Document' """
38
39 meta_type = 'Mosaic Block'
40 _properties = ({'id' : 'xpos', 'type' : 'int', 'mode' : 'w'},
41 {'id' : 'minimized', 'type' : 'boolean', 'mode' : 'w'},)
42
43 def __init__( self, id, title='', xpos = 0):
44 PortalFolder.__init__(self, id, title)
45 self.manage_changeProperties(xpos=xpos)
46 self.manage_changeProperties(minimized=0)
47
48 security = ClassSecurityInfo()
49
50
51 ## Utils methods
52
53 def _getRootBlock(self) :
54 """Return the root block object ie :
55 the first block in the tree which have a "_isRootBlock = 1" flag"""
56
57 urlTool = getToolByName(self, 'portal_url')
58 portalObject = urlTool.getPortalObject()
59
60 block = self
61 while block != portalObject :
62 if hasattr(block.aq_self, '_isRootBlock') :
63 return block
64 else :
65 block = block.aq_parent
66 return None
67
68 def _getSelfRules(self) :
69 """Return block rules informations"""
70 mosTool = getToolByName(self, 'mosaic_tool')
71 myTi = mosTool.getTypeInfo(self)
72 ruleDic = {}
73 for rule in myTi.objectValues(['Rule Information']) :
74 ruleDic[rule.getId()] = rule
75 return ruleDic
76
77 def _allowedMoves(self, block) :
78 if type(block) == StringType :
79 block = getattr(self, block)
80 rules = self._getSelfRules()[block.portal_type]
81 move_dic = {'global' : rules.allowMove,
82 'rightLeft' : rules.allowMoveRightAndLeft,
83 'upDown' : rules.allowMoveUpAndDown,
84 }
85 return move_dic
86
87 security.declarePrivate('_getParentRules')
88 def _getParentRules(self) :
89 """Return block rules informations"""
90 mosTool = getToolByName(self, 'mosaic_tool')
91 parentTi = mosTool.getTypeInfo(self.aq_parent)
92 return parentTi.objectValues(['Rule Information'])
93
94 def _redirectAfterEdit(self, REQUEST, rootBlock=None, blockId=None) :
95 if rootBlock is None :
96 rootBlock = self._getRootBlock()
97
98 if REQUEST.get('ajax') :
99 url = rootBlock.getActionInfo('object_ajax/edit')['url']
100 elif REQUEST.SESSION.get('editBoxes') :
101 utool = getToolByName(self, 'portal_url')
102 url = utool() + '/manage_boxes'
103 else :
104 url = rootBlock.getActionInfo('object/edit')['url'] + (blockId and '#' + blockId or '')
105 return REQUEST.RESPONSE.redirect(url)
106
107 security.declareProtected(ModifyPortalContent, 'getAllowedBlocks')
108 def getAllowedBlocks(self) :
109 """Return a list with allowed blocks"""
110 rules = self._getSelfRules()
111 mosTool = getToolByName(self, 'mosaic_tool')
112 allowedBlocks = ()
113 for ruleId in rules.keys() :
114 ti = mosTool.getTypeInfo(ruleId)
115 try :
116 if ti.isConstructionAllowed(self) :
117 allowedBlocks += ({'id' : ti.id, 'title' : ti.title},)
118 except :
119 continue
120 return allowedBlocks
121
122 security.declareProtected(ModifyPortalContent, 'haveRules')
123 def haveRules(self) :
124 """ return 1 if type info from self have rules """
125 mosTool = getToolByName(self, 'mosaic_tool')
126 myTi = mosTool.getTypeInfo(self)
127 if myTi.objectValues(['Rule Information']) :
128 return 1
129 else :
130 return 0
131
132
133 security.declareProtected(View, 'getSlots')
134 def getSlots(self) :
135 """return slots"""
136 return [ob for ob in self.objectValues() if hasattr(ob, '_isMosaicSlot')]
137
138 security.declareProtected(View, 'getSlotsDic')
139 def getSlotsDic(self) :
140 """return slots in dictionary"""
141 slots = self.getSlots()
142 slotDic = {}
143 for slot in slots :
144 slotDic[slot.getId()] = slot
145 return slotDic
146
147 security.declarePublic('Title')
148 def Title(self) :
149 """Return title"""
150 return self.getProperty('title', d='') or (hasattr(self, 'caption') and self.caption.text) or ''
151
152 title = Title
153
154 security.declareProtected(ModifyPortalContent, 'setTitle')
155 def setTitle(self, title) :
156 if hasattr(self, 'caption') :
157 self.caption.text = title
158 self.title = title
159
160 def title_or_id(self) :
161 """Return title or id"""
162 return self.Title() or self.id
163
164 ## Methods for displaying
165
166 security.declareProtected(View, 'getBlocksTable')
167 def getBlocksTable(self, filteredTypes=[]) :
168 """return blocks ordered in a 2 dimensions table"""
169 blocks = self.objectValues(['Mosaic Block',])
170
171 if filteredTypes :
172 blocks = [block for block in blocks if block.portal_type in filteredTypes]
173
174 #blocks.sort(lambda x, y : cmp(x.xpos, y.xpos)) inutile ???
175 rows = 0
176 try :
177 cols = blocks[-1].xpos
178 except :
179 cols = 0
180 columnsTable = [] # columns list
181 rules = self._getSelfRules()
182 for xpos in range(cols + 1) :
183 colBlockList = [ block for block in blocks if block.xpos == xpos ] # opt : baliser les debuts de colonne
184 colBlockListLength = len(colBlockList)
185
186 # build a column by iterating over blocks with the position xpos
187 colBlockInfoList = []
188 for blockIndex in range(colBlockListLength) :
189 block = colBlockList[blockIndex]
190 blockRule = rules[block.portal_type]
191 moveDic = {'up' : 0,
192 'down' : 0,
193 'left' : 0,
194 'right' : 0}
195 if blockRule.allowMoveUpAndDown :
196 moveDic['up'] = blockIndex > 0
197 moveDic['down'] = blockIndex < colBlockListLength - 1
198 if blockRule.allowMoveRightAndLeft :
199 moveDic['left'] = xpos > 0
200 moveDic['right'] = 1
201
202 # every block will be displayed in a cell in a table
203 colBlockInfoList.append({'block' : block,
204 'col' : block.xpos,
205 'moves' : moveDic,
206 'mode' : blockRule.mode})
207
208 # append the new column in the column list ie : columnsTable
209 if colBlockListLength > rows :
210 rows = colBlockListLength
211 columnsTable.append(colBlockInfoList)
212
213 # Now the max number of rows in known.
214 # We can determine rowspan attributes,
215 # Building lines for an easy iterating over <tr> tag
216
217 linesTable = []
218 cols += 1
219 for lineIndex in range(rows) :
220 line = []
221 for columnIndex in range(cols) :
222 try :
223 blockInfo = columnsTable[columnIndex][lineIndex]
224 if lineIndex == rows - 1 :
225 blockInfo.update({'lastOne' : 1})
226 line.append(blockInfo)
227
228 except :
229 if lineIndex and linesTable[lineIndex - 1][columnIndex]['block'] is not None :
230 linesTable[lineIndex - 1][columnIndex].update({'rowspan' : rows - lineIndex + 1,
231 'lastOne' : 1})
232
233 if lineIndex and linesTable[lineIndex - 1][columnIndex].get('rowspan') :
234 rowspan = -1 # flag for ignoring td insertion
235 else :
236 rowspan = rows - lineIndex
237 line.append({'block' : None,
238 'col' : columnIndex,
239 'rowspan' : rowspan})
240
241 linesTable.append(line)
242
243 tableInfo = {'rows' : rows,
244 'cols' : cols,
245 'lines' : linesTable,
246 'columns' : columnsTable}
247 return tableInfo
248
249 security.declareProtected(View, 'callTemplate')
250 def callTemplate(self, displayAction='renderer', **kw) :
251 """ Return block template name from block meta fti """
252 mosTool = getToolByName(self, 'mosaic_tool')
253 mfti = mosTool.getTypeInfo(self)
254 templateObj = self.restrictedTraverse(mfti.template)
255 return templateObj(block = self, displayAction = displayAction, **kw)
256
257 security.declareProtected(ModifyPortalContent, 'toggle_minimized')
258 def toggle_minimized(self, REQUEST = None) :
259 "toggle minimized property"
260 if not self.minimized :
261 self.minimized = 1
262 else :
263 self.minimized = 0
264 if REQUEST is not None:
265 return self._redirectAfterEdit(REQUEST, blockId = self.id)
266
267 security.declareProtected(View, 'ypos')
268 def ypos(self) :
269 """ Return the position of self into
270 the parent block """
271 return self.aq_parent.getObjectPosition(self.id)
272
273 ## Block edition methods
274
275 security.declareProtected(ModifyPortalContent, 'addBlock')
276 def addBlock(self, blockType, xpos, id='', beforeBlock='', afterBlock='', REQUEST=None) :
277 """ add a new block type """
278 mosTool = getToolByName(self, 'mosaic_tool')
279 blockId = id or str(int(DateTime()))+str(randrange(1000,10000))
280 mosTool.constructContent(blockType,
281 self,
282 blockId,
283 xpos=xpos,
284 beforeBlock=beforeBlock,
285 afterBlock=afterBlock)
286 if REQUEST is not None :
287 return self._redirectAfterEdit(REQUEST, blockId = blockId)
288 else :
289 return blockId
290
291 security.declareProtected(ModifyPortalContent, 'saveBlock')
292 def saveBlock(self, REQUEST=None, **kw) :
293 """ Save block content """
294 mosTool = getToolByName(self, 'mosaic_tool')
295 ti = mosTool.getTypeInfo(self)
296 slotIds = ti.objectIds(['Slot Information',])
297
298 if REQUEST is not None: kw.update(REQUEST.form)
299 dicArgsListItems = [ (argKey, kw[argKey]) for argKey in kw.keys() if type(kw[argKey]) in [InstanceType, DictType] ]
300
301 # iteration over slots for applying edit method
302 # an exception is raised when a slot name is not defined in the portal type
303 for slotId, kwords in dicArgsListItems :
304 if slotId in slotIds :
305 slot = getattr(self, slotId) # could raise an exception
306 kwArgs = {}
307 for k in kwords.keys() : #kwords is an InstanceType not a DictType...
308 kwArgs[k] = kwords[k]
309 slot.edit(**kwArgs)
310 else :
311 raise KeyError, "this slot : '%s' is not defined in the type information" % slotId
312
313 rootBlock = self._getRootBlock()
314 if rootBlock is not None :
315 rootBlock.reindexObject()
316
317 if REQUEST is not None :
318 return self._redirectAfterEdit(REQUEST, rootBlock = rootBlock, blockId = self.getId())
319
320 security.declareProtected(View, 'SearchableText')
321 def SearchableText(self) :
322 blocks = self.objectValues(['Mosaic Block',])
323 slots = [ slot for slot in self.objectValues() if hasattr(slot, '_isMosaicSlot') ]
324 text = ''
325
326 for slot in slots :
327 text += ' %s' % slot.SearchableText()
328 for block in blocks :
329 text += ' %s' % block.SearchableText()
330
331 return text
332
333 security.declareProtected(ModifyPortalContent, 'deleteBlock')
334 def deleteBlock(self, blockId, REQUEST=None) :
335 """ Delete the blockId """
336 old_pos = self.getObjectPosition(blockId)
337 if old_pos != 0 :
338 redirectBlockId = self._objects[old_pos -1]['id']
339 else :
340 redirectBlockId = self.getId()
341
342 self.manage_delObjects([blockId,])
343
344 if REQUEST :
345 # évite des appels répétitifs à reindexObject lorsque deleBlock est appelée
346 # par deleteBlocks
347 rootBlock = self._getRootBlock()
348 rootBlock.reindexObject()
349 return self._redirectAfterEdit(REQUEST, blockId = redirectBlockId)
350 else :
351 return redirectBlockId
352
353 security.declareProtected(ModifyPortalContent, 'deleteBlocks')
354 def deleteBlocks(self, blockIds, REQUEST=None) :
355 """ Delete block id list"""
356 redirectBlockId = ''
357 for blockId in blockIds :
358 redirectBlockId = self.deleteBlock(blockId)
359
360 rootBlock = self._getRootBlock()
361 rootBlock.reindexObject()
362 if REQUEST :
363 return self._redirectAfterEdit(REQUEST, rootBlock = rootBlock, blockId = redirectBlockId)
364
365
366 ## cut and paste methods
367
368 security.declareProtected(ModifyPortalContent, 'pushCp')
369 def pushCp(self, blockId, REQUEST) :
370 """ push block in clipboard """
371 previousCp = None
372 oblist = []
373 if REQUEST.has_key('__cp') :
374 previousCp = REQUEST['__cp']
375 previousCp = _cb_decode(previousCp)
376 oblist = previousCp[1]
377
378 block = getattr(self, blockId)
379 m = Moniker(block)
380 oblist.append(m.dump())
381 cp=(0, oblist)
382 cp=_cb_encode(cp)
383
384 resp=REQUEST['RESPONSE']
385 resp.setCookie('__cp', cp, path='/')
386 REQUEST['__cp'] = cp
387 return self._redirectAfterEdit(REQUEST, blockId = blockId)
388
389 security.declareProtected(ModifyPortalContent, 'pasteBlocks')
390 def pasteBlocks(self, REQUEST) :
391 """ check rules and paste blocks from cp """
392
393 if REQUEST.has_key('__cp') :
394 cp = REQUEST['__cp']
395 cp = _cb_decode(cp)
396 mosTool = getToolByName(self, 'mosaic_tool')
397 app = self.getPhysicalRoot()
398 self_fti = getattr(mosTool, self.portal_type)
399
400
401 # paste element one by one for
402 # checking rules on every iteration
403 for mdata in cp[1] :
404 m = loadMoniker(mdata)
405 ob = m.bind(app)
406 ob_type = ob.portal_type
407 ob_fti = getattr(mosTool, ob_type)
408 isBlock = (ob_fti.meta_type == 'Mosaic Block Information')
409 isRootBlock = getattr(ob, '_isRootBlock', 0) # a block witch have this attribute is a content type
410
411 if isBlock and not isRootBlock and ob_fti.isConstructionAllowed(self) :
412 # internal copy and paste handling
413 cp = (0, [m.dump(),])
414 cp=_cb_encode(cp)
415 self.manage_pasteObjects(cb_copy_data = cp)
416
417 _reOrderBlocks(self)
418
419 self.flushCp(REQUEST)
420 return self._redirectAfterEdit(REQUEST, blockId = self.getId())
421
422 security.declarePublic('flushCp')
423 def flushCp(self, REQUEST) :
424 """ Expire cp cookie """
425 REQUEST.RESPONSE.expireCookie('__cp', path='/')
426
427 security.declareProtected(ModifyPortalContent, 'getCpInfos')
428 def getCpInfos(self, REQUEST) :
429 """ Return information about loaded objects in cp """
430 if REQUEST.has_key('__cp') :
431 cp = REQUEST['__cp']
432 cp = _cb_decode(cp)
433 return len(cp[1])
434 else :
435 return 0
436
437
438 ## moves methods
439
440 security.declareProtected(ModifyPortalContent, 'moveLeft')
441 def moveLeft(self, blockId, REQUEST=None) :
442 """Move block left"""
443 move_dic = self._allowedMoves(blockId)
444 if not(move_dic['global'] or move_dic['rightLeft']) :
445 raise RuleError, "It's not allowed to move this block in this context"
446 block = getattr(self, blockId)
447 if block.xpos > 0 :
448 block.xpos -= 1
449 _reOrderBlocks(self)
450 if REQUEST is not None :
451 return self._redirectAfterEdit(REQUEST, blockId = blockId)
452
453
454
455 security.declareProtected(ModifyPortalContent, 'moveRight')
456 def moveRight(self, blockId, REQUEST=None) :
457 """Move block Right"""
458 move_dic = self._allowedMoves(blockId)
459 if not (move_dic['global'] or move_dic['rightLeft']) :
460 raise RuleError, "It's not allowed to move this block in this context"
461 block = getattr(self, blockId)
462 block.xpos += 1
463 _reOrderBlocks(self)
464 if REQUEST is not None :
465 return self._redirectAfterEdit(REQUEST, blockId = blockId)
466
467
468 security.declareProtected(ModifyPortalContent, 'moveUp')
469 def moveUp(self, blockId, REQUEST=None) :
470 """Move block Up"""
471 move_dic = self._allowedMoves(blockId)
472 if not(move_dic['global'] or move_dic['upDown']) :
473 raise RuleError, "It's not allowed to move this block in this context"
474 self.moveObjectsUp(blockId)
475 if REQUEST is not None :
476 return self._redirectAfterEdit(REQUEST, blockId = blockId)
477
478
479 security.declareProtected(ModifyPortalContent, 'moveDown')
480 def moveDown(self, blockId, REQUEST=None) :
481 """Move block left"""
482 move_dic = self._allowedMoves(blockId)
483 if not(move_dic['global'] or move_dic['upDown']) :
484 raise RuleError, "It's not allowed to move this block in this context"
485 self.moveObjectsDown(blockId)
486 if REQUEST is not None :
487 return self._redirectAfterEdit(REQUEST, blockId = blockId)
488
489 security.declareProtected(ModifyPortalContent, 'movesUp')
490 def movesUp(self, blockIds = [], REQUEST=None) :
491 """move blocks up"""
492 for blockId in blockIds :
493 self.moveUp(blockId)
494 if REQUEST is not None :
495 return self._redirectAfterEdit(REQUEST, blockId = blockId)
496
497 security.declareProtected(ModifyPortalContent, 'movesDown')
498 def movesDown(self, blockIds = [], REQUEST=None) :
499 """move blocks down"""
500 for blockId in blockIds :
501 self.moveDown(blockId)
502 if REQUEST is not None :
503 return self._redirectAfterEdit(REQUEST, blockId = blockId)
504
505
506 InitializeClass(MosaicBlock)
507
508 def _reOrderBlocks(container) :
509 # This method order blocks.
510 # It's useful when a left or right move or a block creation in a middle of a column happens.
511 blocks = list(container.objectValues(['Mosaic Block',]))
512
513 # get the maximum value for xpos attribute
514 blocks.sort(lambda b1, b2 : cmp(b1.xpos, b2.xpos))
515 rows = 0
516 try :
517 cols = blocks[-1].xpos
518 except :
519 cols = 0
520
521 blockPosition = 0
522 for xpos in range(cols + 1) :
523 colBlockList = [ block for block in blocks if block.xpos == xpos ]
524 colBlockListLength = len(colBlockList)
525
526 for blockIndex in range(colBlockListLength) :
527 block = colBlockList[blockIndex]
528 container.moveObjectToPosition(block.getId(), blockPosition)
529 blockPosition += 1
530
531
532 def addMosaicBlock(dispatcher, id, xpos=0, beforeBlock='', afterBlock='') :
533 """Add a new mosaicBlock"""
534 parentBlock = dispatcher.Destination()
535 parentBlock._setObject(id, MosaicBlock(id, xpos=xpos))
536 if beforeBlock :
537 position = parentBlock.getObjectPosition(beforeBlock)
538 parentBlock.moveObjectToPosition(id, position)
539
540 elif afterBlock :
541 position = parentBlock.getObjectPosition(afterBlock)
542 parentBlock.moveObjectToPosition(id, position + 1)
543 else :
544 try : _reOrderBlocks(parentBlock)
545 except : pass
546