'''This package contains stuff used at run-time for installing a generated Plone product.''' # ------------------------------------------------------------------------------ import os, os.path from StringIO import StringIO from sets import Set from appy.gen.utils import produceNiceMessage from appy.gen.plone25.utils import updateRolesForPermission class PloneInstaller: '''This Plone installer runs every time the generated Plone product is installed or uninstalled (in the Plone configuration interface).''' def __init__(self, reinstall, productName, ploneSite, minimalistPlone, appClasses, appClassNames, allClassNames, catalogMap, applicationRoles, defaultAddRoles, workflows, appFrontPage, showPortlet, ploneStuff): self.reinstall = reinstall # Is it a fresh install or a re-install? self.productName = productName self.ploneSite = ploneSite self.minimalistPlone = minimalistPlone # If True, lots of basic Plone # stuff will be hidden. self.appClasses = appClasses # The list of classes declared in the # gen-application. self.appClassNames = appClassNames # Names of those classes self.allClassNames = allClassNames # Includes Flavour and PodTemplate self.catalogMap = catalogMap # Indicates classes to be indexed or not self.applicationRoles = applicationRoles # Roles defined in the app self.defaultAddRoles = defaultAddRoles # The default roles that can add # content self.workflows = workflows # Dict whose keys are class names and whose # values are workflow names (=the workflow # used by the content type) self.appFrontPage = appFrontPage # Does this app define a site-wide # front page? self.showPortlet = showPortlet # Must we show the application portlet? self.ploneStuff = ploneStuff # A dict of some Plone functions or vars self.toLog = StringIO() self.typeAliases = {'sharing': '', 'gethtml': '', '(Default)': '%s_appy_view' % self.productName, 'edit': '%s_appy_edit' % self.productName, 'index.html': '', 'properties': '', 'view': ''} self.tool = None # The Plone version of the application tool self.appyTool = None # The Appy version of the application tool self.toolName = '%sTool' % self.productName self.toolInstanceName = 'portal_%s' % self.productName.lower() actionsToHide = { 'portal_actions': ('sitemap', 'accessibility', 'change_state','sendto'), 'portal_membership': ('mystuff', 'preferences'), 'portal_undo': ('undo',) } def customizePlone(self): '''Hides some UI elements that appear by default in Plone.''' for portalName, toHide in self.actionsToHide.iteritems(): portal = getattr(self.ploneSite, portalName) portalActions = portal.listActions() for action in portalActions: if action.id in toHide: action.visible = False appyFolderType = 'AppyFolder' def registerAppyFolderType(self): '''We need a specific content type for the folder that will hold all objects created from this application, in order to remove it from Plone navigation settings. We will create a new content type based on Large Plone Folder.''' if not hasattr(self.ploneSite.portal_types, self.appyFolderType): portal_types = self.ploneSite.portal_types lpf = 'Large Plone Folder' largePloneFolder = getattr(portal_types, lpf) typeInfoName = 'ATContentTypes: ATBTreeFolder (ATBTreeFolder)' portal_types.manage_addTypeInformation( largePloneFolder.meta_type, id=self.appyFolderType, typeinfo_name=typeInfoName) appyFolder = getattr(portal_types, self.appyFolderType) appyFolder.title = 'Appy folder' #appyFolder.factory = largePloneFolder.factory #appyFolder.product = largePloneFolder.product # Copy actions and aliases appyFolder._actions = tuple(largePloneFolder._cloneActions()) # Copy aliases from the base portal type appyFolder.setMethodAliases(largePloneFolder.getMethodAliases()) # Prevent Appy folders to be visible in standard Plone navigation nv = self.ploneSite.portal_properties.navtree_properties metaTypesNotToList = list(nv.getProperty('metaTypesNotToList')) if self.appyFolderType not in metaTypesNotToList: metaTypesNotToList.append(self.appyFolderType) nv.manage_changeProperties( metaTypesNotToList=tuple(metaTypesNotToList)) def getAddPermission(self, className): '''What is the name of the permission allowing to create instances of class whose name is p_className?''' return self.productName + ': Add ' + className def installRootFolder(self): '''Creates and/or configures, at the root of the Plone site and if needed, the folder where the application will store instances of root classes.''' # Register first our own Appy folder type if needed. site = self.ploneSite if not hasattr(site.portal_types, self.appyFolderType): self.registerAppyFolderType() # Create the folder if not hasattr(site.aq_base, self.productName): # Temporarily allow me to create Appy large plone folders getattr(site.portal_types, self.appyFolderType).global_allow = 1 site.invokeFactory(self.appyFolderType, self.productName, title=self.productName) getattr(site.portal_types, self.appyFolderType).global_allow = 0 appFolder = getattr(site, self.productName) # All roles defined as creators should be able to create the # corresponding root content types in this folder. i = -1 allCreators = set() for klass in self.appClasses: i += 1 if klass.__dict__.has_key('root') and klass.__dict__['root']: # It is a root class. creators = getattr(klass, 'creators', None) if not creators: creators = self.defaultAddRoles allCreators = allCreators.union(creators) className = self.appClassNames[i] updateRolesForPermission(self.getAddPermission(className), tuple(creators), appFolder) # Beyond content-type-specific "add" permissions, creators must also # have the main permission "Add portal content". updateRolesForPermission('Add portal content', tuple(allCreators), appFolder) def installTypes(self): '''Registers and configures the Plone content types that correspond to gen-classes.''' site = self.ploneSite # Do Plone-based type registration classes = self.ploneStuff['listTypes'](self.productName) self.ploneStuff['installTypes'](site, self.toLog, classes, self.productName) self.ploneStuff['install_subskin'](site, self.toLog, self.ploneStuff['GLOBALS']) # Set appy view/edit pages for every created type for className in self.allClassNames + ['%sTool' % self.productName]: # I did not put the app tool in self.allClassNames because it # must not be registered in portal_factory if hasattr(site.portal_types, className): # className may correspond to an abstract class that has no # corresponding Plone content type typeInfo = getattr(site.portal_types, className) typeInfo.setMethodAliases(self.typeAliases) # Update edit and view actions typeActions = typeInfo.listActions() for action in typeActions: if action.id == 'view': page = '%s_appy_view' % self.productName action.edit(action='string:${object_url}/%s' % page) elif action.id == 'edit': page = '%s_appy_edit' % self.productName action.edit(action='string:${object_url}/%s' % page) # Configure types for instance creation through portal_factory factoryTool = site.portal_factory factoryTypes = self.allClassNames + factoryTool.getFactoryTypes().keys() factoryTool.manage_setPortalFactoryTypes(listOfTypeIds=factoryTypes) # Configure CatalogMultiplex: tell what types will be catalogued or not. atTool = getattr(site, self.ploneStuff['ARCHETYPETOOLNAME']) for meta_type in self.catalogMap: submap = self.catalogMap[meta_type] current_catalogs = Set( [c.id for c in atTool.getCatalogsByType(meta_type)]) if 'white' in submap: for catalog in submap['white']: current_catalogs.update([catalog]) if 'black' in submap: for catalog in submap['black']: if catalog in current_catalogs: current_catalogs.remove(catalog) atTool.setCatalogsByType(meta_type, list(current_catalogs)) def findPodFile(self, klass, podTemplateName): '''Finds the file that corresponds to p_podTemplateName for p_klass.''' res = None exec 'import %s' % klass.__module__ exec 'moduleFile = %s.__file__' % klass.__module__ folderName = os.path.dirname(moduleFile) fileName = os.path.join(folderName, '%s.odt' % podTemplateName) if os.path.isfile(fileName): res = fileName return res def updatePodTemplates(self): '''Creates or updates the POD templates in flavours according to pod declarations in the application classes.''' i = -1 for klass in self.appClasses: i += 1 if klass.__dict__.has_key('pod'): pod = getattr(klass, 'pod') if isinstance(pod, bool): podTemplates = [klass.__name__] else: podTemplates = pod for templateName in podTemplates: fileName = self.findPodFile(klass, templateName) if fileName: # Create the corresponding PodTemplate in all flavours for flavour in self.appyTool.flavours: podId='%s_%s' % (self.appClassNames[i],templateName) podAttr = 'podTemplatesFor%s'% self.appClassNames[i] allPodTemplates = getattr(flavour, podAttr) if allPodTemplates: if isinstance(allPodTemplates, list): allIds = [p.id for p in allPodTemplates] else: allIds = [allPodTemplates.id] else: allIds = [] if podId not in allIds: # Create a PodTemplate instance f = file(fileName) flavour.create(podAttr, id=podId, podTemplate=f, title=produceNiceMessage(templateName)) f.close() def installTool(self): '''Configures the application tool and flavours.''' # Register the tool in Plone try: self.ploneSite.manage_addProduct[ self.productName].manage_addTool(self.toolName) except self.ploneStuff['BadRequest']: # If an instance with the same name already exists, this error will # be unelegantly raised by Zope. pass except: e = sys.exc_info() if e[0] != 'Bad Request': raise # Hide the tool from the search form portalProperties = self.ploneSite.portal_properties if portalProperties is not None: siteProperties = getattr(portalProperties, 'site_properties', None) if siteProperties is not None and \ siteProperties.hasProperty('types_not_searched'): current = list(siteProperties.getProperty('types_not_searched')) if self.toolName not in current: current.append(self.toolName) siteProperties.manage_changeProperties( **{'types_not_searched' : current}) # Hide the tool in the navigation if portalProperties is not None: nvProps = getattr(portalProperties, 'navtree_properties', None) if nvProps is not None and nvProps.hasProperty('idsNotToList'): current = list(nvProps.getProperty('idsNotToList')) if self.toolInstanceName not in current: current.append(self.toolInstanceName) nvProps.manage_changeProperties(**{'idsNotToList': current}) # Remove workflow for the tool #wfTool = self.ploneSite.portal_workflow #wfTool.setChainForPortalTypes([self.toolName], '') # Create the default flavour self.tool = getattr(self.ploneSite, self.toolInstanceName) self.appyTool = self.tool._appy_getWrapper(force=True) if self.reinstall: self.tool.at_post_edit_script() else: self.tool.at_post_create_script() if not self.appyTool.flavours: self.appyTool.create('flavours', title=self.productName, number=1) self.updatePodTemplates() # Uncatalog tool self.tool.unindexObject() # Register tool as configlet portalControlPanel = self.ploneSite.portal_controlpanel portalControlPanel.unregisterConfiglet(self.toolName) portalControlPanel.registerConfiglet( self.toolName, self.productName, 'string:${portal_url}/%s' % self.toolInstanceName, 'python:True', 'Manage portal', # Access permission 'Products', # Section to which the configlet should be added: # (Plone, Products (default) or Member) 1, # Visibility '%sID' % self.toolName, 'site_icon.gif', # Icon in control_panel self.productName, None) def installRolesAndGroups(self): '''Registers roles used by workflows defined in this application if they are not registered yet. Creates the corresponding groups if needed.''' site = self.ploneSite data = list(site.__ac_roles__) for role in self.applicationRoles: if not role in data: data.append(role) # Add to portal_role_manager # First, try to fetch it. If it's not there, we probaly have no # PAS or another way to deal with roles was configured. try: prm = site.acl_users.get('portal_role_manager', None) if prm is not None: try: prm.addRole(role, role, "Added by product '%s'" % self.productName) except KeyError: # Role already exists pass except AttributeError: pass # Create a specific group and grant him this role group = '%s_group' % role if not site.portal_groups.getGroupById(group): site.portal_groups.addGroup(group, title=group) site.portal_groups.setRolesForGroup(group, [role]) site.__ac_roles__ = tuple(data) def installWorkflows(self): '''Creates or updates the workflows defined in the application.''' wfTool = self.ploneSite.portal_workflow for contentType, workflowName in self.workflows.iteritems(): # Register the workflow if needed if workflowName not in wfTool.listWorkflows(): wfMethod = self.ploneStuff['ExternalMethod']('temp', 'temp', self.productName + '.workflows', 'create_%s' % workflowName) workflow = wfMethod(self, workflowName) wfTool._setObject(workflowName, workflow) else: self.log('%s already in workflows.' % workflowName) # Link the workflow to the current content type wfTool.setChainForPortalTypes([contentType], workflowName) return wfTool def installStyleSheet(self): '''Registers In Plone the stylesheet linked to this application.''' cssName = self.productName + '.css' cssTitle = self.productName + ' CSS styles' cssInfo = {'id': cssName, 'title': cssTitle} try: portalCss = self.ploneSite.portal_css try: portalCss.unregisterResource(cssInfo['id']) except: pass defaults = {'id': '', 'media': 'all', 'enabled': True} defaults.update(cssInfo) portalCss.registerStylesheet(**defaults) except: # No portal_css registry pass def managePortlets(self): '''Shows or hides the application-specific portlet and configures other Plone portlets if relevant.''' portletName= 'here/%s_portlet/macros/portlet' % self.productName.lower() site = self.ploneSite leftPortlets = site.getProperty('left_slots') if not leftPortlets: leftPortlets = [] else: leftPortlets = list(leftPortlets) if self.showPortlet and (portletName not in leftPortlets): leftPortlets.insert(0, portletName) if not self.showPortlet and (portletName in leftPortlets): leftPortlets.remove(portletName) # Remove some basic Plone portlets that make less sense when building # web applications. portletsToRemove = ["here/portlet_navigation/macros/portlet", "here/portlet_recent/macros/portlet", "here/portlet_related/macros/portlet"] if not self.minimalistPlone: portletsToRemove = [] for p in portletsToRemove: if p in leftPortlets: leftPortlets.remove(p) site.manage_changeProperties(left_slots=tuple(leftPortlets)) if self.minimalistPlone: site.manage_changeProperties(right_slots=()) def finalizeInstallation(self): '''Performs some final installation steps.''' site = self.ploneSite # Do not generate an action (tab) for each root folder if self.minimalistPlone: site.portal_properties.site_properties.manage_changeProperties( disable_folder_sections=True) # Do not allow an anonymous user to register himself as new user site.manage_permission('Add portal member', ('Manager',), acquire=0) # Call custom installer if any if hasattr(self.appyTool, 'install'): self.tool.executeAppyAction('install', reindex=False) # Replace Plone front-page with an application-specific page if needed if self.appFrontPage: frontPageName = self.productName + 'FrontPage' site.manage_changeProperties(default_page=frontPageName) def log(self, msg): print >> self.toLog, msg def install(self): self.log("Installation of %s:" % self.productName) if self.minimalistPlone: self.customizePlone() self.installRootFolder() self.installTypes() self.installTool() self.installRolesAndGroups() self.installWorkflows() self.installStyleSheet() self.managePortlets() self.finalizeInstallation() self.log("Installation of %s done." % self.productName) return self.toLog.getvalue() def uninstallTool(self): site = self.ploneSite # Unmention tool in the search form portalProperties = getattr(site, 'portal_properties', None) if portalProperties is not None: siteProperties = getattr(portalProperties, 'site_properties', None) if siteProperties is not None and \ siteProperties.hasProperty('types_not_searched'): current = list(siteProperties.getProperty('types_not_searched')) if self.toolName in current: current.remove(self.toolName) siteProperties.manage_changeProperties( **{'types_not_searched' : current}) # Unmention tool in the navigation if portalProperties is not None: nvProps = getattr(portalProperties, 'navtree_properties', None) if nvProps is not None and nvProps.hasProperty('idsNotToList'): current = list(nvProps.getProperty('idsNotToList')) if self.toolInstanceName in current: current.remove(self.toolInstanceName) nvProps.manage_changeProperties(**{'idsNotToList': current}) def uninstall(self): self.log("Uninstallation of %s:" % self.productName) self.uninstallTool() self.log("Uninstallation of %s done." % self.productName) return self.toLog.getvalue() # ------------------------------------------------------------------------------ class ZopeInstaller: '''This Zope installer runs every time Zope starts and encounters this generated Zope product.''' def __init__(self, zopeContext, productName, toolClass, defaultAddContentPermission, addContentPermissions, logger, ploneStuff): self.zopeContext = zopeContext self.productName = productName self.toolClass = toolClass self.defaultAddContentPermission = defaultAddContentPermission self.addContentPermissions = addContentPermissions self.logger = logger self.ploneStuff = ploneStuff # A dict of some Plone functions or vars def installApplication(self): '''Performs some application-wide installation steps.''' self.ploneStuff['DirectoryView'].registerDirectory('skins', self.ploneStuff['product_globals']) def installTool(self): '''Installs the tool.''' self.ploneStuff['ToolInit'](self.productName + ' Tools', tools = [self.toolClass], icon='tool.gif').initialize( self.zopeContext) def installTypes(self): '''Installs and configures the types defined in the application.''' contentTypes, constructors, ftis = self.ploneStuff['process_types']( self.ploneStuff['listTypes'](self.productName), self.productName) self.ploneStuff['cmfutils'].ContentInit(self.productName + ' Content', content_types = contentTypes, permission = self.defaultAddContentPermission, extra_constructors = constructors, fti = ftis).initialize( self.zopeContext) # Define content-specific "add" permissions for i in range(0, len(contentTypes)): className = contentTypes[i].__name__ if not className in self.addContentPermissions: continue self.zopeContext.registerClass(meta_type = ftis[i]['meta_type'], constructors = (constructors[i],), permission = self.addContentPermissions[className]) def finalizeInstallation(self): '''Performs some final installation steps.''' # Apply customization policy if any cp = self.ploneStuff['CustomizationPolicy'] if cp and hasattr(cp, 'register'): cp.register(context) def install(self): self.logger.info('is being installed...') self.installApplication() self.installTool() self.installTypes() self.finalizeInstallation() # ------------------------------------------------------------------------------