eggification
[MosaicDocument.git] / Products / MosaicDocument / MosaicBlock.py
diff --git a/Products/MosaicDocument/MosaicBlock.py b/Products/MosaicDocument/MosaicBlock.py
new file mode 100755 (executable)
index 0000000..2904cbf
--- /dev/null
@@ -0,0 +1,546 @@
+# -*- coding: utf-8 -*-
+# (c) 2003 Centre de Recherche en Informatique ENSMP Fontainebleau <http://cri.ensmp.fr>
+# (c) 2003 Benoît PIN <mailto:pin@cri.ensmp.fr>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+# 02111-1307, USA.
+
+
+from Products.CMFCore.PortalFolder import PortalFolder
+from Globals import InitializeClass
+from AccessControl import ClassSecurityInfo
+from AccessControl.Permission import Permission
+from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.permissions import View, ModifyPortalContent, AccessContentsInformation
+from random import randrange
+from DateTime import DateTime
+from types import InstanceType, StringType, DictType
+from MosaicBlockInformation import RuleError
+
+from OFS.Moniker import Moniker, loadMoniker
+from OFS.CopySupport import _cb_encode, _cb_decode#, cookie_path
+from MosaicBlockInformation import RuleError
+
+
+class MosaicBlock(PortalFolder) :
+       """ Block class for 'Mosaic Document' """
+
+       meta_type = 'Mosaic Block'
+       _properties = ({'id' : 'xpos', 'type' : 'int', 'mode' : 'w'},
+                                  {'id' : 'minimized', 'type' : 'boolean', 'mode' : 'w'},)
+
+       def __init__( self, id, title='', xpos = 0):
+               PortalFolder.__init__(self, id, title)
+               self.manage_changeProperties(xpos=xpos)
+               self.manage_changeProperties(minimized=0)
+
+       security = ClassSecurityInfo()
+
+
+       ## Utils methods
+
+       def _getRootBlock(self) :
+               """Return the root block object ie : 
+                       the first block in the tree which have a "_isRootBlock = 1" flag"""
+                       
+               urlTool = getToolByName(self, 'portal_url')
+               portalObject = urlTool.getPortalObject()
+               
+               block = self
+               while block != portalObject :
+                       if hasattr(block.aq_self, '_isRootBlock') :
+                               return block
+                       else :
+                               block = block.aq_parent
+               return None
+       
+       def _getSelfRules(self) :
+               """Return block rules informations"""
+               mosTool = getToolByName(self, 'mosaic_tool')
+               myTi = mosTool.getTypeInfo(self)
+               ruleDic = {} 
+               for rule in myTi.objectValues(['Rule Information']) :
+                       ruleDic[rule.getId()] = rule
+               return ruleDic
+
+       def _allowedMoves(self, block) :
+               if type(block) == StringType :
+                       block = getattr(self, block)
+               rules = self._getSelfRules()[block.portal_type]
+               move_dic = {'global' : rules.allowMove,
+                                       'rightLeft' : rules.allowMoveRightAndLeft,
+                                       'upDown' : rules.allowMoveUpAndDown,
+                                       }
+               return move_dic
+
+       security.declarePrivate('_getParentRules')
+       def _getParentRules(self) :
+               """Return block rules informations"""
+               mosTool = getToolByName(self, 'mosaic_tool')
+               parentTi = mosTool.getTypeInfo(self.aq_parent)
+               return parentTi.objectValues(['Rule Information'])
+       
+       def _redirectAfterEdit(self, REQUEST, rootBlock=None, blockId=None) :
+               if rootBlock is None :
+                       rootBlock = self._getRootBlock()
+               
+               if REQUEST.get('ajax') :
+                       url = rootBlock.getActionInfo('object_ajax/edit')['url']
+               elif REQUEST.SESSION.get('editBoxes') :
+                       utool = getToolByName(self, 'portal_url')
+                       url = utool() + '/manage_boxes'
+               else :
+                       url = rootBlock.getActionInfo('object/edit')['url'] + (blockId and '#' + blockId or '')
+               return REQUEST.RESPONSE.redirect(url)
+
+       security.declareProtected(ModifyPortalContent, 'getAllowedBlocks')
+       def getAllowedBlocks(self) :
+               """Return a list with allowed blocks"""
+               rules = self._getSelfRules()
+               mosTool = getToolByName(self, 'mosaic_tool')
+               allowedBlocks = ()
+               for ruleId in rules.keys() :
+                       ti = mosTool.getTypeInfo(ruleId)
+                       try :
+                               if ti.isConstructionAllowed(self) :
+                                       allowedBlocks += ({'id' : ti.id, 'title' : ti.title},)
+                       except :
+                               continue
+               return allowedBlocks
+
+       security.declareProtected(ModifyPortalContent, 'haveRules')
+       def haveRules(self) :
+               """ return 1 if type info from self have rules """
+               mosTool = getToolByName(self, 'mosaic_tool')
+               myTi = mosTool.getTypeInfo(self)
+               if myTi.objectValues(['Rule Information']) :
+                       return 1
+               else :
+                       return 0                
+       
+
+       security.declareProtected(View, 'getSlots')
+       def getSlots(self) :
+               """return slots"""
+               return [ob for ob in self.objectValues() if hasattr(ob, '_isMosaicSlot')]
+
+       security.declareProtected(View, 'getSlotsDic')
+       def getSlotsDic(self) :
+               """return slots in dictionary"""
+               slots = self.getSlots()
+               slotDic = {}
+               for slot in slots :
+                       slotDic[slot.getId()] = slot
+               return slotDic
+       
+       security.declarePublic('Title')
+       def Title(self) :
+               """Return title"""
+               return self.getProperty('title', d='') or (hasattr(self, 'caption') and self.caption.text) or ''
+       
+       title = Title
+       
+       security.declareProtected(ModifyPortalContent, 'setTitle')
+       def setTitle(self, title) :
+               if hasattr(self, 'caption') :
+                       self.caption.text = title
+               self.title = title
+               
+       def title_or_id(self) :
+               """Return title or id"""
+               return self.Title() or self.id
+
+       ## Methods for displaying
+       
+       security.declareProtected(View, 'getBlocksTable')
+       def getBlocksTable(self, filteredTypes=[]) :
+               """return blocks ordered in a 2 dimensions table"""
+               blocks = self.objectValues(['Mosaic Block',])
+
+               if filteredTypes :
+                       blocks = [block for block in blocks if block.portal_type in filteredTypes]
+                       
+               #blocks.sort(lambda x, y : cmp(x.xpos, y.xpos)) inutile ???
+               rows = 0
+               try :
+                       cols = blocks[-1].xpos
+               except :
+                       cols = 0
+               columnsTable = [] # columns list
+               rules = self._getSelfRules()
+               for xpos in range(cols + 1) :
+                       colBlockList = [ block for block in blocks if block.xpos == xpos ] # opt : baliser les debuts de colonne
+                       colBlockListLength = len(colBlockList)
+
+                       # build a column by iterating over blocks with the position xpos
+                       colBlockInfoList = []
+                       for blockIndex in range(colBlockListLength) :
+                               block = colBlockList[blockIndex]
+                               blockRule = rules[block.portal_type]
+                               moveDic = {'up' : 0,
+                                                  'down' : 0,
+                                                  'left' : 0,
+                                                  'right' : 0}
+                               if blockRule.allowMoveUpAndDown :
+                                       moveDic['up']    = blockIndex > 0
+                                       moveDic['down']  = blockIndex < colBlockListLength - 1
+                               if blockRule.allowMoveRightAndLeft :
+                                       moveDic['left']  = xpos > 0
+                                       moveDic['right'] = 1
+
+                               # every block will be displayed in a cell in a table
+                               colBlockInfoList.append({'block' : block,
+                                                                                'col' : block.xpos,
+                                                                                'moves' : moveDic,
+                                                                                'mode' : blockRule.mode})
+                               
+                       # append the new column in the column list ie : columnsTable
+                       if colBlockListLength > rows :
+                               rows = colBlockListLength
+                       columnsTable.append(colBlockInfoList)
+                       
+               # Now the max number of rows in known.
+               # We can determine rowspan attributes,
+               # Building lines for an easy iterating over <tr> tag
+               
+               linesTable = []
+               cols += 1
+               for lineIndex in range(rows) :
+                       line = []                        
+                       for columnIndex in range(cols) :
+                               try :
+                                       blockInfo = columnsTable[columnIndex][lineIndex]                                        
+                                       if lineIndex == rows - 1 :
+                                               blockInfo.update({'lastOne' : 1})
+                                       line.append(blockInfo)
+                                               
+                               except :
+                                       if lineIndex and linesTable[lineIndex - 1][columnIndex]['block'] is not None :
+                                               linesTable[lineIndex - 1][columnIndex].update({'rowspan' : rows - lineIndex + 1,
+                                                                                                                                          'lastOne' : 1})
+                                               
+                                       if lineIndex and linesTable[lineIndex - 1][columnIndex].get('rowspan') :
+                                               rowspan = -1 # flag for ignoring td insertion
+                                       else :
+                                               rowspan = rows - lineIndex
+                                       line.append({'block' : None,
+                                                                'col' : columnIndex,
+                                                                'rowspan' : rowspan})                                    
+                                                       
+                       linesTable.append(line)
+
+               tableInfo = {'rows' : rows,
+                                        'cols' : cols,
+                                        'lines' : linesTable,
+                                        'columns' : columnsTable}
+               return tableInfo
+
+       security.declareProtected(View, 'callTemplate')
+       def callTemplate(self, displayAction='renderer', **kw) :
+               """ Return block template name from block meta fti """
+               mosTool = getToolByName(self, 'mosaic_tool')
+               mfti = mosTool.getTypeInfo(self)
+               templateObj = self.restrictedTraverse(mfti.template)
+               return templateObj(block = self, displayAction = displayAction, **kw)
+
+       security.declareProtected(ModifyPortalContent, 'toggle_minimized')
+       def toggle_minimized(self, REQUEST = None) :
+               "toggle minimized property"
+               if not self.minimized :
+                       self.minimized = 1
+               else :
+                       self.minimized = 0
+               if REQUEST is not None:
+                       return self._redirectAfterEdit(REQUEST, blockId = self.id)
+       
+       security.declareProtected(View, 'ypos')
+       def ypos(self) :
+               """ Return the position of self into 
+                       the parent block """
+               return self.aq_parent.getObjectPosition(self.id)
+
+       ## Block edition methods
+          
+       security.declareProtected(ModifyPortalContent, 'addBlock')
+       def addBlock(self, blockType, xpos, id='', beforeBlock='', afterBlock='', REQUEST=None) :
+               """ add a new block type """
+               mosTool = getToolByName(self, 'mosaic_tool')
+               blockId = id or str(int(DateTime()))+str(randrange(1000,10000))
+               mosTool.constructContent(blockType,
+                                                                  self,
+                                                                  blockId,
+                                                                  xpos=xpos,
+                                                                  beforeBlock=beforeBlock,
+                                                                  afterBlock=afterBlock)
+               if REQUEST is not None :        
+                       return self._redirectAfterEdit(REQUEST, blockId = blockId)
+               else :
+                       return blockId
+
+       security.declareProtected(ModifyPortalContent, 'saveBlock')
+       def saveBlock(self, REQUEST=None, **kw) :
+               """ Save block content """
+               mosTool = getToolByName(self, 'mosaic_tool')
+               ti = mosTool.getTypeInfo(self)
+               slotIds = ti.objectIds(['Slot Information',])
+
+               if REQUEST is not None: kw.update(REQUEST.form)
+               dicArgsListItems = [ (argKey, kw[argKey]) for argKey in kw.keys() if type(kw[argKey]) in [InstanceType, DictType] ]
+
+               # iteration over slots for applying edit method
+               # an exception is raised when a slot name is not defined in the portal type
+               for slotId, kwords in dicArgsListItems :
+                       if slotId in slotIds :
+                               slot = getattr(self, slotId) # could raise an exception
+                               kwArgs = {}
+                               for k in kwords.keys() : #kwords is an InstanceType not a DictType...
+                                       kwArgs[k] = kwords[k]
+                               slot.edit(**kwArgs)
+                       else :
+                               raise KeyError, "this slot : '%s' is not defined in the type information" % slotId
+
+               rootBlock = self._getRootBlock()
+               if rootBlock is not None :
+                       rootBlock.reindexObject()
+               
+               if REQUEST is not None :
+                       return self._redirectAfterEdit(REQUEST, rootBlock = rootBlock, blockId = self.getId())
+
+       security.declareProtected(View, 'SearchableText')
+       def SearchableText(self) :
+               blocks = self.objectValues(['Mosaic Block',])
+               slots = [ slot for slot in self.objectValues() if hasattr(slot, '_isMosaicSlot') ]
+               text = ''
+
+               for slot in slots :
+                       text += ' %s' % slot.SearchableText()
+               for block in blocks :
+                       text += ' %s' % block.SearchableText()
+
+               return text
+
+       security.declareProtected(ModifyPortalContent, 'deleteBlock')
+       def deleteBlock(self, blockId, REQUEST=None) :
+               """ Delete the blockId """
+               old_pos = self.getObjectPosition(blockId)
+               if old_pos != 0 :
+                       redirectBlockId = self._objects[old_pos -1]['id']
+               else :
+                       redirectBlockId = self.getId()
+                       
+               self.manage_delObjects([blockId,])
+               
+               if REQUEST :
+                       # évite des appels répétitifs à reindexObject lorsque deleBlock est appelée
+                       # par deleteBlocks
+                       rootBlock = self._getRootBlock()
+                       rootBlock.reindexObject()
+                       return self._redirectAfterEdit(REQUEST, blockId = redirectBlockId)
+               else :
+                       return redirectBlockId
+
+       security.declareProtected(ModifyPortalContent, 'deleteBlocks')
+       def deleteBlocks(self, blockIds, REQUEST=None) :
+               """ Delete block id list"""
+               redirectBlockId = ''
+               for blockId in blockIds :
+                       redirectBlockId = self.deleteBlock(blockId)
+
+               rootBlock = self._getRootBlock()
+               rootBlock.reindexObject()
+               if REQUEST :
+                       return self._redirectAfterEdit(REQUEST, rootBlock = rootBlock, blockId = redirectBlockId)                       
+
+
+       ## cut and paste methods
+
+       security.declareProtected(ModifyPortalContent, 'pushCp')
+       def pushCp(self, blockId, REQUEST) :
+               """ push block in clipboard """
+               previousCp = None
+               oblist = []
+               if REQUEST.has_key('__cp') :
+                       previousCp = REQUEST['__cp']
+                       previousCp = _cb_decode(previousCp)
+                       oblist = previousCp[1]
+
+               block = getattr(self, blockId)            
+               m = Moniker(block)
+               oblist.append(m.dump())
+               cp=(0, oblist)
+               cp=_cb_encode(cp)
+               
+               resp=REQUEST['RESPONSE']
+               resp.setCookie('__cp', cp, path='/')
+               REQUEST['__cp'] = cp
+               return self._redirectAfterEdit(REQUEST, blockId = blockId)
+
+       security.declareProtected(ModifyPortalContent, 'pasteBlocks')
+       def pasteBlocks(self, REQUEST) :
+               """ check rules and paste blocks from cp """
+               
+               if REQUEST.has_key('__cp') :
+                       cp = REQUEST['__cp']
+                       cp = _cb_decode(cp)
+                       mosTool = getToolByName(self, 'mosaic_tool')
+                       app = self.getPhysicalRoot()
+                       self_fti = getattr(mosTool, self.portal_type)
+                       
+
+                       # paste element one by one for
+                       # checking rules on every iteration
+                       for mdata in cp[1] :
+                               m = loadMoniker(mdata)
+                               ob = m.bind(app)
+                               ob_type = ob.portal_type
+                               ob_fti = getattr(mosTool, ob_type)
+                               isBlock = (ob_fti.meta_type == 'Mosaic Block Information')
+                               isRootBlock = getattr(ob, '_isRootBlock', 0) # a block witch have this attribute is a content type
+                               
+                               if isBlock and not isRootBlock and ob_fti.isConstructionAllowed(self) :
+                                       # internal copy and paste handling
+                                       cp = (0, [m.dump(),])
+                                       cp=_cb_encode(cp)
+                                       self.manage_pasteObjects(cb_copy_data = cp)
+
+                       _reOrderBlocks(self)
+
+               self.flushCp(REQUEST)
+               return self._redirectAfterEdit(REQUEST, blockId = self.getId())
+
+       security.declarePublic('flushCp')
+       def flushCp(self, REQUEST) :
+               """ Expire cp cookie """
+               REQUEST.RESPONSE.expireCookie('__cp', path='/')
+
+       security.declareProtected(ModifyPortalContent, 'getCpInfos')
+       def getCpInfos(self, REQUEST) :
+               """ Return information about loaded objects in cp """
+               if REQUEST.has_key('__cp') :
+                       cp = REQUEST['__cp']
+                       cp = _cb_decode(cp)
+                       return len(cp[1])
+               else :
+                       return 0
+
+
+       ## moves methods
+       
+       security.declareProtected(ModifyPortalContent, 'moveLeft')
+       def moveLeft(self, blockId, REQUEST=None) :
+               """Move block left"""
+               move_dic = self._allowedMoves(blockId)
+               if not(move_dic['global'] or move_dic['rightLeft']) :
+                       raise RuleError, "It's not allowed to move this block in this context"
+               block = getattr(self, blockId)
+               if block.xpos > 0 :
+                       block.xpos -= 1
+                       _reOrderBlocks(self)
+               if REQUEST is not None :
+                       return self._redirectAfterEdit(REQUEST, blockId = blockId)
+
+
+
+       security.declareProtected(ModifyPortalContent, 'moveRight')
+       def moveRight(self, blockId, REQUEST=None) :
+               """Move block Right"""
+               move_dic = self._allowedMoves(blockId)
+               if not (move_dic['global'] or move_dic['rightLeft']) :
+                       raise RuleError, "It's not allowed to move this block in this context"
+               block = getattr(self, blockId)
+               block.xpos += 1
+               _reOrderBlocks(self)
+               if REQUEST is not None :
+                       return self._redirectAfterEdit(REQUEST, blockId = blockId)
+
+
+       security.declareProtected(ModifyPortalContent, 'moveUp')
+       def moveUp(self, blockId, REQUEST=None) :
+               """Move block Up"""
+               move_dic = self._allowedMoves(blockId)
+               if not(move_dic['global'] or move_dic['upDown']) :
+                       raise RuleError, "It's not allowed to move this block in this context"
+               self.moveObjectsUp(blockId)
+               if REQUEST is not None :
+                       return self._redirectAfterEdit(REQUEST, blockId = blockId)
+
+
+       security.declareProtected(ModifyPortalContent, 'moveDown')
+       def moveDown(self, blockId, REQUEST=None) :
+               """Move block left"""
+               move_dic = self._allowedMoves(blockId)
+               if not(move_dic['global'] or move_dic['upDown']) :
+                       raise RuleError, "It's not allowed to move this block in this context"
+               self.moveObjectsDown(blockId)
+               if REQUEST is not None :
+                       return self._redirectAfterEdit(REQUEST, blockId = blockId)
+       
+       security.declareProtected(ModifyPortalContent, 'movesUp')
+       def movesUp(self, blockIds = [], REQUEST=None) :
+               """move blocks up"""
+               for blockId in blockIds :
+                       self.moveUp(blockId)
+               if REQUEST is not None :
+                       return self._redirectAfterEdit(REQUEST, blockId = blockId)
+       
+       security.declareProtected(ModifyPortalContent, 'movesDown')
+       def movesDown(self, blockIds = [], REQUEST=None) :
+               """move blocks down"""
+               for blockId in blockIds :
+                       self.moveDown(blockId)
+               if REQUEST is not None :
+                       return self._redirectAfterEdit(REQUEST, blockId = blockId)
+
+
+InitializeClass(MosaicBlock)
+
+def _reOrderBlocks(container) :
+       # This method order blocks.
+       # It's useful when a left or right move or a block creation in a middle of a column happens.
+       blocks = list(container.objectValues(['Mosaic Block',]))
+
+       # get the maximum value for xpos attribute
+       blocks.sort(lambda b1, b2 : cmp(b1.xpos, b2.xpos))
+       rows = 0
+       try :
+               cols = blocks[-1].xpos
+       except :
+               cols = 0
+               
+       blockPosition = 0
+       for xpos in range(cols + 1) :
+               colBlockList = [ block for block in blocks if block.xpos == xpos ]
+               colBlockListLength = len(colBlockList)
+
+               for blockIndex in range(colBlockListLength) :
+                       block = colBlockList[blockIndex]
+                       container.moveObjectToPosition(block.getId(), blockPosition)
+                       blockPosition += 1
+
+
+def addMosaicBlock(dispatcher, id, xpos=0, beforeBlock='', afterBlock='') :
+       """Add a new mosaicBlock"""
+       parentBlock = dispatcher.Destination()
+       parentBlock._setObject(id, MosaicBlock(id, xpos=xpos))
+       if beforeBlock :
+               position = parentBlock.getObjectPosition(beforeBlock)
+               parentBlock.moveObjectToPosition(id, position)
+               
+       elif afterBlock :
+               position = parentBlock.getObjectPosition(afterBlock)
+               parentBlock.moveObjectToPosition(id, position + 1)
+       else :
+               try : _reOrderBlocks(parentBlock)
+               except : pass
+