class DocumentBank(Topic): """ A documentbank is a topic where you are able to arrange attachments in an hierarchical way. """ isObjectManager = True isPrincipiaFolderish = True meta_type = 'Topic' security = ClassSecurityInfo() security.declareObjectProtected(View) def __init__(self, topicid, *args, **kw): self.agendaManager = IOBTree() self.agendaTopLevel = [] Topic.__init__(self, topicid, *args, **kw) def manage_afterAdd(self, container, item): Topic.manage_afterAdd(self, container, item) def documentbanktopic(self): """ The documentbank itself. """ return self.aq_inner security.declarePrivate('notifyWorkflowCreated') def notifyWorkflowCreated(self): """ Notify the workflow that self was just created. """ wftool = self._getWorkflowTool() if wftool is not None: wftool.notifyCreated(self) #self._View_Permission = ['Editor', 'Manager']#list(self._View_Permission) def addAgendaObject(self, title, ingress, invokedOn=0, level=0): invokedOnAgendaNode = self.agendaManager.get(invokedOn, None) agendaNode = None if invokedOnAgendaNode: if level == 0: #same level agendaNode = AgendaObject(title, ingress, parent=invokedOnAgendaNode.parent) children = self.agendaTopLevel if invokedOnAgendaNode.parent: parent = self.agendaManager.get(invokedOnAgendaNode.parent, None) parent.p_modified = True children = parent.children else: self.p_modified = True index = 0 for serial in children: if serial == invokedOn: children.insert(index+1, agendaNode.serial) break; index = index + 1 elif level == 1: #level below parent = invokedOnAgendaNode.serial agendaNode = AgendaObject(title, ingress, parent=parent) invokedOnAgendaNode.children.append(agendaNode.serial) invokedOnAgendaNode._p_changed = True #bootstrap else: agendaNode = AgendaObject(title, ingress, parent=0) self.agendaTopLevel.append(agendaNode.serial) if agendaNode: self.agendaManager.insert(agendaNode.serial, agendaNode) self._p_changed = True def removeAgendaObject(self, invokedOn): invokedOnNode = self.agendaManager.get(invokedOn, None) if invokedOnNode: parentNode = self.agendaManager.get(invokedOnNode.parent, None) siblings = self.agendaTopLevel if parentNode: siblings = parentNode.children parentNode._p_changed = True else: self._p_changed = True children = invokedOnNode.children def recursiveRemove(children): for child in children: try: recursiveRemove(self.agendaManager.get(child, None).children) except AttributeError: pass del(self.agendaManager[child]) #Remove all children from the node we want to remove as well. recursiveRemove(children) siblings.remove(invokedOn) del(self.agendaManager[invokedOn]) def indentAgendaObject(self, invokedOn): invokedOnNode = self.agendaManager.get(invokedOn, None) if invokedOnNode: parentNode = self.agendaManager.get(invokedOnNode.parent, None) children = self.agendaTopLevel if parentNode: children = parentNode.children parentNode._p_changed = True else: self._p_changed = True index = 0 for serial in children: if serial == invokedOnNode.serial: if index == 0: raise AssertionError, "can't indent" #get previous node prevSibling = self.agendaManager.get(children[index-1], None) #make previous node parent invokedOnNode.parent = prevSibling.serial #add node to new parent children prevSibling.children.append(invokedOn) prevSibling._p_changed = True #remove node from current parent children.remove(invokedOnNode.serial) break index = index+1 return True def outdentAgendaObject(self, invokedOn): invokedOnNode = self.agendaManager.get(invokedOn, None) if invokedOnNode: parentNode = self.agendaManager.get(invokedOnNode.parent, None) if not parentNode: raise SyntaxError, "can't outdent" parentNode.children.remove(invokedOn) parentNode._p_changed = True grandparentNode = self.agendaManager.get(parentNode.parent, None) children = self.agendaTopLevel if grandparentNode: children = grandparentNode.children grandparentNode._p_changed = True else: self._p_changed = True index = 0 for serial in children: if serial == parentNode.serial: children.insert(index+1, invokedOn) break index = index+1 #set new parent invokedOnNode.parent = parentNode.parent return True def moveAgendaObjectUp(self, invokedOn): invokedOnNode = self.agendaManager.get(invokedOn, None) if invokedOnNode: parentNode = self.agendaManager.get(invokedOnNode.parent, None) children = self.agendaTopLevel if parentNode: children = parentNode.children parentNode._p_changed = True else: self._p_changed = True index = 0 for serial in children: if serial == invokedOnNode.serial: if index == 0: raise AssertionError, "on the top" else: children.remove(invokedOnNode.serial) children.insert(index-1, invokedOnNode.serial) break index = index+1 return True def moveAgendaObjectDown(self, invokedOn): invokedOnNode = self.agendaManager.get(invokedOn, None) if invokedOnNode: parentNode = self.agendaManager.get(invokedOnNode.parent, None) children = self.agendaTopLevel if parentNode: children = parentNode.children parentNode._p_changed = True else: self._p_changed = True index = 0 for serial in children: if serial == invokedOnNode.serial: if index == len(children)-1: raise AssertionError, "on the bottom" else: children.remove(invokedOnNode.serial) children.insert(index+1, invokedOnNode.serial) break index = index+1 return True security.declareProtected(View, 'getAgendaNode') def getAgendaNode(self, serial): return self.agendaManager.get(serial, None) def getAgenda(self): return self.agendaTopLevel security.declareProtected(ModifyPortalContent, 'uploadFile') def uploadFile(self, fileObject, title, overwrite=False): topicmap = self.portal_topicmaps.getTopicMap() atbsi = topicmap.assertTopicBySubjectIdentifier filetopictype = atbsi(psis.file) #TODO: get path creationpath = getattr(self, 'filearchive') filename = altTopicTitle = self.portal_topicmanagement.extractFilename(unicode(fileObject.filename, 'utf8')) origname = filename = self.portal_topicmanagement.normalizeid(filename) counter = 1 idIsAvailable = creationpath.checkIdAvailable(filename) topicid = filename+".topic" if not idIsAvailable: if overwrite and getattr(creationpath, topicid, False): #delete file. creationpath.manage_delObjects([filename]) filetopic = getattr(creationpath, topicid) #update title if set if title: filetopic.setTitleAndName(title) idIsAvailable = True else: raise AssertionError, "%s already exists. If you want to overwrite this file you must check the checkbox titled 'Overwrite existing file'"%title checkIdAvailable = creationpath.checkIdAvailable while not idIsAvailable: filename = origname[0:origname.rindex('.')]+'_'+str(counter)+'_'+origname[origname.rindex('.'):] counter += 1 idIsAvailable = checkIdAvailable(filename) if counter >= 50: raise AssertionError, "You entered an endless loop during fileupload. This should not have happened. Please write down the steps you used to trigger this bug and report it to the ZTM developers." if not title: title = altTopicTitle self.REQUEST.set('type_name', 'File') creationpath.invokeFactory(type_name='File', id=filename) newfile = getattr(creationpath, filename) newfile.manage_upload(fileObject) #create file topic. set file as subjectlocatior if not getattr(creationpath, topicid, False): self.REQUEST.set('type_name', 'file') creationpath.invokeFactory(type_name='file', id=topicid) filetopic = getattr(creationpath, topicid) filetopic.setTitleAndName(title) filetopic.addSubjectLocator('x-zope-path:' + '/'.join(newfile.getPhysicalPath())) else: if overwrite: #overwrite file clear subject locators: filetopic = getattr(creationpath, topicid) for locator in filetopic.getSubjectLocators(): filetopic.removeSubjectLocator(locator) #set new subject locator filetopic.addSubjectLocator('x-zope-path:' + '/'.join(newfile.getPhysicalPath())) filetopic.reindexObject() return filetopic.tm_serial security.declareProtected(ModifyPortalContent, 'openForAnonymousUsers') def openForAnonymousUsers(self, topic=None): #topic not given, open up the documentbank itself if not topic: topic = self allowedToAccessContent = list(getattr(topic, '_Access_contents_information_Permission', [])) if 'Anonymous' not in allowedToAccessContent: allowedToAccessContent.append('Anonymous') topic.manage_permission("Access contents information", allowedToAccessContent, acquire=0) allowedToViewContent = list(getattr(topic, '_View_Permission', [])) if 'Anonymous' not in allowedToViewContent: allowedToViewContent.append('Anonymous') topic.manage_permission("View", allowedToViewContent, acquire=0) security.declareProtected(ModifyPortalContent, 'closeForAnonymousUsers') def closeForAnonymousUsers(self, topic=None): #topic not given, close down the documentbank itself if not topic: lazy = self.portal_catalog(path='/'.join(self.getPhysicalPath()), types=[psis.file], sort_on='modified', sort_order='Reverse') for brain in lazy: topic = brain.getObject() self.closeForAnonymousUsers(topic) topic = self allowedToAccessContent = list(getattr(topic, '_Access_contents_information_Permission', [])) for user in allowedToAccessContent: if user == 'Anonymous': allowedToAccessContent.remove(user) topic.manage_permission("Access contents information",allowedToAccessContent, acquire=0) allowedToViewContent = list(getattr(topic, '_View_Permission', [])) for user in allowedToViewContent: if user == 'Anonymous': allowedToViewContent.remove(user) topic.manage_permission("View", allowedToViewContent, acquire=0) security.declareProtected(View, 'isOpen') def isOpen(self): allowedToAccessContent = list(getattr(self, '_Access_contents_information_Permission', [])) allowedToViewContent = list(getattr(self, '_View_Permission', [])) if 'Anonymous' in allowedToAccessContent and 'Anonymous' in allowedToViewContent: return True else: return False security.declareProtected(View, 'fileArchiveContents') def fileArchiveContents(self): return {} #mark files = [] append = files.append lazy = self.portal_catalog(path='/'.join(self.filearchive.getPhysicalPath()), types=[psis.file], sort_on='modified', sort_order='Reverse') for brain in lazy: topic = brain.getObject() fileobject = topic.resource_object() content_type = fileobject and fileobject.content_type or "" filetype = filetypes.has_key(content_type) and filetypes[content_type] or "fil" file_icon = fileicons[filetype] file_type = filenames[filetype] allowedToAccessContent = list(getattr(topic, '_Access_contents_information_Permission', [])) allowedToViewContent = list(getattr(topic, '_View_Permission', [])) is_open = bool('Anonymous' in allowedToAccessContent and 'Anonymous' in allowedToViewContent) append({'title':topic.title_or_id() ,'filetitle':fileobject.title_or_id() ,'Creator':topic.Creator ,'iconclass':file_icon ,'modified': DateTime(topic.modified()).strftime('%d.%m.%Y %H:%M') ,'tm_serial':topic.tm_serial ,'url':fileobject.absolute_url() ,'is_open':is_open }) files = [(x['title_or_id'],x) for x in files] files.sort() files = [y for (x,y) in files] return files security.declareProtected(ModifyPortalContent, 'publishFiles') def publishFiles(self, tm_serials): topicmap = self.getTopicMap() gtbs = topicmap.getTopicBySerial for tm_serial in tm_serials: try: topic = gtbs(tm_serial) self.openForAnonymousUsers(topic) topic.reindexObject() except KeyError: pass security.declareProtected(ModifyPortalContent, 'retractFiles') def retractFiles(self, tm_serials): topicmap = self.getTopicMap() gtbs = topicmap.getTopicBySerial for tm_serial in tm_serials: try: topic = gtbs(tm_serial) self.closeForAnonymousUsers(topic) topic.unindexObject() topic.indexObject() except KeyError: pass
class BoKeepBook(Persistent): """The equivilent of an accounting book in BoKeep. Meaning there should be one of these per entity keeping accounting records of itself. Each BoKeep book has frontend plugins (as specified by bokeep.prototype_plugin.ProtoTypePlugin) that provide transaction types. The eventual intent is that there can be multiple frontend plugin INSTANCES per frontend plugin in each book, that way the same frontend plugin can be used multiple times in a book but with a different configuration. Some steps in this direction have been taken, the plugin class from each plugin is instantiated, that is class variables aren't relied on, etc. Biggest barrier to this right now is that the plugin module/package name is used to identify each instance Its already the case the separate BoKeepBook's in the same BoKeepBookSet can each use the same frontend plugin, but with a different configuration each. Each book contains BoKeep transactions (bokeep.book_transaction.Trasaction) of those transaction types from the frontend plugins. These are indexed by assigned transaction ids in a BTree. (which allows for both fast map style direct lookup and linear, linked list style iteration) A book also has a backend plugin so that its transactions can be mirrored in a "real" accounting program. The backend plugin should be informed when a BoKeep transaction (bokeep.book_transaction.Transaction) is out of sync, as per the bokeep.backend_plugins.BackendPlugin api. Note that the fact that multiple front end plugins are allowed but only one backend plugin per book isn't an oversight -- the former is essential, the later has no obvious use case. I'd suggest that if someone does want multiple backend plugins per BoKeepBook that they design a Multiplex backend plugin to hide this and allow the simplicity of only dealing with one backend plugin to stay. """ def __init__(self, new_book_name): self.book_name = new_book_name self.trans_tree = IOBTree() self.set_backend_plugin(DEFAULT_BACKEND_MODULE) self.enabled_modules = {} self.disabled_modules = {} def add_frontend_plugin(self, plugin_name): """Add a frontend plugin to this BoKeep book, starting in a disabled state. FrontendPluginImportError is thrown if there's a problem.""" assert( plugin_name not in self.enabled_modules and plugin_name not in self.disabled_modules ) # get the module class and instantiate as a new disabled module try: self.disabled_modules[plugin_name] = __import__( plugin_name, globals(), locals(), [""]).get_plugin_class()() self._p_changed = True except ImportError: raise FrontendPluginImportError(plugin_name) def enable_frontend_plugin(self, plugin_name): """Enable a previously added frontend plugin that is currently disabled.""" assert( plugin_name in self.disabled_modules ) self.enabled_modules[plugin_name] = self.disabled_modules[plugin_name] del self.disabled_modules[plugin_name] self._p_changed = True def disable_frontend_plugin(self, plugin_name): """Disables a presently enabled frontend plugin.""" assert( plugin_name in self.enabled_modules ) self.disabled_modules[plugin_name] = self.enabled_modules[plugin_name] del self.enabled_modules[plugin_name] self._p_changed = True def get_frontend_plugin(self, plugin_name): return self.enabled_modules[plugin_name] def get_frontend_plugins(self): return self.enabled_modules def has_enabled_frontend_plugin(self, plugin_name): return plugin_name in self.enabled_modules def has_disabled_frontend_plugin(self, plugin_name): return plugin_name in self.disabled_modules def has_frontend_plugin(self, plugin_name): return self.has_enabled_frontend_plugin(plugin_name) or \ self.has_disabled_frontend_plugin(plugin_name) def get_iter_of_code_class_module_tripplets(self): # there has got to be a more functional way to write this.. # while also maintaining that good ol iterator don't waste memory # property... some kind of nice nested generator expressions # with some kind of functional/itertool y thing for frontend_plugin in self.enabled_modules.itervalues(): for code in frontend_plugin.get_transaction_type_codes(): yield (code, frontend_plugin.get_transaction_type_from_code(code), frontend_plugin) def get_index_and_code_class_module_tripplet_for_transaction( self, trans_id): actual_trans = self.get_transaction(trans_id) for i, (code, cls, module) in \ enumerate(self.get_iter_of_code_class_module_tripplets()): # the assumption here is that each class only co-responds # to one code, that needs to be clarified in the module # api if module.has_transaction(trans_id) and \ actual_trans.__class__ == cls: return i, (code, cls, module) return None, (None, None, None) def set_backend_plugin(self, backend_plugin_name): if hasattr(self, '_BoKeepBook__backend_module_name'): old_backend_module_name = self.get_backend_plugin_name() old_backend_module = self.get_backend_plugin() try: self.__backend_module_name = backend_plugin_name self.__backend_module = __import__( backend_plugin_name, globals(), locals(), [""] ).\ get_plugin_class()() assert( isinstance(self.__backend_module, BackendPlugin) ) # because we have changed the backend module, all transactions # are now dirty and must be re-written to the new backend module for trans_ident, trans in self.trans_tree.iteritems(): self.__backend_module.mark_transaction_dirty( trans_ident, trans) self.__backend_module.flush_backend() except ImportError: # this is in big trouble if old_backend_module_name isn't set # but that should only happen on book instantiation self.__backend_module_name = old_backend_module_name self.__backend_module = old_backend_module raise BackendPluginImportError(backend_plugin_name) def get_backend_plugin(self): return self.__backend_module def get_backend_plugin_name(self): return self.__backend_module_name backend_plugin = property(get_backend_plugin, set_backend_plugin) def insert_transaction(self, trans): """Adds a transaction to this BoKeep book.""" if len(self.trans_tree) == 0: largest_in_current = -1 else: largest_in_current = self.trans_tree.maxKey() if not hasattr(self, 'largest_key_ever'): self.largest_key_ever = largest_in_current key = max(largest_in_current, self.largest_key_ever) + 1 self.largest_key_ever = key result = self.trans_tree.insert(key, trans) assert( result == 1 ) return key def get_transaction_count(self): return len(self.trans_tree) def has_transaction(self, trans_id): return self.trans_tree.has_key(trans_id) def get_previous_trans(self, trans_id): if not self.has_transaction(trans_id): return None try: prev_key = self.trans_tree.maxKey(trans_id-1) except ValueError: return None else: return prev_key def has_next_trans(self, trans_id): return trans_id < self.trans_tree.maxKey() def has_previous_trans(self, trans_id): return trans_id > self.trans_tree.minKey() def get_next_trans(self, trans_id): """Returns the key of the next transaction or None.""" if not self.has_transaction(trans_id): return None try: next_key = self.trans_tree.minKey(trans_id+1) except ValueError: return None else: return next_key def get_transaction(self, trans_id): return self.trans_tree[trans_id] def get_latest_transaction_id(self): if len(self.trans_tree) == 0: return None else: return self.trans_tree.maxKey() def remove_transaction(self, trans_id): del self.trans_tree[trans_id] self.backend_plugin.mark_transaction_for_removal(trans_id)