appypod-rattail/gen/plone25/installer.py

510 lines
24 KiB
Python

'''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
import appy
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)': 'skyn/view', 'edit': 'skyn/edit',
'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. Creates also the 'appy' folder (more precisely,
a Filesystem Directory View) at the root of the site, for storing
appy-wide ZPTs an images.'''
# 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
# Allow to create Appy large folders in the plone site
getattr(site.portal_types,
'Plone Site').allowed_content_types += (self.appyFolderType,)
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)
# Creates the "appy" Directory view
if not hasattr(site.aq_base, 'skyn'):
addDirView = self.ploneStuff['manage_addDirectoryView']
addDirView(site, appy.getPath() + '/gen/plone25/skin',id='skyn')
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 = 'skyn/view'
action.edit(action='string:${object_url}/%s' % page)
elif action.id == 'edit':
page = 'skyn/edit'
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})
self.tool = getattr(self.ploneSite, self.toolInstanceName)
self.appyTool = self.tool._appy_getWrapper(force=True)
if self.reinstall:
self.tool.createOrUpdate(False)
else:
self.tool.createOrUpdate(True)
if not self.appyTool.flavours:
# Create the default flavour
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.'''
register = self.ploneStuff['DirectoryView'].registerDirectory
register('skins', self.ploneStuff['product_globals'])
# Register the appy skin folder among DirectoryView'able folders
register('skin', appy.getPath() + '/gen/plone25')
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()
# ------------------------------------------------------------------------------