############################################################################## # # Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved. # Jean-Paul Smets-Solane <jp@nexedi.com> # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # 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 Globals import PersistentMapping from Acquisition import Implicit from App.Permission import Permission from AccessControl import ClassSecurityInfo from Products.CMFCore.utils import getToolByName from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5Type.Utils import readLocalPropertySheet, writeLocalPropertySheet, importLocalPropertySheet from Products.ERP5Type.Utils import readLocalExtension, writeLocalExtension from Products.ERP5Type.Utils import readLocalDocument, writeLocalDocument, importLocalDocument from Products.ERP5Type.XMLObject import XMLObject import cStringIO from zLOG import LOG class TemplateItem(Implicit): pass # Compatibility class ObjectTemplateItem(Implicit): """ Attributes: tool_id -- Id of the tool relative_url_or_id -- URL relative to the tool relative_url -- Complete relative_url """ export_string = None def __init__(self, ob, **kw): self.__dict__.update(kw) self.export_string = cStringIO.StringIO() ob._p_jar.exportFile(ob._p_oid, self.export_string) self.export_string.seek(0) self.export_string = self.export_string.read() def install(self, local_configuration): portal = local_configuration.getPortalObject() container_path = self.relative_url.split('/')[0:-1] object_id = self.relative_url.split('/')[-1] container = portal.unrestrictedTraverse(container_path) #LOG('Installing' , 0, '%s in %s with %s' % (self.id, container.getPhysicalPath(), self.export_string)) container_ids = container.objectIds() if object_id in container_ids: # Object already exists pass # Do nothing for now #n = 0 #new_object_id = object_id #while new_object_id in container_ids: # n = n + 1 # new_object_id = '%s_btsave_%s' % (object_id, n) #container.manage_renameObject(object_id, new_object_id) else: container._importObjectFromFile(cStringIO.StringIO(self.export_string)) class PortalTypeTemplateItem(Implicit): """ Attributes: tool_id -- Id of the tool relative_url_or_id -- URL relative to the tool relative_url -- Complete relative_url """ export_string = None def __init__(self, ob, **kw): self.__dict__.update(kw) self.export_string = cStringIO.StringIO() ob._p_jar.exportFile(ob._p_oid, self.export_string) self.export_string.seek(0) self.export_string = self.export_string.read() def install(self, local_configuration): portal = local_configuration.getPortalObject() container_path = self.relative_url.split('/')[0:-1] object_id = self.relative_url.split('/')[-1] container = portal.unrestrictedTraverse(container_path) #LOG('Installing' , 0, '%s in %s with %s' % (self.id, container.getPhysicalPath(), self.export_string)) container_ids = container.objectIds() if object_id in container_ids: # Object already exists pass # Do nothing for now else: container._importObjectFromFile(cStringIO.StringIO(self.export_string)) class CatalogMethodTemplateItem(ObjectTemplateItem): def __init__(self, ob, **kw): ObjectTemplateItem.__init__(self, ob, **kw) method_id = ob.getId() portal_catalog = ob.portal_catalog self._is_catalog_method = method_id in portal_catalog.sql_catalog_object self._is_uncatalog_method = method_id in portal_catalog.sql_uncatalog_object self._is_update_method = method_id in portal_catalog.sql_update_object self._is_clear_method = method_id in portal_catalog.sql_clear_catalog def install(self, local_configuration): ObjectTemplateItem.install(self, local_configuration) portal = local_configuration.getPortalObject() portal_catalog = portal.portal_catalog method_id = self.id if self._is_catalog_method and method_id not in portal_catalog.sql_catalog_object: portal_catalog.sql_catalog_object = tuple([method_id] + portal_catalog.sql_catalog_object) if self._is_uncatalog_method and method_id not in portal_catalog.sql_uncatalog_object: portal_catalog.sql_uncatalog_object = tuple([method_id] + portal_catalog.sql_uncatalog_object) if self._is_update_method and method_id not in portal_catalog.sql_update_object: portal_catalog.sql_update_object = tuple([method_id] + portal_catalog.sql_update_object) if self._is_clear_method and method_id not in portal_catalog.sql_clear_catalog: portal_catalog.sql_clear_catalog = tuple([method_id] + portal_catalog.sql_clear_catalog) class ActionTemplateItem(Implicit): export_string = None def __init__(self, ai, **kw): self.__dict__.update(kw) self.__dict__.update(ai.__dict__) def install(self, portal, local_configuration): portal = local_configuration.getPortalObject() portal_type = portal.unrestrictedTraverse(self.relative_url) found_action = 0 for ai in object.listActions(): if getattr(ai, 'id') == self.action_id: found_action = 1 if not found_action: portal_type.addAction( self.id , self.title , self.action , self.permission , self.category , visible=self.visible ) class PropertyTemplateItem(Implicit): export_string = None def __init__(self, pi, **kw): self.__dict__.update(kw) self.property_definition = pi.copy() def install(self, local_configuration): portal = local_configuration.getPortalObject() object = portal.unrestrictedTraverse(self.relative_url) if not object.hasProperty(self.pi['id']): object._setProperty(pi['id'], type=pi['type']) class ModuleTemplateItem(Implicit): export_string = None def __init__(self, module, **kw): self.__dict__.update(kw) self.module_id = module.getId() self.module_type = module.getPortalType() self.module_permission_list = [] for p in module.ac_inherited_permissions(1): name, value = p[:2] permission=Permission(name,value,module) self.module_permission_list.append(permission) def install(self, local_configuration): portal = local_configuration.getPortalObject() if self.module_id not in portal.objectIds(): # No renaming mapping for now portal.newContent(id=self.module_id, portal_type=self.module_type) # Missing permissions class BusinessTemplate(XMLObject): """ A business template allows to construct ERP5 modules in part or completely. It may include: - dependency - conflicts - catalog definition ( -> formal definition + sql files ) - SQL methods including: - purpose (catalog, uncatalog, etc.) - filter definition - Mapping definition - id (ex. getTitle) - column_id (ex. title) - indexed - preferred table (ex. catalog) - portal_types definition ( -> zexp/xml file) - id - actions - module definition ( -> zexp/xml file) - id - relative_url - menus - roles/security - workflow definitions ( -> zexp/xml file) - workflow_id - XML/XMI definition - relevant portal_types - tool definition ( -> formal definition) - categories definition Each definition should be usable in both import and update mode. Technology: - download a zip file (from the web, from a CVS repository) - install files to the right location (publish / update) (in the ZODB) - PUBLISH: publish method allows to publish an application (and share code) publication in a CVS repository allows to develop THIS IS THE MOST IMPORTANT CONCEPT Use case: - install core ERP5 (the minimum) - go to "BT" menu. Refresh list. Select BT. Click register. - go to "BT" menu. Select register BT. Define params. Click install / update. - go to "BT" menu. Create new BT. Define BT elements (workflow, methods, attributes, etc.). Click publish. Provide URL. Done. """ meta_type = 'ERP5 Business Template' portal_type = 'Business Template' add_permission = Permissions.AddERP5Content isPortalContent = 1 isRADContent = 1 # Declarative security security = ClassSecurityInfo() security.declareObjectProtected(Permissions.View) # Declarative interfaces __implements__ = ( Interface.Variated, ) # Declarative properties property_sheets = ( PropertySheet.Base , PropertySheet.XMLObject , PropertySheet.CategoryCore , PropertySheet.BusinessTemplate ) # Factory Type Information factory_type_information = \ { 'id' : portal_type , 'meta_type' : meta_type , 'description' : """\ Une ligne tarifaire.""" , 'icon' : 'order_line_icon.gif' , 'product' : 'ERP5' , 'factory' : 'addBusinessTemplate' , 'immediate_view' : 'BusinessTemplate_view' , 'allow_discussion' : 1 , 'allowed_content_types': ('BusinessTemplate', ) , 'filter_content_types' : 1 , 'global_allow' : 1 , 'actions' : ( { 'id' : 'view' , 'name' : 'View' , 'category' : 'object_view' , 'action' : 'BusinessTemplate_view' , 'permissions' : ( Permissions.View, ) } , { 'id' : 'list' , 'name' : 'Object Contents' , 'category' : 'object_action' , 'action' : 'folder_contents' , 'permissions' : ( Permissions.View, ) } , { 'id' : 'print' , 'name' : 'Print' , 'category' : 'object_print' , 'action' : 'BusinessTemplate_print' , 'permissions' : ( Permissions.View, ) } , { 'id' : 'metadata' , 'name' : 'Metadata' , 'category' : 'object_view' , 'action' : 'metadata_view' , 'permissions' : ( Permissions.View, ) } , { 'id' : 'translate' , 'name' : 'Translate' , 'category' : 'object_action' , 'action' : 'translation_template_view' , 'permissions' : ( Permissions.TranslateContent, ) } ) } def initInstance(self): self._object_archive = PersistentMapping() self._portal_type_archive = PersistentMapping() self._action_archive = PersistentMapping() self._property_archive = PersistentMapping() self._module_archive = PersistentMapping() self._document_archive = PersistentMapping() self._property_sheet_archive = PersistentMapping() self._extension_archive = PersistentMapping() def checkInstance(self): if not hasattr(self, '_object_archive'): self._object_archive = PersistentMapping() if not hasattr(self, '_action_archive'): self._action_archive = PersistentMapping() if not hasattr(self, '_property_archive'): self._property_archive = PersistentMapping() if not hasattr(self, '_module_archive'): self._module_archive = PersistentMapping() if not hasattr(self, '_document_archive'): self._document_archive = PersistentMapping() if not hasattr(self, '_property_sheet_archive'): self._property_sheet_archive = PersistentMapping() if not hasattr(self, '_extension_archive'): self._extension_archive = PersistentMapping() def addObjectTemplateItem(self, relative_url_or_id, tool_id=None): if relative_url_or_id in ('', None): return # Make sure empty lines are eliminated p = self.getPortalObject() if tool_id is not None: relative_url = "%s/%s" % (tool_id, relative_url_or_id) object = p.unrestrictedTraverse(relative_url) if object is not None: self._object_archive[(relative_url_or_id, tool_id)] = ObjectTemplateItem(object, id = object.id, tool_id=tool_id, relative_url=relative_url, relative_url_or_id=relative_url_or_id) def addPortalTypeTemplateItem(self, relative_url_or_id, tool_id=None): if relative_url_or_id in ('', None): return # Make sure empty lines are eliminated p = self.getPortalObject() if tool_id is not None: relative_url = "%s/%s" % (tool_id, relative_url_or_id) object = p.unrestrictedTraverse(relative_url) if object is not None: # Set the workflow_chain thanks to the portal_workflow portal_type = relative_url_or_id (default_chain, chain_dict) = self._getChainByType() workflow_chain = chain_dict['chain_%s' % portal_type] self._portal_type_archive[(relative_url_or_id, tool_id)] = \ PortalTypeTemplateItem(object, id = object.id, tool_id=tool_id, relative_url=relative_url, relative_url_or_id=relative_url_or_id, portal_type = portal_type, workflow_chain = workflow_chain) def addCatalogMethodTemplateItem(self, relative_url_or_id, tool_id=None): if relative_url_or_id in ('', None): return # Make sure empty lines are eliminated p = self.getPortalObject() if tool_id is not None: relative_url = "%s/%s" % (tool_id, relative_url_or_id) object = p.unrestrictedTraverse(relative_url) if object is not None: self._object_archive[(relative_url_or_id, tool_id)] = CatalogMethodTemplateItem(object, id = object.id, tool_id=tool_id, relative_url=relative_url, relative_url_or_id=relative_url_or_id) def splitPath(self, path): """ Split path tries to split a complexe path such as: "foo/bar[id=zoo]" into "foo/bar", "id", "zoo" This is used mostly for generic objects """ # Add error checking here if path.find('[') >= 0 and path.find(']') > path.find('=') and path.find('=') > path.find('['): relative_url = path[0:path.find('[')] id_block = path[path.find('[')+1:path.find(']')] key = id_block.split('=')[0] value = id_block.split('=')[1] return relative_url, key, value return path, None, None def addActionTemplateItem(self, path): relative_url, key, value = self.splitPath(path) p = self.getPortalObject() object = p.unrestrictedTraverse(relative_url) for ai in object.listActions(): # Replace this with some kind of regexp if getattr(ai, key) == value: self._action_archive[path] = ActionTemplateItem(ai, id = (key, value), relative_url = relative_url, path = path) def addSitePropertyTemplateItem(self, path): relative_url, key, value = self.splitPath(path) p = self.getPortalObject() object = p.unrestrictedTraverse(relative_url) for pi in object.propertyMap(): if pi.get(key) == value: # Replace this with some kind of regexp self._property_archive[path] = PropertyTemplateItem(pi, id = (key, value), value = object.getProperty(value), type = object.getPropertyType(value), relative_url = relative_url, path = path) def addModuleTemplateItem(self, id): module = self.getPortalObject().unrestrictedTraverse(id) self._module_archive[id] = ModuleTemplateItem(module, id=id) def addDocumentTemplateItem(self, id): self._document_archive[id] = readLocalDocument(id) def addPropertySheetTemplateItem(self, id): self._property_sheet_archive[id] = readLocalPropertySheet(id) def addExtensionTemplateItem(self, id): self._extension_archive[id] = readLocalExtension(id) def build(self): """ Copy existing portal objects to self """ self.initInstance() # Copy portal_types for id in self.getTemplatePortalTypeIdList(): self.addPortalTypeTemplateItem(id, 'portal_types') # Copy workflows for id in self.getTemplateWorkflowIdList(): self.addObjectTemplateItem(id, 'portal_workflow') # Copy skins for id in self.getTemplateSkinIdList(): self.addObjectTemplateItem(id, 'portal_skins') # Copy categories for id in self.getTemplateBaseCategoryList(): self.addObjectTemplateItem(id, 'portal_categories') # Copy catalog methods for id in self.getTemplateCatalogMethodIdList(): self.addCatalogMethodTemplateItem(id, 'portal_catalog') # Copy actions for path in self.getTemplateActionPathList(): self.addActionTemplateItem(path) # Copy properties for id in self.getTemplateSitePropertyIdList(): self.addSitePropertyTemplateItem("[id=%s]" % id) # Copy modules for id in self.getTemplateModuleIdList(): self.addModuleTemplateItem(id) # Copy Document Classes for id in self.getTemplateDocumentIdList(): self.addDocumentTemplateItem(id) # Copy Propertysheet Classes for id in self.getTemplatePropertySheetIdList(): self.addPropertySheetTemplateItem(id) # Copy Extensions Classes (useful for catalog) for id in self.getTemplateExtensionIdList(): self.addExtensionTemplateItem(id) # Copy Products ### Make a tar archive and copy into local archive # Copy roles ### Nothing to do # Copy catalog columns ### Nothing to do # Copy catalog result tables ### Nothing to do # Copy Permissions ### Copy root values # Other objects and properties for path in self.getTemplatePathList(): for id in self.getTemplatePortalTypeIdList(): if path.find('=') >= 0: # This is a property self.addSitePropertyTemplateItem(path) else: # This is an object self.addObjectTemplateItem(path) def publish(self, url, username=None, password=None): """ Publish in a format or another """ return self.portal_templates.publish(self, url, username=username, password=password) def update(self): """ Update template: download new template defition """ return self.portal_templates.update(self) def upgrade(self): """ Upgrade the current portal with self.installModules(linstallObjectsocal_configuration, update=update) this template definition """ self.install(update=1) def install(self, update=0, **kw): """ For install based on paramaters provided in **kw """ # Update local dictionnary containing all setup parameters # This may include mappings self.portal_templates.updateLocalConfiguration(self, **kw) local_configuration = self.portal_templates.getLocalConfiguration(self) LOG('install Business Template: ',0,'local dictionnary updated') # Classes and security information self.installPropertySheets(local_configuration, update=update) self.installDocuments(local_configuration, update=update) self.installExtensions(local_configuration, update=update) self.installRoles(local_configuration, update=update) self.installPermissions(local_configuration, update=update) LOG('install Business Template: ',0,'security information updated') # Objects and properties self.installObjects(local_configuration, update=update) self.installProperties(local_configuration, update=update) LOG('install Business Template: ',0,'object and properties updated') # Skins self.installSkins(local_configuration, update=update) LOG('install Business Template: ',0,'skins updated') # Portal Types self.installPortalTypes(local_configuration, update=update) LOG('install Business Template: ',0,'portal types updated') # Actions, modules, catalog self.installActions(local_configuration, update=update) self.installModules(local_configuration, update=update) self.installCatalog(local_configuration, update=update) LOG('install Business Template: ',0,'action, modules and catalog updated') def installPropertySheets(self, local_configuration, update=0): """ Install PropertySheet files into local instance """ for id, text in self._property_sheet_archive.items(): writeLocalPropertySheet(id, text) importLocalPropertySheet(id) def installDocuments(self, local_configuration, update=0): """ Install Document files into local instance """ for id, text in self._document_archive.items(): writeLocalDocument(id, text) importLocalDocument(id) def installExtensions(self, local_configuration, update=0): """ Install Extension files into local instance """ for id, text in self._extension_archive.items(): writeLocalExtension(id, text) def installRoles(self, local_configuration, update=0): """ Add template roles to portal """ p = local_configuration.getPortalObject() roles = {} for role in p.__ac_roles__: roles[role] = 1 for role in self.getTemplateRoleList(): roles[role] = 1 p.__ac_roles__ = tuple(roles.keys()) def installPermissions(self, local_configuration, update=0): """ Nothing for now """ #mp = p.manage_permission #mp('Set own password', ['Member','Manager',], 1) def installSkins(self, local_configuration, update=0): """ Make sure installed skins are defined in skin properties """ portal_skins = self.portal_skins for skin_name, selection in portal_skins.getSkinPaths(): new_selection = [] for skin_id in self.getTemplateSkinIdList(): if skin_id not in selection: new_selection.append(skin_id) new_selection.append(selection) portal_skins.manage_skinLayers(chosen = tuple(new_selection), skinname=skin_name) def installProperties(self, local_configuration, update=0): """ Create properties if needed """ for o in self._property_archive.values(): o.install(local_configuration) def installActions(self, local_configuration, update=0): """ Create actions if needed """ for o in self._action_archive.values(): o.install(local_configuration) def installModules(self, local_configuration, update=0): """ Create modules if needed """ for o in self._module_archive.values(): o.install(local_configuration) def installCatalog(self, local_configuration, update=0): """ Add tables and keys to catalog default search_result """ portal_catalog = self.portal_catalog for c in self.getTemplateCatalogResultKeyList(): if c not in portal_catalog.sql_search_result_keys: portal_catalog.sql_search_result_keys = tuple([c] + portal_catalog.sql_search_result_keys) for t in self.getTemplateCatalogResultTableList(): if c not in portal_catalog.sql_search_tables: portal_catalog.sql_search_tables = tuple([c] + portal_catalog.sql_search_tables) def installObjects(self, local_configuration, update=0): """ """ for o in self._object_archive.values(): o.install(local_configuration) def installPortalTypes(self, local_configuration, update=0): """ """ portal_workflow = self.portal_workflow for o in self._portal_type_archive.values(): o.install(local_configuration) # We now need to setup the list of workflows corresponding to # each portal type (default_chain, chain_dict) = self._getChainByType() # Set the default chain to the empty string is probably the # best solution, by default it is 'default_workflow', wich is # not very usefull default_chain = '' LOG('installPortalTypes, portal_type: ',0,o.portal_type) LOG('installPortalTypes, workflow_chain: ',0,repr(o.workflow_chain)) LOG('installPortalTypes, chain_dict: ',0,chain_dict) LOG('installPortalTypes, default_chain: ',0,default_chain) chain_dict['chain_%s' % o.portal_type] = o.workflow_chain portal_workflow.manage_changeWorkflows(default_chain,props=chain_dict) def _getChainByType(self): """ This is used in order to construct the full list of mapping between type and list of workflow associated This is only usefull in order to use portal_workflow.manage_changeWorkflows """ self = self.portal_workflow cbt = self._chains_by_type ti = self._listTypeInfo() types_info = [] for t in ti: id = t.getId() title = t.Title() if title == id: title = None if cbt is not None and cbt.has_key(id): chain = ', '.join(cbt[id]) else: chain = '(Default)' types_info.append({'id': id, 'title': title, 'chain': chain}) new_dict = {} for item in types_info: new_dict['chain_%s' % item['id']] = item['chain'] default_chain=', '.join(self._default_chain) return (default_chain, new_dict)