class XMLMatrix(Folder): """ A mix-in class which provides a matrix like access to objects. Matrices are of any dimension. A single XMLMatrix may contain multiple matrices, of different dimension. Each matrix is associated to a so-called 'base_id'. We still must make XMLMatrix a subclass of Base so that we can inherit from ExtensionClass.Base which is required for multiple inheritance to work as expected. Read this for more information: http://www.daa.com.au/pipermail/pygtk/2001-May/001180.html In our case, we will use Folder because we want to inherit from Folder consistency checking """ # Declarative security security = ClassSecurityInfo() # Matrix Methods security.declareProtected( Permissions.AccessContentsInformation, 'getCell' ) def getCell(self, *kw , **kwd): """ Access a cell at row and column """ if getattr(aq_base(self), 'index', None) is None: return None base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return None cell_id = self.keyToId(kw, base_id = base_id) if cell_id is None: return None return self.get(cell_id) security.declareProtected( Permissions.AccessContentsInformation, 'getCellProperty' ) def getCellProperty(self, *kw , **kwd): """ Get a property of a cell at row and column """ base_id= kwd.get('base_id', "cell") cell = self.getCell(*kw, **kwd) if cell is None: return None return cell.getProperty(base_id) security.declareProtected( Permissions.AccessContentsInformation, 'hasCell' ) def hasCell(self, *kw , **kwd): """ Checks if matrix corresponding to base_id contains cell specified by *kw coordinates. """ return self.getCell(*kw, **kwd) is not None security.declareProtected( Permissions.AccessContentsInformation, 'hasCellContent' ) def hasCellContent(self, base_id='cell'): """ Checks if matrix corresponding to base_id contains cells. """ aq_self = aq_base(self) if getattr(aq_self, 'index', None) is None: return 0 if not self.index.has_key(base_id): return 0 for i in self.getCellIds(base_id=base_id): if hasattr(self, i): # We should try to use aq_self if possible but XXX return 1 return 0 security.declareProtected( Permissions.AccessContentsInformation, 'hasInRange' ) def hasInRange(self, *kw , **kwd): """ Checks if *kw coordinates are in the range of the matrix in kwd['base_id']. """ if getattr(aq_base(self), 'index', None) is None: return 0 base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return 0 base_item = self.index[base_id] for i, my_id in enumerate(kw): if not base_item.has_key(i) or not base_item[i].has_key(my_id): return 0 return 1 security.declareProtected( Permissions.ModifyPortalContent, '_setCellRange' ) def _setCellRange(self, *args, **kw): """Set a new range for a matrix Each value for each axis is assigned an integer id. If the number of axis changes, everything is reset. Otherwise, ids are never changed, so that cells never need to be renamed: this means no sort is garanteed, and there can be holes. """ base_id = kw.get('base_id', 'cell') # Get (initialize if necessary) index for considered matrix (base_id). try: index = aq_base(self).index except AttributeError: index = self.index = PersistentMapping() to_delete = [] try: index = index[base_id] if len(args) != len(index): # The number of axis changes so we'll delete all existing cells and # renumber everything from 1. to_delete = INFINITE_SET, index.clear() except KeyError: index[base_id] = index = PersistentMapping() # For each axis ... for i, axis in enumerate(args): # ... collect old axis keys and allocate ids for new ones. axis = set(axis) last_id = -1 try: id_dict = index[i] except KeyError: index[i] = id_dict = PersistentMapping() else: delete = set() to_delete.append(delete) for k, v in id_dict.items(): try: axis.remove(k) if last_id < v: last_id = v except KeyError: delete.add(v) del id_dict[k] # At this point, last_id contains the greatest id. for k in sorted(axis): last_id += 1 id_dict[k] = last_id # Remove old cells if any. if any(to_delete): prefix = base_id + '_' prefix_len = len(prefix) for cell_id in list(self.objectIds()): if cell_id.startswith(prefix): for i, j in enumerate(cell_id[prefix_len:].split('_')): if int(j) in to_delete[i]: self._delObject(cell_id) break security.declareProtected( Permissions.ModifyPortalContent, 'setCellRange' ) def setCellRange(self, *kw, **kwd): """ Update the matrix ranges using provided lists of indexes (kw). Any number of list can be provided """ self._setCellRange(*kw, **kwd) self.reindexObject() security.declareProtected(Permissions.ModifyPortalContent, '_updateCellRange') def _updateCellRange(self, base_id, **kw): """ The asCellRange script is Portal Type dependent which is not the case with this kind of code a better implementation consists in defining asCellRange as a generic method at matrix level (OverridableMethod(portal_type)) which lookups for script in class, meta_type and PT form interaction could be implemented with interaction workflow this method should be renamed updateCellRange or updateMatrixCellRange base_id is parameter of updateCellRange asCellRange scripts should be unified if possible """ script = self._getTypeBasedMethod('asCellRange', **kw) if script is None: raise UnboundLocalError,\ "Did not find cell range script for portal type: %r" %\ self.getPortalType() cell_range = script(base_id=base_id, matrixbox=0, **kw) self._setCellRange(base_id=base_id, *cell_range) security.declareProtected(Permissions.ModifyPortalContent, 'updateCellRange') def updateCellRange(self, base_id='cell', **kw): """ same as _updateCellRange, but reindex the object. """ self._updateCellRange(base_id=base_id, **kw) self.reindexObject() security.declareProtected( Permissions.ModifyPortalContent, '_renameCellRange' ) def _renameCellRange(self, *kw, **kwd): """ Rename a range for a matrix, this method can also handle a changement of the size of a matrix """ base_id = kwd.get('base_id', 'cell') if getattr(aq_base(self), 'index', None) is None: self.index = PersistentMapping() # Return if previous range is the same current_range = self.getCellRange(base_id=base_id) or [] if current_range == list(kw): # kw is a tuple LOG('XMLMatrix',0,'return form _setCellRange - no need to change range') return current_len = len(current_range) new_len = len(kw) len_delta = new_len - current_len # We must make sure the base_id exists # in the event of a matrix creation for example if not self.index.has_key(base_id): # Create an index for this base_id self.index[base_id] = PersistentMapping() cell_id_list = [] for cell_id in self.getCellIdList(base_id = base_id): if self.get(cell_id) is not None: cell_id_list.append(cell_id) # First, delete all cells which are out of range. size_list = map(len, kw) if len_delta < 0: size_list.extend([1] * (-len_delta)) def is_in_range(cell_id): for i, index in enumerate(cell_id[len(base_id)+1:].split('_')): if int(index) >= size_list[i]: self._delObject(cell_id) return False return True cell_id_list = filter(is_in_range, cell_id_list) # Secondly, rename coordinates. This does not change cell ids. for i in range(max(new_len, current_len)): if i >= new_len: del self.index[base_id][i] else: if i >= current_len: self.index[base_id][i] = PersistentMapping() for place in self.index[base_id][i].keys(): if place not in kw[i]: del self.index[base_id][i][place] for j, place in enumerate(kw[i]): self.index[base_id][i][place] = j # Lastly, rename ids and catalog/uncatalog everything. if len_delta > 0: # Need to move, say, base_1_2 -> base_1_2_0 appended_id = '_0' * len_delta for old_id in cell_id_list: cell = self.get(old_id) if cell is not None: new_id = old_id + appended_id self._delObject(old_id) cell.isIndexable = ConstantGetter('isIndexable', value=False) cell.id = new_id self._setObject(new_id, aq_base(cell)) cell.isIndexable = ConstantGetter('isIndexable', value=True) cell.reindexObject() #cell.unindexObject(path='%s/%s' % (self.getUrl(), old_id)) elif len_delta < 0: # Need to move, say, base_1_2_0 -> base_1_2 removed_id_len = 2 * (-len_delta) for old_id in cell_id_list: cell = self.get(old_id) if cell is not None: new_id = old_id[:-removed_id_len] self._delObject(old_id) cell.isIndexable = ConstantGetter('isIndexable', value=False) cell.id = new_id self._setObject(new_id, aq_base(cell)) cell.isIndexable = ConstantGetter('isIndexable', value=True) cell.reindexObject() #cell.unindexObject(path='%s/%s' % (self.getUrl(), old_id)) security.declareProtected( Permissions.ModifyPortalContent, 'renameCellRange' ) def renameCellRange(self, *kw, **kwd): """ Update the matrix ranges using provided lists of indexes (kw). This keep cell values if we add/remove dimensions Any number of list can be provided """ self._renameCellRange(*kw, **kwd) self.reindexObject() security.declareProtected(Permissions.AccessContentsInformation, 'getCellRange') def getCellRange(self, base_id='cell'): """ Returns the cell range as a list of index ids """ try: cell_range = aq_base(self).index[base_id] except (AttributeError, KeyError): return [] return [x.keys() for _, x in sorted(cell_range.iteritems())] security.declareProtected( Permissions.ModifyPortalContent, 'newCell' ) def newCell(self, *kw, **kwd): """ This method creates a new cell """ if getattr(aq_base(self), 'index', None) is None: return None base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return None cell_id = self.keyToId(kw, base_id = base_id) if cell_id is None: raise KeyError, 'Invalid key: %s' % str(kw) cell = self.get(cell_id) if cell is not None: return cell else: return self.newCellContent(cell_id,**kwd) security.declareProtected( Permissions.ModifyPortalContent, 'newCellContent' ) def newCellContent(self, cell_id, portal_type=None, **kw): """ Creates a new content as a cell. This method is meant to be overriden by subclasses. """ if portal_type is None: for x in self.allowedContentTypes(): portal_type_id = x.getId() if portal_type_id.endswith(' Cell'): portal_type = portal_type_id break return self.newContent(id=cell_id, portal_type=portal_type, **kw) security.declareProtected( Permissions.AccessContentsInformation, 'getCellKeyList' ) def getCellKeyList(self, base_id = 'cell'): """ Returns a list of possible keys as tuples """ if getattr(aq_base(self), 'index', None) is None: return () if not self.index.has_key(base_id): return () index = self.index[base_id] id_tuple = [v.keys() for v in index.itervalues()] if len(id_tuple) == 0: return () return cartesianProduct(id_tuple) security.declareProtected( Permissions.AccessContentsInformation, 'getCellKeys' ) getCellKeys = getCellKeyList # We should differenciate in future existing tuples from possible tuples security.declareProtected( Permissions.AccessContentsInformation, 'getCellRangeKeyList' ) getCellRangeKeyList = getCellKeyList security.declareProtected( Permissions.AccessContentsInformation, 'keyToId' ) def keyToId(self, kw, base_id = 'cell'): """ Converts a key into a cell id """ index = self.index[base_id] cell_id_list = [base_id] append = cell_id_list.append for i, item in enumerate(kw): try: append(str(index[i][item])) except KeyError: return None return '_'.join(cell_id_list) security.declareProtected( Permissions.AccessContentsInformation, 'getCellIdList' ) def getCellIdList(self, base_id = 'cell'): """ Returns a list of possible ids as tuples """ if getattr(aq_base(self), 'index', None) is None: return () if not self.index.has_key(base_id): return () result = [] append = result.append for kw in self.getCellKeys(base_id = base_id): cell_id = self.keyToId(kw, base_id = base_id ) if cell_id is not None: append(cell_id) return result security.declareProtected( Permissions.AccessContentsInformation, 'getCellIds' ) getCellIds = getCellIdList security.declareProtected( Permissions.AccessContentsInformation, 'cellIds' ) cellIds = getCellIdList # We should differenciate in future all possible ids for existing ids security.declareProtected( Permissions.AccessContentsInformation, 'getCellRangeIdList' ) getCellRangeIdList = getCellIdList security.declareProtected( Permissions.AccessContentsInformation, 'getCellValueList' ) def getCellValueList(self, base_id = 'cell'): """ Returns a list of cell values as tuples """ result = [] append = result.append for id in self.getCellIdList(base_id=base_id): o = self.get(id) if o is not None: append(o) return result security.declareProtected( Permissions.AccessContentsInformation, 'cellValues' ) cellValues = getCellValueList security.declareProtected( Permissions.AccessContentsInformation, 'getMatrixList' ) def getMatrixList(self): """ Return possible base_id values """ if getattr(aq_base(self), 'index', None) is None: return () return self.index.keys() security.declareProtected( Permissions.ModifyPortalContent, 'delMatrix' ) def delMatrix(self, base_id = 'cell'): """ Delete all cells for a given base_id XXX BAD NAME: make a difference between deleting matrix and matrix cells """ ids = self.getCellIds(base_id = base_id) my_ids = [] append = my_ids.append for i in self.objectIds(): if i in ids: append(i) if len(my_ids) > 0: self.manage_delObjects(ids=my_ids) security.declareProtected( Permissions.AccessContentsInformation, 'delCells' ) delCells = delMatrix security.declareProtected( Permissions.AccessContentsInformation, '_checkConsistency' ) def _checkConsistency(self, fixit=0): """ Constraint API. """ # Check useless cells to_delete_set = set() error_list = [] def addError(error_message): if fixit: error_message += ' (fixed)' error = (self.getRelativeUrl(), 'XMLMatrix inconsistency', 102, error_message) error_list.append(error) # We make sure first that there is an index if getattr(aq_base(self), 'index', None) is None: self.index = PersistentMapping() # We will check each cell of the matrix the matrix # XXX This code assumes the following predicate: # each subobject of an XMLMatrix is either a Cell that needs # consistency checking OR ( is not a Cell, and has an id that is # not like "(\w+_)+(\d+_)*\d+" ) # But Documents inheriting XMLMatrix can have unrelated, non-cell # subobjects, possibly with id looking like some_id_2. If it ever happens, # an error will be wrongly raised. for obj in self.objectValues(): object_id = obj.getId() # obect_id is equal to something like 'something_quantity_3_2' # So we need to check for every object.id if the value # looks like good or not. We split the name # check each key in index # First we make sure this is a cell object_id_split = object_id.split('_') base_id = None cell_coordinate_list = [] while object_id_split: coordinate = None try: coordinate = int(object_id_split[-1]) except ValueError: # The last item is not a coordinate, object_id_split hence # only contains the base_id elements base_id = '_'.join(object_id_split) break else: cell_coordinate_list.insert(0, coordinate) # the last item is a coordinate not part of base_id object_id_split.pop() current_dimension = len(cell_coordinate_list) if current_dimension > 0 and base_id is not None: if not self.index.has_key(base_id): # The matrix does not have this base_id addError("There is no index for base_id %s" % base_id) to_delete_set.add(object_id) continue # Check empty indices. empty_list = [] base_item = self.index[base_id] for key, value in base_item.iteritems(): if value is None or len(value) == 0: addError("There is no id for the %dth axis of base_id %s" % (key, base_id)) empty_list.append(key) if fixit: for i in empty_list: del base_item[key] len_id = len(base_item) if current_dimension != len_id: addError("Dimension of cell is %s but should be %s" % (current_dimension, len_id)) to_delete_set.add(object_id) else : for i, coordinate in enumerate(cell_coordinate_list): if coordinate >= len(base_item[i]): addError("Cell %s is out of bound" % object_id) to_delete_set.add(object_id) break if fixit and len(to_delete_set) > 0: self.manage_delObjects(list(to_delete_set)) return error_list security.declareProtected( Permissions.ModifyPortalContent, 'notifyAfterUpdateRelatedContent' ) def notifyAfterUpdateRelatedContent(self, previous_category_url, new_category_url): """ We must do some matrix range update in the event matrix range is defined by a category """ LOG('XMLMatrix notifyAfterUpdateRelatedContent', 0, str(new_category_url)) update_method = self.portal_categories.updateRelatedCategory for base_id in self.getMatrixList(): cell_range = self.getCellRange(base_id=base_id) new_cell_range = [] for range_item_list in cell_range: new_range_item_list = map(lambda c: update_method(c, previous_category_url, new_category_url), range_item_list) new_cell_range.append(new_range_item_list) kwd = {'base_id': base_id} LOG('XMLMatrix notifyAfterUpdateRelatedContent matrix', 0, str(base_id)) LOG('XMLMatrix notifyAfterUpdateRelatedContent _renameCellRange', 0, str(new_cell_range)) self._renameCellRange(*new_cell_range,**kwd)
class XMLMatrix(Folder): """ A mix-in class which provides a matrix like access to objects. Matrices are of any dimension. A single XMLMatrix may contain multiple matrices, of different dimension. Each matrix is associated to a so-called 'base_id'. We still must make XMLMatrix a subclass of Base so that we can inherit from ExtensionClass.Base which is required for multiple inheritance to work as expected. Read this for more information: http://www.daa.com.au/pipermail/pygtk/2001-May/001180.html In our case, we will use Folder because we want to inherit from Folder consistency checking """ # Declarative security security = ClassSecurityInfo() # Matrix Methods security.declareProtected( Permissions.AccessContentsInformation, 'getCell' ) def getCell(self, *kw , **kwd): """ Access a cell at row and column """ if getattr(aq_base(self), 'index', None) is None: return None base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return None cell_id = self.keyToId(kw, base_id = base_id) if cell_id is None: return None return self.get(cell_id) security.declareProtected( Permissions.AccessContentsInformation, 'getCellProperty' ) def getCellProperty(self, *kw , **kwd): """ Get a property of a cell at row and column """ base_id= kwd.get('base_id', "cell") cell = self.getCell(*kw, **kwd) if cell is None: return None return cell.getProperty(base_id) security.declareProtected( Permissions.AccessContentsInformation, 'hasCell' ) def hasCell(self, *kw , **kwd): """ Checks if matrix corresponding to base_id contains cell specified by *kw coordinates. """ return self.getCell(*kw, **kwd) is not None security.declareProtected( Permissions.AccessContentsInformation, 'hasCellContent' ) def hasCellContent(self, base_id='cell'): """ Checks if matrix corresponding to base_id contains cells. """ aq_self = aq_base(self) if getattr(aq_self, 'index', None) is None: return 0 if not self.index.has_key(base_id): return 0 for i in self.getCellIds(base_id=base_id): if hasattr(self, i): # We should try to use aq_self if possible but XXX return 1 return 0 security.declareProtected( Permissions.AccessContentsInformation, 'hasInRange' ) def hasInRange(self, *kw , **kwd): """ Checks if *kw coordinates are in the range of the matrix in kwd['base_id']. """ if getattr(aq_base(self), 'index', None) is None: return 0 base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return 0 base_item = self.index[base_id] for i, my_id in enumerate(kw): if not base_item.has_key(i) or not base_item[i].has_key(my_id): return 0 return 1 security.declareProtected( Permissions.ModifyPortalContent, '_setCellRange' ) def _setCellRange(self, *kw, **kwd): """ Set a new range for a matrix. If needed, it will resize and/or reorder matrix content. """ movement = {} # Maps original cell id to its new id for each moved cell. new_index = PersistentMapping() base_id = kwd.get('base_id', 'cell') if getattr(aq_base(self), 'index', None) is None: self.index = PersistentMapping() # Return if previous range is the same current_range = self.getCellRange(base_id=base_id) if current_range == list(kw): # kw is a tuple return # Create the new index for the range given in *kw # *kw practical example: # kw = [ ['color/blue', 'color/red'], ['size/S', 'size/L']] for i, index_ids in enumerate(kw): temp = PersistentMapping() for j, my_id in enumerate(index_ids): temp[my_id] = j new_index[i] = temp if self.index.has_key(base_id): # Compute cell movement from their position in previous range to their # position in the new range. for i, i_value in self.index[base_id].iteritems(): if new_index.has_key(i): temp = {} for my_id, my_value in i_value.iteritems(): temp[my_value] = new_index[i].get(my_id) movement[i] = temp # List all valid cell ids for current base_id. object_id_list = [] for obj in self.objectValues(): object_id = obj.getId() if object_id.find(base_id) == 0: # Check that all '_'-separated fields are of int type. if (object_id) > len(base_id): try: int(object_id[len(base_id)+1:].split('_')[0]) test = self._getOb(object_id) # If the object was created # during this transaction, # then we do not need to # work on it object_id_list.append(object_id) except (ValueError, KeyError): pass # Prepend 'temp_' to all cells, to avoid id conflicts while renaming. for object_id in object_id_list: new_name = 'temp_' + object_id obj = self._getOb(object_id) self._delObject(object_id) obj.id = new_name self._setObject(new_name, aq_base(obj)) # Rename all cells to their final name. for object_id in object_id_list: # Retrieve the place of the object, for movement_0_0 it is ['0','0'] object_place = object_id[len(base_id)+1:].split('_') to_delete = 1 # We must have the same number of dimensions if len(object_place) == len(new_index): # Let us browse each dimension of the previous index for i in range(len(object_place)): # Build each part of the nex id by looking up int. values old_place = int(object_place[i]) # We are looking inside the movement dictionnary where # we should move the object, so for example # 'qantity_2_5' is renamed as 'quantity_4_3' if movement.has_key(i): if movement[i].has_key(old_place): # Replace the place of the cell only if there where a change if (movement[i][old_place]) != None: object_place[i] = str(movement[i][old_place]) to_delete = 0 else: object_place[i] = None # XXX In this case, we delete every cell wich are not in the # movement dictionnary, may be this is very bad, so may be # we need to add an extra check here, ie if # if movement[i].has_key(old_place) returns None, # We may want to keep the cell, but only if we are sure # the movement dictionnary will not define a new cell with this id new_object_id_list = [] temp_object_id = 'temp_' + object_id o = self._getOb(temp_object_id) if not to_delete and not (None in object_place): self._delObject(temp_object_id) # In all cases, we have # to remove the temp object object_place.insert(0, base_id) new_name = '_'.join(object_place) o.id = new_name new_object_id_list.extend(new_name) self._setObject(new_name, aq_base(o)) if new_name != object_id: # Theses two lines are very important, if the object is renamed # then we must uncatalog the previous one o.unindexObject(path='%s/%s' % (self.getUrl() , object_id)) # Force a new uid to be allocated, because unindexObject creates # an activity which will later delete lines from catalog based # on their uid, and it is not garanted that indexation will happen # after this deletion. # It is bad to waste uids, but this data structure offers no # alternative because cell id gives its index in the matrix, # so reordering axes requires the cell id to change. # XXX: It can be improved, but requires most of this file to be # rewritten, and compatibility code must be written as data # structure would most probably change. o.uid = None o.reindexObject() # we reindex in case position has changed # uid should be consistent else: # In all cases, we have to remove the temp object #WARNING -> if path is not good, it will not be able to uncatalog !!! #o.immediateReindexObject() # STILL A PROBLEM -> getUidForPath XXX if object_id not in new_object_id_list: # do not unindex a new object o.isIndexable = ConstantGetter('isIndexable', value=True) o.unindexObject(path='%s/%s' % (self.getUrl() , object_id)) # unindexed already forced o.isIndexable = ConstantGetter('isIndexable', value=False) self._delObject(temp_object_id) # object will be removed # from catalog automaticaly # We don't need the old index any more, we # can set the new index self.index[base_id] = new_index security.declareProtected( Permissions.ModifyPortalContent, 'setCellRange' ) def setCellRange(self, *kw, **kwd): """ Update the matrix ranges using provided lists of indexes (kw). Any number of list can be provided """ self._setCellRange(*kw, **kwd) self.reindexObject() security.declareProtected(Permissions.ModifyPortalContent, '_updateCellRange') def _updateCellRange(self, base_id, **kw): """ The asCellRange script is Portal Type dependent which is not the case with this kind of code a better implementation consists in defining asCellRange as a generic method at matrix level (OverridableMethod(portal_type)) which lookups for script in class, meta_type and PT form interaction could be implemented with interaction workflow this method should be renamed updateCellRange or updateMatrixCellRange base_id is parameter of updateCellRange asCellRange scripts should be unified if possible """ script = self._getTypeBasedMethod('asCellRange', **kw) if script is None: raise UnboundLocalError,\ "Did not find cell range script for portal type: %r" %\ self.getPortalType() cell_range = script(base_id=base_id, matrixbox=0, **kw) self._setCellRange(base_id=base_id, *cell_range) security.declareProtected(Permissions.ModifyPortalContent, 'updateCellRange') def updateCellRange(self, base_id='cell', **kw): """ same as _updateCellRange, but reindex the object. """ self._updateCellRange(base_id=base_id, **kw) self.reindexObject() security.declareProtected( Permissions.ModifyPortalContent, '_renameCellRange' ) def _renameCellRange(self, *kw, **kwd): """ Rename a range for a matrix, this method can also handle a changement of the size of a matrix """ base_id = kwd.get('base_id', 'cell') if getattr(aq_base(self), 'index', None) is None: self.index = PersistentMapping() # Return if previous range is the same current_range = self.getCellRange(base_id=base_id) or [] if current_range == list(kw): # kw is a tuple LOG('XMLMatrix',0,'return form _setCellRange - no need to change range') return current_len = len(current_range) new_len = len(kw) len_delta = new_len - current_len # We must make sure the base_id exists # in the event of a matrix creation for example if not self.index.has_key(base_id): # Create an index for this base_id self.index[base_id] = PersistentMapping() cell_id_list = [] for cell_id in self.getCellIdList(base_id = base_id): if self.get(cell_id) is not None: cell_id_list.append(cell_id) # First, delete all cells which are out of range. size_list = map(len, kw) if len_delta < 0: size_list.extend([1] * (-len_delta)) def is_in_range(cell_id): for i, index in enumerate(cell_id[len(base_id)+1:].split('_')): if int(index) >= size_list[i]: self._delObject(cell_id) return False return True cell_id_list = filter(is_in_range, cell_id_list) # Secondly, rename coordinates. This does not change cell ids. for i in range(max(new_len, current_len)): if i >= new_len: del self.index[base_id][i] else: if i >= current_len: self.index[base_id][i] = PersistentMapping() for place in self.index[base_id][i].keys(): if place not in kw[i]: del self.index[base_id][i][place] for j, place in enumerate(kw[i]): self.index[base_id][i][place] = j # Lastly, rename ids and catalog/uncatalog everything. if len_delta > 0: # Need to move, say, base_1_2 -> base_1_2_0 appended_id = '_0' * len_delta for old_id in cell_id_list: cell = self.get(old_id) if cell is not None: new_id = old_id + appended_id self._delObject(old_id) cell.isIndexable = ConstantGetter('isIndexable', value=False) cell.id = new_id self._setObject(new_id, aq_base(cell)) cell.isIndexable = ConstantGetter('isIndexable', value=True) cell.reindexObject() #cell.unindexObject(path='%s/%s' % (self.getUrl(), old_id)) elif len_delta < 0: # Need to move, say, base_1_2_0 -> base_1_2 removed_id_len = 2 * (-len_delta) for old_id in cell_id_list: cell = self.get(old_id) if cell is not None: new_id = old_id[:-removed_id_len] self._delObject(old_id) cell.isIndexable = ConstantGetter('isIndexable', value=False) cell.id = new_id self._setObject(new_id, aq_base(cell)) cell.isIndexable = ConstantGetter('isIndexable', value=True) cell.reindexObject() #cell.unindexObject(path='%s/%s' % (self.getUrl(), old_id)) security.declareProtected( Permissions.ModifyPortalContent, 'renameCellRange' ) def renameCellRange(self, *kw, **kwd): """ Update the matrix ranges using provided lists of indexes (kw). This keep cell values if we add/remove dimensions Any number of list can be provided """ self._renameCellRange(*kw, **kwd) self.reindexObject() security.declareProtected(Permissions.AccessContentsInformation, 'getCellRange') def getCellRange(self, base_id='cell'): """ Returns the cell range as a list of index ids """ if getattr(aq_base(self), 'index', None) is None: return [] cell_range = self.index.get(base_id, None) if cell_range is None: return None result = [] for value in cell_range.itervalues(): result_items = sorted(value.iteritems(), key=lambda x:x[1]) result.append([x[0] for x in result_items]) return result security.declareProtected( Permissions.ModifyPortalContent, 'newCell' ) def newCell(self, *kw, **kwd): """ This method creates a new cell """ if getattr(aq_base(self), 'index', None) is None: return None base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return None cell_id = self.keyToId(kw, base_id = base_id) if cell_id is None: raise KeyError, 'Invalid key: %s' % str(kw) cell = self.get(cell_id) if cell is not None: return cell else: return self.newCellContent(cell_id,**kwd) security.declareProtected( Permissions.ModifyPortalContent, 'newCellContent' ) def newCellContent(self, cell_id, portal_type=None, **kw): """ Creates a new content as a cell. This method is meant to be overriden by subclasses. """ if portal_type is None: for x in self.allowedContentTypes(): portal_type_id = x.getId() if portal_type_id.endswith(' Cell'): portal_type = portal_type_id break return self.newContent(id=cell_id, portal_type=portal_type, **kw) security.declareProtected( Permissions.AccessContentsInformation, 'getCellKeyList' ) def getCellKeyList(self, base_id = 'cell'): """ Returns a list of possible keys as tuples """ if getattr(aq_base(self), 'index', None) is None: return () if not self.index.has_key(base_id): return () index = self.index[base_id] id_tuple = [v.keys() for v in index.itervalues()] if len(id_tuple) == 0: return () return cartesianProduct(id_tuple) security.declareProtected( Permissions.AccessContentsInformation, 'getCellKeys' ) getCellKeys = getCellKeyList # We should differenciate in future existing tuples from possible tuples security.declareProtected( Permissions.AccessContentsInformation, 'getCellRangeKeyList' ) getCellRangeKeyList = getCellKeyList security.declareProtected( Permissions.AccessContentsInformation, 'keyToId' ) def keyToId(self, kw, base_id = 'cell'): """ Converts a key into a cell id """ index = self.index[base_id] cell_id_list = [base_id] append = cell_id_list.append for i, item in enumerate(kw): try: append(str(index[i][item])) except KeyError: return None return '_'.join(cell_id_list) security.declareProtected( Permissions.AccessContentsInformation, 'getCellIdList' ) def getCellIdList(self, base_id = 'cell'): """ Returns a list of possible ids as tuples """ if getattr(aq_base(self), 'index', None) is None: return () if not self.index.has_key(base_id): return () result = [] append = result.append for kw in self.getCellKeys(base_id = base_id): cell_id = self.keyToId(kw, base_id = base_id ) if cell_id is not None: append(cell_id) return result security.declareProtected( Permissions.AccessContentsInformation, 'getCellIds' ) getCellIds = getCellIdList security.declareProtected( Permissions.AccessContentsInformation, 'cellIds' ) cellIds = getCellIdList # We should differenciate in future all possible ids for existing ids security.declareProtected( Permissions.AccessContentsInformation, 'getCellRangeIdList' ) getCellRangeIdList = getCellIdList security.declareProtected( Permissions.AccessContentsInformation, 'getCellValueList' ) def getCellValueList(self, base_id = 'cell'): """ Returns a list of cell values as tuples """ result = [] append = result.append for id in self.getCellIdList(base_id=base_id): o = self.get(id) if o is not None: append(o) return result security.declareProtected( Permissions.AccessContentsInformation, 'cellValues' ) cellValues = getCellValueList security.declareProtected( Permissions.AccessContentsInformation, 'getMatrixList' ) def getMatrixList(self): """ Return possible base_id values """ if getattr(aq_base(self), 'index', None) is None: return () return self.index.keys() security.declareProtected( Permissions.ModifyPortalContent, 'delMatrix' ) def delMatrix(self, base_id = 'cell'): """ Delete all cells for a given base_id XXX BAD NAME: make a difference between deleting matrix and matrix cells """ ids = self.getCellIds(base_id = base_id) my_ids = [] append = my_ids.append for i in self.objectIds(): if i in ids: append(i) if len(my_ids) > 0: self.manage_delObjects(ids=my_ids) security.declareProtected( Permissions.AccessContentsInformation, 'delCells' ) delCells = delMatrix security.declareProtected( Permissions.AccessContentsInformation, '_checkConsistency' ) def _checkConsistency(self, fixit=0): """ Constraint API. """ # Check useless cells to_delete_set = set() error_list = [] def addError(error_message): if fixit: error_message += ' (fixed)' error = (self.getRelativeUrl(), 'XMLMatrix inconsistency', 102, error_message) error_list.append(error) # We make sure first that there is an index if getattr(aq_base(self), 'index', None) is None: self.index = PersistentMapping() # We will check each cell of the matrix the matrix # XXX This code assumes the following predicate: # each subobject of an XMLMatrix is either a Cell that needs # consistency checking OR ( is not a Cell, and has an id that is # not like "(\w+_)+(\d+_)*\d+" ) # But Documents inheriting XMLMatrix can have unrelated, non-cell # subobjects, possibly with id looking like some_id_2. If it ever happens, # an error will be wrongly raised. for obj in self.objectValues(): object_id = obj.getId() # obect_id is equal to something like 'something_quantity_3_2' # So we need to check for every object.id if the value # looks like good or not. We split the name # check each key in index # First we make sure this is a cell object_id_split = object_id.split('_') base_id = None cell_coordinate_list = [] while object_id_split: coordinate = None try: coordinate = int(object_id_split[-1]) except ValueError: # The last item is not a coordinate, object_id_split hence # only contains the base_id elements base_id = '_'.join(object_id_split) break else: cell_coordinate_list.insert(0, coordinate) # the last item is a coordinate not part of base_id object_id_split.pop() current_dimension = len(cell_coordinate_list) if current_dimension > 0 and base_id is not None: if not self.index.has_key(base_id): # The matrix does not have this base_id addError("There is no index for base_id %s" % base_id) to_delete_set.add(object_id) continue # Check empty indices. empty_list = [] base_item = self.index[base_id] for key, value in base_item.iteritems(): if value is None or len(value) == 0: addError("There is no id for the %dth axis of base_id %s" % (key, base_id)) empty_list.append(key) if fixit: for i in empty_list: del base_item[key] len_id = len(base_item) if current_dimension != len_id: addError("Dimension of cell is %s but should be %s" % (current_dimension, len_id)) to_delete_set.add(object_id) else : for i, coordinate in enumerate(cell_coordinate_list): if coordinate >= len(base_item[i]): addError("Cell %s is out of bound" % object_id) to_delete_set.add(object_id) break if fixit and len(to_delete_set) > 0: self.manage_delObjects(list(to_delete_set)) return error_list security.declareProtected( Permissions.ModifyPortalContent, 'notifyAfterUpdateRelatedContent' ) def notifyAfterUpdateRelatedContent(self, previous_category_url, new_category_url): """ We must do some matrix range update in the event matrix range is defined by a category """ LOG('XMLMatrix notifyAfterUpdateRelatedContent', 0, str(new_category_url)) update_method = self.portal_categories.updateRelatedCategory for base_id in self.getMatrixList(): cell_range = self.getCellRange(base_id=base_id) new_cell_range = [] for range_item_list in cell_range: new_range_item_list = map(lambda c: update_method(c, previous_category_url, new_category_url), range_item_list) new_cell_range.append(new_range_item_list) kwd = {'base_id': base_id} LOG('XMLMatrix notifyAfterUpdateRelatedContent matrix', 0, str(base_id)) LOG('XMLMatrix notifyAfterUpdateRelatedContent _renameCellRange', 0, str(new_cell_range)) self._renameCellRange(*new_cell_range,**kwd)
class OAuthTool(BaseTool): """ OAuthTool is used to allow API authentification """ title = 'OAuth Tool' id = 'portal_oauth' meta_type = 'ERP5 OAuth Tool' portal_type = 'OAuth Tool' allowed_types = () # Declarative Security security = ClassSecurityInfo() security.declareProtected(Permissions.ManagePortal, 'manage_overview' ) manage_overview = DTMLFile( 'explainOAuthTool', _dtmldir ) signature_methods = {} def __init__(self, *args, **kw): self.signature_methods = PersistentMapping() self.add_signature_method(OAuthSignatureMethod_PLAINTEXT()) self.add_signature_method(OAuthSignatureMethod_HMAC_SHA1()) self.consumer = OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET) self.my_request_token = OAuthToken('requestkey', 'requestsecret') self.my_access_token = OAuthToken('accesskey', 'accesssecret') self.nonce = 'nonce' self.verifier = VERIFIER def add_signature_method(self, signature_method): self.signature_methods[signature_method.get_name()] = signature_method return self.signature_methods def fetch_request_token(self, oauth_request): """Processes a request_token request and returns the request token on success. """ try: # Get the request token for authorization. token = self._get_token(oauth_request, 'request') except OAuthError: LOG("initial token request called", 300, "") # No token required for the initial token request. version = self._get_version(oauth_request) consumer = self._get_consumer(oauth_request) try: callback = self.get_callback(oauth_request) except OAuthError: callback = None # 1.0, no callback specified. self._check_signature(oauth_request, consumer, None) # Fetch a new token. if consumer.key == self.consumer.key: if callback: # want to check here if callback is sensible # for mock store, we assume it is LOG("setting callback method %s" %(callback), 300, "") self.my_request_token.set_callback(callback) token = self.my_request_token else: token = None return token def fetch_access_token(self, oauth_request): """Processes an access_token request and returns the access token on success. """ version = self._get_version(oauth_request) consumer = self._get_consumer(oauth_request) try: verifier = self._get_verifier(oauth_request) except OAuthError: verifier = None # Get the request token. token = self._get_token(oauth_request, 'request') self._check_signature(oauth_request, consumer, token) if consumer.key == self.consumer.key and \ token.key == self.my_request_token.key and \ verifier == self.verifier: # want to check here if token is authorized # for mock store, we assume it is return self.my_access_token return None def verify_request(self, oauth_request): """Verifies an api call and checks all the parameters.""" # -> consumer and token version = self._get_version(oauth_request) consumer = self._get_consumer(oauth_request) # Get the access token. token = self._get_token(oauth_request, 'access') self._check_signature(oauth_request, consumer, token) parameters = oauth_request.get_nonoauth_parameters() return consumer, token, parameters def authorize_token(self, token, user): """Authorize a request token.""" if token.key == self.my_request_token.key: return self.my_request_token return None def get_callback(self, oauth_request): """Get the callback URL.""" return oauth_request.get_parameter('oauth_callback') def build_authenticate_header(self, realm=''): """Optional support for the authenticate header.""" return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} def _get_version(self, oauth_request): """Verify the correct version request for this server.""" try: version = oauth_request.get_parameter('oauth_version') except: version = VERSION if version and version != VERSION: raise OAuthError('OAuth version %s not supported.' % str(version)) return version def _get_signature_method(self, oauth_request): """Figure out the signature with some defaults.""" try: signature_method = oauth_request.get_parameter( 'oauth_signature_method') except: signature_method = SIGNATURE_METHOD try: # Get the signature method object. signature_method = self.signature_methods[signature_method] except: signature_method_names = ', '.join(self.signature_methods.keys()) raise OAuthError('Signature method %s not supported try one of the ' 'following: %s' % (signature_method, signature_method_names)) return signature_method def _get_consumer(self, oauth_request): consumer_key = oauth_request.get_parameter('oauth_consumer_key') if consumer_key != self.consumer.key: raise OAuthError('Invalid consumer.') return self.consumer def _get_token(self, oauth_request, token_type='access'): """Try to find the token for the provided request token key.""" token_field = oauth_request.get_parameter('oauth_token') token_attrib = getattr(self, 'my_%s_token' % token_type) if token_field == token_attrib.key: try: callback = self.get_callback(oauth_request) except OAuthError: callback = None # 1.0, no callback specified. LOG("setting callback method %s" %(callback), 300, "in _get_token") token_attrib.set_callback(callback) return token_attrib else: raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) def _get_verifier(self, oauth_request): return oauth_request.get_parameter('oauth_verifier') def _check_signature(self, oauth_request, consumer, token): timestamp, nonce = oauth_request._get_timestamp_nonce() self._check_timestamp(timestamp) self._check_nonce(consumer, token, nonce) signature_method = self._get_signature_method(oauth_request) try: signature = oauth_request.get_parameter('oauth_signature') except: raise OAuthError('Missing signature.') # Validate the signature. valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) if not valid_sig: key, base = signature_method.build_signature_base_string( oauth_request, consumer, token) raise OAuthError('Invalid signature. Expected signature base ' 'string: %s' % base) built = signature_method.build_signature(oauth_request, consumer, token) def _check_timestamp(self, timestamp): """Verify that timestamp is recentish.""" timestamp = int(timestamp) now = int(time.time()) lapsed = abs(now - timestamp) if lapsed > TIMESTAMP_THRESHOLD: raise OAuthError('Expired timestamp: given %d and now %s has a ' 'greater difference than threshold %d' % (timestamp, now, TIMESTAMP_THRESHOLD)) def _check_nonce(self, consumer, token, nonce): """Verify that the nonce is uniqueish.""" if token and consumer.key == self.consumer.key and \ (token.key == self.my_request_token.key or token.key == self.my_access_token.key) \ and nonce == self.nonce: raise OAuthError('Nonce already used: %s' % str(nonce)) def send_oauth_error(self, err, REQUEST): """ return error """ print err REQUEST.response.setStatus(status=401, reason=err) return REQUEST.response def call(self, REQUEST=None, **kw): """ this method handle all the call on the portal """ path = REQUEST.getURL() headers = REQUEST._auth command = REQUEST['REQUEST_METHOD'] parameters = REQUEST.form postdata = None LOG("-------call--------", 300, "\npath %s\nheader %s\ncommand %s\nparameters %s\n\nXXXXXXXXXXXXXXX" %(path, headers, command, parameters)) # if command == "POST": # import pdb # pdb.set_trace() # construct the oauth request from the request parameters oauth_request = OAuthRequest.from_request(command, path, headers=headers, parameters=parameters, query_string=postdata) # request token if path.startswith(REQUEST_TOKEN_URL): try: # create a request token token = self.fetch_request_token(oauth_request) LOG("Return %s" %(token.to_string()), 300, "") return token.to_string() # # send okay response # self.send_response(200, 'OK') # self.end_headers() # # return the token # self.wfile.write(token.to_string()) except OAuthError, err: raise LOG("Error returned %s" %(err,), 300, "") self.send_oauth_error(err, REQUEST) return # user authorization if path.startswith(AUTHORIZATION_URL): try: return self.manage_oauth_authorize(oauth_token=self._get_token(oauth_request, "request"), oauth_callback=self.get_callback(oauth_request)) # get the request token # token = self.fetch_request_token(oauth_request) # # authorize the token (kind of does nothing for now) # token = self.authorize_token(token, None) # token.set_verifier(VERIFIER) # return token.get_callback_url() # send okay response # self.send_response(200, 'OK') # self.end_headers() # # return the callback url (to show server has it) # self.wfile.write(token.get_callback_url()) except OAuthError, err: self.send_oauth_error(err, REQUEST) return
class XMLMatrix(Folder): """ A mix-in class which provides a matrix like access to objects. Matrices are of any dimension. A single XMLMatrix may contain multiple matrices, of different dimension. Each matrix is associated to a so-called 'base_id'. We still must make XMLMatrix a subclass of Base so that we can inherit from ExtensionClass.Base which is required for multiple inheritance to work as expected. Read this for more information: http://www.daa.com.au/pipermail/pygtk/2001-May/001180.html In our case, we will use Folder because we want to inherit from Folder consistency checking """ # Declarative security security = ClassSecurityInfo() # Matrix Methods security.declareProtected(Permissions.AccessContentsInformation, 'getCell') def getCell(self, *kw, **kwd): """ Access a cell at row and column """ if getattr(aq_base(self), 'index', None) is None: return None base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return None cell_id = self.keyToId(kw, base_id=base_id) if cell_id is None: return None return self.get(cell_id) security.declareProtected(Permissions.AccessContentsInformation, 'getCellProperty') def getCellProperty(self, *kw, **kwd): """ Get a property of a cell at row and column """ base_id = kwd.get('base_id', "cell") cell = self.getCell(*kw, **kwd) if cell is None: return None return cell.getProperty(base_id) security.declareProtected(Permissions.AccessContentsInformation, 'hasCell') def hasCell(self, *kw, **kwd): """ Checks if matrix corresponding to base_id contains cell specified by *kw coordinates. """ return self.getCell(*kw, **kwd) is not None security.declareProtected(Permissions.AccessContentsInformation, 'hasCellContent') def hasCellContent(self, base_id='cell'): """ Checks if matrix corresponding to base_id contains cells. """ aq_self = aq_base(self) if getattr(aq_self, 'index', None) is None: return 0 if not self.index.has_key(base_id): return 0 for i in self.getCellIds(base_id=base_id): if hasattr(self, i): # We should try to use aq_self if possible but XXX return 1 return 0 security.declareProtected(Permissions.AccessContentsInformation, 'hasInRange') def hasInRange(self, *kw, **kwd): """ Checks if *kw coordinates are in the range of the matrix in kwd['base_id']. """ if getattr(aq_base(self), 'index', None) is None: return 0 base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return 0 base_item = self.index[base_id] for i, my_id in enumerate(kw): if not base_item.has_key(i) or not base_item[i].has_key(my_id): return 0 return 1 security.declareProtected(Permissions.ModifyPortalContent, '_setCellRange') def _setCellRange(self, *args, **kw): """Set a new range for a matrix Each value for each axis is assigned an integer id. If the number of axis changes, everything is reset. Otherwise, ids are never changed, so that cells never need to be renamed: this means no sort is garanteed, and there can be holes. """ base_id = kw.get('base_id', 'cell') # Get (initialize if necessary) index for considered matrix (base_id). try: index = aq_base(self).index except AttributeError: index = self.index = PersistentMapping() to_delete = [] try: index = index[base_id] if len(args) != len(index): # The number of axis changes so we'll delete all existing cells and # renumber everything from 1. to_delete = INFINITE_SET, index.clear() except KeyError: index[base_id] = index = PersistentMapping() # For each axis ... for i, axis in enumerate(args): # ... collect old axis keys and allocate ids for new ones. axis = set(axis) last_id = -1 try: id_dict = index[i] except KeyError: index[i] = id_dict = PersistentMapping() else: delete = set() to_delete.append(delete) for k, v in id_dict.items(): try: axis.remove(k) if last_id < v: last_id = v except KeyError: delete.add(v) del id_dict[k] # At this point, last_id contains the greatest id. for k in sorted(axis): last_id += 1 id_dict[k] = last_id # Remove old cells if any. if any(to_delete): prefix = base_id + '_' prefix_len = len(prefix) for cell_id in list(self.objectIds()): if cell_id.startswith(prefix): for i, j in enumerate(cell_id[prefix_len:].split('_')): if int(j) in to_delete[i]: self._delObject(cell_id) break security.declareProtected(Permissions.ModifyPortalContent, 'setCellRange') def setCellRange(self, *kw, **kwd): """ Update the matrix ranges using provided lists of indexes (kw). Any number of list can be provided """ self._setCellRange(*kw, **kwd) self.reindexObject() security.declareProtected(Permissions.ModifyPortalContent, '_updateCellRange') def _updateCellRange(self, base_id, **kw): """ The asCellRange script is Portal Type dependent which is not the case with this kind of code a better implementation consists in defining asCellRange as a generic method at matrix level (OverridableMethod(portal_type)) which lookups for script in class, meta_type and PT form interaction could be implemented with interaction workflow this method should be renamed updateCellRange or updateMatrixCellRange base_id is parameter of updateCellRange asCellRange scripts should be unified if possible """ script = self._getTypeBasedMethod('asCellRange', **kw) if script is None: raise UnboundLocalError,\ "Did not find cell range script for portal type: %r" %\ self.getPortalType() cell_range = script(base_id=base_id, matrixbox=0, **kw) self._setCellRange(base_id=base_id, *cell_range) security.declareProtected(Permissions.ModifyPortalContent, 'updateCellRange') def updateCellRange(self, base_id='cell', **kw): """ same as _updateCellRange, but reindex the object. """ self._updateCellRange(base_id=base_id, **kw) self.reindexObject() security.declareProtected(Permissions.ModifyPortalContent, '_renameCellRange') def _renameCellRange(self, *kw, **kwd): """ Rename a range for a matrix, this method can also handle a changement of the size of a matrix """ base_id = kwd.get('base_id', 'cell') if getattr(aq_base(self), 'index', None) is None: self.index = PersistentMapping() # Return if previous range is the same current_range = self.getCellRange(base_id=base_id) or [] if current_range == list(kw): # kw is a tuple LOG('XMLMatrix', 0, 'return form _setCellRange - no need to change range') return current_len = len(current_range) new_len = len(kw) len_delta = new_len - current_len # We must make sure the base_id exists # in the event of a matrix creation for example if not self.index.has_key(base_id): # Create an index for this base_id self.index[base_id] = PersistentMapping() cell_id_list = [] for cell_id in self.getCellIdList(base_id=base_id): if self.get(cell_id) is not None: cell_id_list.append(cell_id) # First, delete all cells which are out of range. size_list = map(len, kw) if len_delta < 0: size_list.extend([1] * (-len_delta)) def is_in_range(cell_id): for i, index in enumerate(cell_id[len(base_id) + 1:].split('_')): if int(index) >= size_list[i]: self._delObject(cell_id) return False return True cell_id_list = filter(is_in_range, cell_id_list) # Secondly, rename coordinates. This does not change cell ids. for i in range(max(new_len, current_len)): if i >= new_len: del self.index[base_id][i] else: if i >= current_len: self.index[base_id][i] = PersistentMapping() for place in self.index[base_id][i].keys(): if place not in kw[i]: del self.index[base_id][i][place] for j, place in enumerate(kw[i]): self.index[base_id][i][place] = j # Lastly, rename ids and catalog/uncatalog everything. if len_delta > 0: # Need to move, say, base_1_2 -> base_1_2_0 appended_id = '_0' * len_delta for old_id in cell_id_list: cell = self.get(old_id) if cell is not None: new_id = old_id + appended_id self._delObject(old_id) cell.isIndexable = ConstantGetter('isIndexable', value=False) cell.id = new_id self._setObject(new_id, aq_base(cell)) cell.isIndexable = ConstantGetter('isIndexable', value=True) cell.reindexObject() #cell.unindexObject(path='%s/%s' % (self.getUrl(), old_id)) elif len_delta < 0: # Need to move, say, base_1_2_0 -> base_1_2 removed_id_len = 2 * (-len_delta) for old_id in cell_id_list: cell = self.get(old_id) if cell is not None: new_id = old_id[:-removed_id_len] self._delObject(old_id) cell.isIndexable = ConstantGetter('isIndexable', value=False) cell.id = new_id self._setObject(new_id, aq_base(cell)) cell.isIndexable = ConstantGetter('isIndexable', value=True) cell.reindexObject() #cell.unindexObject(path='%s/%s' % (self.getUrl(), old_id)) security.declareProtected(Permissions.ModifyPortalContent, 'renameCellRange') def renameCellRange(self, *kw, **kwd): """ Update the matrix ranges using provided lists of indexes (kw). This keep cell values if we add/remove dimensions Any number of list can be provided """ self._renameCellRange(*kw, **kwd) self.reindexObject() security.declareProtected(Permissions.AccessContentsInformation, 'getCellRange') def getCellRange(self, base_id='cell'): """ Returns the cell range as a list of index ids """ try: cell_range = aq_base(self).index[base_id] except (AttributeError, KeyError): return [] return [x.keys() for _, x in sorted(cell_range.iteritems())] security.declareProtected(Permissions.ModifyPortalContent, 'newCell') def newCell(self, *kw, **kwd): """ This method creates a new cell """ if getattr(aq_base(self), 'index', None) is None: return None base_id = kwd.get('base_id', "cell") if not self.index.has_key(base_id): return None cell_id = self.keyToId(kw, base_id=base_id) if cell_id is None: raise KeyError, 'Invalid key: %s' % str(kw) cell = self.get(cell_id) if cell is not None: return cell else: return self.newCellContent(cell_id, **kwd) security.declareProtected(Permissions.ModifyPortalContent, 'newCellContent') def newCellContent(self, cell_id, portal_type=None, **kw): """ Creates a new content as a cell. This method is meant to be overriden by subclasses. """ if portal_type is None: for x in self.allowedContentTypes(): portal_type_id = x.getId() if portal_type_id.endswith(' Cell'): portal_type = portal_type_id break return self.newContent(id=cell_id, portal_type=portal_type, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getCellKeyList') def getCellKeyList(self, base_id='cell'): """ Returns a list of possible keys as tuples """ if getattr(aq_base(self), 'index', None) is None: return () if not self.index.has_key(base_id): return () index = self.index[base_id] id_tuple = [v.keys() for v in index.itervalues()] if len(id_tuple) == 0: return () return cartesianProduct(id_tuple) security.declareProtected(Permissions.AccessContentsInformation, 'getCellKeys') getCellKeys = getCellKeyList # We should differenciate in future existing tuples from possible tuples security.declareProtected(Permissions.AccessContentsInformation, 'getCellRangeKeyList') getCellRangeKeyList = getCellKeyList security.declareProtected(Permissions.AccessContentsInformation, 'keyToId') def keyToId(self, kw, base_id='cell'): """ Converts a key into a cell id """ index = self.index[base_id] cell_id_list = [base_id] append = cell_id_list.append for i, item in enumerate(kw): try: append(str(index[i][item])) except KeyError: return None return '_'.join(cell_id_list) security.declareProtected(Permissions.AccessContentsInformation, 'getCellIdList') def getCellIdList(self, base_id='cell'): """ Returns a list of possible ids as tuples """ if getattr(aq_base(self), 'index', None) is None: return () if not self.index.has_key(base_id): return () result = [] append = result.append for kw in self.getCellKeys(base_id=base_id): cell_id = self.keyToId(kw, base_id=base_id) if cell_id is not None: append(cell_id) return result security.declareProtected(Permissions.AccessContentsInformation, 'getCellIds') getCellIds = getCellIdList security.declareProtected(Permissions.AccessContentsInformation, 'cellIds') cellIds = getCellIdList # We should differenciate in future all possible ids for existing ids security.declareProtected(Permissions.AccessContentsInformation, 'getCellRangeIdList') getCellRangeIdList = getCellIdList security.declareProtected(Permissions.AccessContentsInformation, 'getCellValueList') def getCellValueList(self, base_id='cell'): """ Returns a list of cell values as tuples """ result = [] append = result.append for id in self.getCellIdList(base_id=base_id): o = self.get(id) if o is not None: append(o) return result security.declareProtected(Permissions.AccessContentsInformation, 'cellValues') cellValues = getCellValueList security.declareProtected(Permissions.AccessContentsInformation, 'getMatrixList') def getMatrixList(self): """ Return possible base_id values """ if getattr(aq_base(self), 'index', None) is None: return () return self.index.keys() security.declareProtected(Permissions.ModifyPortalContent, 'delMatrix') def delMatrix(self, base_id='cell'): """ Delete all cells for a given base_id XXX BAD NAME: make a difference between deleting matrix and matrix cells """ ids = self.getCellIds(base_id=base_id) my_ids = [] append = my_ids.append for i in self.objectIds(): if i in ids: append(i) if len(my_ids) > 0: self.manage_delObjects(ids=my_ids) security.declareProtected(Permissions.AccessContentsInformation, 'delCells') delCells = delMatrix security.declareProtected(Permissions.AccessContentsInformation, '_checkConsistency') def _checkConsistency(self, fixit=0): """ Constraint API. """ # Check useless cells to_delete_set = set() error_list = [] def addError(error_message): if fixit: error_message += ' (fixed)' error = (self.getRelativeUrl(), 'XMLMatrix inconsistency', 102, error_message) error_list.append(error) # We make sure first that there is an index if getattr(aq_base(self), 'index', None) is None: self.index = PersistentMapping() # We will check each cell of the matrix the matrix # XXX This code assumes the following predicate: # each subobject of an XMLMatrix is either a Cell that needs # consistency checking OR ( is not a Cell, and has an id that is # not like "(\w+_)+(\d+_)*\d+" ) # But Documents inheriting XMLMatrix can have unrelated, non-cell # subobjects, possibly with id looking like some_id_2. If it ever happens, # an error will be wrongly raised. for obj in self.objectValues(): object_id = obj.getId() # obect_id is equal to something like 'something_quantity_3_2' # So we need to check for every object.id if the value # looks like good or not. We split the name # check each key in index # First we make sure this is a cell object_id_split = object_id.split('_') base_id = None cell_coordinate_list = [] while object_id_split: coordinate = None try: coordinate = int(object_id_split[-1]) except ValueError: # The last item is not a coordinate, object_id_split hence # only contains the base_id elements base_id = '_'.join(object_id_split) break else: cell_coordinate_list.insert(0, coordinate) # the last item is a coordinate not part of base_id object_id_split.pop() current_dimension = len(cell_coordinate_list) if current_dimension > 0 and base_id is not None: if not self.index.has_key(base_id): # The matrix does not have this base_id addError("There is no index for base_id %s" % base_id) to_delete_set.add(object_id) continue # Check empty indices. empty_list = [] base_item = self.index[base_id] for key, value in base_item.iteritems(): if value is None or len(value) == 0: addError( "There is no id for the %dth axis of base_id %s" % (key, base_id)) empty_list.append(key) if fixit: for i in empty_list: del base_item[key] len_id = len(base_item) if current_dimension != len_id: addError("Dimension of cell is %s but should be %s" % (current_dimension, len_id)) to_delete_set.add(object_id) else: for i, coordinate in enumerate(cell_coordinate_list): if coordinate >= len(base_item[i]): addError("Cell %s is out of bound" % object_id) to_delete_set.add(object_id) break if fixit and len(to_delete_set) > 0: self.manage_delObjects(list(to_delete_set)) return error_list security.declareProtected(Permissions.ModifyPortalContent, 'notifyAfterUpdateRelatedContent') def notifyAfterUpdateRelatedContent(self, previous_category_url, new_category_url): """ We must do some matrix range update in the event matrix range is defined by a category """ LOG('XMLMatrix notifyAfterUpdateRelatedContent', 0, str(new_category_url)) update_method = self.portal_categories.updateRelatedCategory for base_id in self.getMatrixList(): cell_range = self.getCellRange(base_id=base_id) new_cell_range = [] for range_item_list in cell_range: new_range_item_list = map( lambda c: update_method(c, previous_category_url, new_category_url), range_item_list) new_cell_range.append(new_range_item_list) kwd = {'base_id': base_id} LOG('XMLMatrix notifyAfterUpdateRelatedContent matrix', 0, str(base_id)) LOG('XMLMatrix notifyAfterUpdateRelatedContent _renameCellRange', 0, str(new_cell_range)) self._renameCellRange(*new_cell_range, **kwd)
class PDFForm(File): """This class allows to fill PDF Form with TALES expressions, using a TALES expression for each cell. TODO: * cache compiled TALES * set _v_errors when setting invalid TALES (setCellTALES can raise, but not doEditCells) """ meta_type = "ERP5 PDF Form" icon = "www/PDFForm.png" # Those 2 are ugly names, but we keep compatibility # the page range we want to print (a TALES expr) __page_range__ = '' # the method to format values (a TALES expr) __format_method__ = '' # Declarative Security security = ClassSecurityInfo() # Declarative properties _properties = File._properties + ( {'id' : 'download_url', 'type' : 'lines', 'mode' : 'w' }, {'id' : 'business_template_include_content', 'type' : 'boolean', 'mode' : 'w' }, ) download_url = () business_template_include_content = 1 # Constructors constructors = (manage_addPDFForm, addPDFForm) manage_options = ( ( {'label':'Edit Cell TALES', 'action':'manage_cells'}, {'label':'Display Cell Names', 'action':'showCellNames'}, {'label':'Test PDF generation', 'action':'generatePDF'}, {'label':'View original', 'action':'viewOriginal'}, {'label':'Download PDF content from URL', 'action':'downloadPdfContent'}, ) + filter(lambda option:option['label'] != "View", File.manage_options) ) def __init__ (self, id, title='', pdf_file=''): # holds all the cell informations, even those not related to this form self.all_cells = PersistentMapping() # holds the cells related to this pdf form self.cells = PersistentMapping() # File constructor will set the file content File.__init__(self, id, title, pdf_file) security.declareProtected(Permissions.ManagePortal, 'manage_upload') def manage_upload(self, file=None, REQUEST=None) : """ Zope calls this when the content of the enclosed file changes. The 'cells' attribute is updated, but already defined cells are not erased, they are saved in the 'all_cells' attribute so if the pdf file is reverted, you do not loose the cells definitions. """ if not file or not hasattr(file, "read") : raise ValueError ("The pdf form file should not be empty") file.seek(0) # file is always valid here values = PDFTk().dumpDataFields(file) self.cells = {} for v in values : if v["FieldType"] not in ("Button", "Choice")\ or not int(v["FieldFlags"]) & 65536: k = v["FieldName"] if not self.all_cells.has_key(k) : self.cells[k] = "" else : self.cells[k] = self.all_cells[k] self.all_cells.update(self.cells) file.seek(0) File.manage_upload(self, file, REQUEST) if REQUEST: message = "Saved changes." return self.manage_main(self, REQUEST, manage_tabs_message=message) security.declareProtected(Permissions.ViewManagementScreens, 'manage_cells') manage_cells = PageTemplateFile('www/PDFForm_manageCells', globals(), __name__='manage_cells') security.declareProtected(Permissions.View, 'manage_FTPget') def manage_FTPget(self, REQUEST=None, RESPONSE=None) : """ get this pdf form via webDAV/FTP, it returns an XML representation of all the fields, then the pdf itself.""" from xml.dom.minidom import getDOMImplementation impl = getDOMImplementation() newdoc = impl.createDocument(None, "pdfform", None) top_element = newdoc.documentElement cells = newdoc.createElement('cells') pdfform_cell_list = self.cells.keys() pdfform_cell_list.sort() for cell in pdfform_cell_list : cell_node = newdoc.createElement('cell') cell_node.setAttribute('name', cell) tales = newdoc.createTextNode(self.cells[cell]) cell_node.appendChild(tales) cells.appendChild(cell_node) top_element.appendChild(cells) pdf_data = newdoc.createElement('pdf_data') pdf_content = newdoc.createTextNode(str(self.data)) pdf_data.appendChild(pdf_content) top_element.appendChild(pdf_data) content = newdoc.toprettyxml(' ') if RESPONSE : RESPONSE.setHeader('Content-Type', 'application/x-erp5-pdfform') RESPONSE.setHeader('Content-Length', len(content)) RESPONSE.write(content) return content manage_DAVget = manage_FTPget security.declareProtected(Permissions.ManagePortal, 'PUT') def PUT(self, REQUEST, RESPONSE): """(does not) Handle HTTP PUT requests.""" RESPONSE.setStatus(501) return RESPONSE manage_FTPput = PUT security.declareProtected(Permissions.View, 'hasPdfContent') def hasPdfContent(self) : """Return true if there is an enclosed PDF in this PDF Form.""" return self.data is not None and len(self.data) > 0 security.declareProtected(Permissions.ManagePortal, 'downloadPdfContent') def downloadPdfContent(self, REQUEST=None) : """Download the pdf content from one of `download_url` URL """ for url in self.getProperty('download_url') : try : response = urllib.urlopen(url) except IOError, e : LOG("PDFForm", WARNING, "Unable to download from %s" % url, e) continue if response.headers.getheader('Content-Type') != 'application/pdf': LOG("PDFForm", WARNING, "%s is not application/pdf" % url) continue self.manage_upload(cStringIO.StringIO(response.read())) self.content_type = 'application/pdf' if REQUEST is not None : return REQUEST.RESPONSE.redirect( "%s/manage_main?manage_tabs_message=Content+Downloaded" % self.absolute_url()) return raise ValueError, "Unable to download from any url from the "\ "`download_url` property."
class PDFForm(File): """This class allows to fill PDF Form with TALES expressions, using a TALES expression for each cell. TODO: * cache compiled TALES * set _v_errors when setting invalid TALES (setCellTALES can raise, but not doEditCells) OBSOLETE : Not used any more. Such functionalities could be done with more modern tools """ meta_type = "ERP5 PDF Form" icon = "www/PDFForm.png" # Those 2 are ugly names, but we keep compatibility # the page range we want to print (a TALES expr) __page_range__ = '' # the method to format values (a TALES expr) __format_method__ = '' # Declarative Security security = ClassSecurityInfo() # Declarative properties _properties = File._properties + ( {'id' : 'download_url', 'type' : 'lines', 'mode' : 'w' }, {'id' : 'business_template_include_content', 'type' : 'boolean', 'mode' : 'w' }, ) download_url = () business_template_include_content = 1 # Constructors constructors = (manage_addPDFForm, addPDFForm) manage_options = ( ( {'label':'Edit Cell TALES', 'action':'manage_cells'}, {'label':'Display Cell Names', 'action':'showCellNames'}, {'label':'Test PDF generation', 'action':'generatePDF'}, {'label':'View original', 'action':'viewOriginal'}, {'label':'Download PDF content from URL', 'action':'downloadPdfContent'}, ) + filter(lambda option:option['label'] != "View", File.manage_options) ) def __init__ (self, id, title='', pdf_file=''): # holds information about all cells, even those not related to this form self.all_cells = PersistentMapping() # holds the cells related to this pdf form self.cells = PersistentMapping() # File constructor will set the file content File.__init__(self, id, title, pdf_file) security.declareProtected(Permissions.ManagePortal, 'manage_upload') def manage_upload(self, file=None, REQUEST=None) : """ Zope calls this when the content of the enclosed file changes. The 'cells' attribute is updated, but already defined cells are not erased, they are saved in the 'all_cells' attribute so if the pdf file is reverted, you do not loose the cells definitions. """ if not file or not hasattr(file, "read") : raise ValueError ("The pdf form file should not be empty") file.seek(0) # file is always valid here values = PDFTk().dumpDataFields(file) self.cells = {} for v in values : if v["FieldType"] not in ("Button", "Choice")\ or not int(v["FieldFlags"]) & 65536: k = v["FieldName"] if not self.all_cells.has_key(k) : self.cells[k] = "" else : self.cells[k] = self.all_cells[k] self.all_cells.update(self.cells) file.seek(0) File.manage_upload(self, file, REQUEST) if REQUEST: message = "Saved changes." return self.manage_main(self, REQUEST, manage_tabs_message=message) security.declareProtected(Permissions.ViewManagementScreens, 'manage_cells') manage_cells = PageTemplateFile('www/PDFForm_manageCells', globals(), __name__='manage_cells') security.declareProtected(Permissions.View, 'manage_FTPget') def manage_FTPget(self, REQUEST=None, RESPONSE=None) : """ get this pdf form via webDAV/FTP, it returns an XML representation of all the fields, then the pdf itself.""" from xml.dom.minidom import getDOMImplementation impl = getDOMImplementation() newdoc = impl.createDocument(None, "pdfform", None) top_element = newdoc.documentElement cells = newdoc.createElement('cells') pdfform_cell_list = self.cells.keys() pdfform_cell_list.sort() for cell in pdfform_cell_list : cell_node = newdoc.createElement('cell') cell_node.setAttribute('name', cell) tales = newdoc.createTextNode(self.cells[cell]) cell_node.appendChild(tales) cells.appendChild(cell_node) top_element.appendChild(cells) pdf_data = newdoc.createElement('pdf_data') pdf_content = newdoc.createTextNode(str(self.data)) pdf_data.appendChild(pdf_content) top_element.appendChild(pdf_data) content = newdoc.toprettyxml(' ') if RESPONSE : RESPONSE.setHeader('Content-Type', 'application/x-erp5-pdfform') RESPONSE.setHeader('Content-Length', len(content)) RESPONSE.write(content) return content manage_DAVget = manage_FTPget security.declareProtected(Permissions.ManagePortal, 'PUT') def PUT(self, REQUEST, RESPONSE): """(does not) Handle HTTP PUT requests.""" RESPONSE.setStatus(501) return RESPONSE manage_FTPput = PUT security.declareProtected(Permissions.View, 'hasPdfContent') def hasPdfContent(self) : """Return true if there is an enclosed PDF in this PDF Form.""" return self.data is not None and len(self.data) > 0 security.declareProtected(Permissions.ManagePortal, 'downloadPdfContent') def downloadPdfContent(self, REQUEST=None) : """Download the pdf content from one of `download_url` URL """ for url in self.getProperty('download_url') : try : response = urllib.urlopen(url) except IOError, e : LOG("PDFForm", WARNING, "Unable to download from %s" % url, e) continue if response.headers.getheader('Content-Type') != 'application/pdf': LOG("PDFForm", WARNING, "%s is not application/pdf" % url) continue self.manage_upload(cStringIO.StringIO(response.read())) self.content_type = 'application/pdf' if REQUEST is not None : return REQUEST.RESPONSE.redirect( "%s/manage_main?manage_tabs_message=Content+Downloaded" % self.absolute_url()) return raise ValueError, "Unable to download from any url from the "\ "`download_url` property."