appy.shared: improved deployment of a Appy app (creation of a Zope instance is no more required; corresponding folders are created in standard unix locations: /etc for the config file, /var/log for logs, /var/lib for the database, /usr/bin for scripts that start and stop the instance). appy.gen: first draft of a migration script that allows to migrate data from Plone-dependent Appy apps (<= 0.7.1) to Ploneless Appy 0.8.0.
This commit is contained in:
parent
95a899f3de
commit
1275df5753
13 changed files with 351 additions and 161 deletions
|
@ -120,7 +120,8 @@ class Generator:
|
|||
# Determine application name
|
||||
self.applicationName = os.path.basename(application)
|
||||
# Determine output folder (where to store the generated product)
|
||||
self.outputFolder = os.path.join(application, 'zope')
|
||||
self.outputFolder = os.path.join(application, 'zope',
|
||||
self.applicationName)
|
||||
self.options = options
|
||||
# Determine templates folder
|
||||
genFolder = os.path.dirname(__file__)
|
||||
|
|
|
@ -8,6 +8,7 @@ import appy.version
|
|||
import appy.gen as gen
|
||||
from appy.gen.po import PoParser
|
||||
from appy.gen.utils import updateRolesForPermission, createObject
|
||||
from appy.gen.migrator import Migrator
|
||||
from appy.shared.data import languages
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -87,14 +88,16 @@ class ZopeInstaller:
|
|||
|
||||
def installUi(self):
|
||||
'''Installs the user interface.'''
|
||||
# Delete the existing folder if it existed.
|
||||
zopeContent = self.app.objectIds()
|
||||
if 'ui' in zopeContent: self.app.manage_delObjects(['ui'])
|
||||
self.app.manage_addFolder('ui')
|
||||
# Some useful imports
|
||||
from OFS.Folder import manage_addFolder
|
||||
from OFS.Image import manage_addImage, manage_addFile
|
||||
from Products.PythonScripts.PythonScript import PythonScript
|
||||
from Products.PageTemplates.ZopePageTemplate import \
|
||||
manage_addPageTemplate
|
||||
# Delete the existing folder if it existed.
|
||||
zopeContent = self.app.objectIds()
|
||||
if 'ui' in zopeContent: self.app.manage_delObjects(['ui'])
|
||||
manage_addFolder(self.app, 'ui')
|
||||
# Browse the physical folder and re-create it in the Zope folder
|
||||
j = os.path.join
|
||||
ui = j(j(appy.getPath(), 'gen'), 'ui')
|
||||
|
@ -106,13 +109,13 @@ class ZopeInstaller:
|
|||
for name in folderName.strip(os.sep).split(os.sep):
|
||||
zopeFolder = zopeFolder._getOb(name)
|
||||
# Create sub-folders at this level
|
||||
for name in dirs: zopeFolder.manage_addFolder(name)
|
||||
for name in dirs: manage_addFolder(zopeFolder, name)
|
||||
# Create files at this level
|
||||
for name in files:
|
||||
baseName, ext = os.path.splitext(name)
|
||||
f = file(j(root, name))
|
||||
if ext in gen.File.imageExts:
|
||||
zopeFolder.manage_addImage(name, f)
|
||||
manage_addImage(zopeFolder, name, f)
|
||||
elif ext == '.pt':
|
||||
manage_addPageTemplate(zopeFolder, baseName, '', f.read())
|
||||
elif ext == '.py':
|
||||
|
@ -120,7 +123,7 @@ class ZopeInstaller:
|
|||
zopeFolder._setObject(baseName, obj)
|
||||
zopeFolder._getOb(baseName).write(f.read())
|
||||
else:
|
||||
zopeFolder.manage_addFile(name, f)
|
||||
manage_addFile(zopeFolder, name, f)
|
||||
f.close()
|
||||
# Update the home page
|
||||
if 'index_html' in zopeContent:
|
||||
|
@ -199,9 +202,10 @@ class ZopeInstaller:
|
|||
'''Creates the tool and the root data folder if they do not exist.'''
|
||||
# Create or update the base folder for storing data
|
||||
zopeContent = self.app.objectIds()
|
||||
from OFS.Folder import manage_addFolder
|
||||
|
||||
if 'data' not in zopeContent:
|
||||
self.app.manage_addFolder('data')
|
||||
manage_addFolder(self.app, 'data')
|
||||
data = self.app.data
|
||||
# Manager has been granted Add permissions for all root classes.
|
||||
# This may not be desired, so remove this.
|
||||
|
@ -240,7 +244,13 @@ class ZopeInstaller:
|
|||
appyTool.log('Appy version is "%s".' % appy.version.short)
|
||||
|
||||
# Create the admin user if no user exists.
|
||||
if not self.app.acl_users.getUsers():
|
||||
try:
|
||||
users = self.app.acl_users.getUsers()
|
||||
except:
|
||||
# When Plone has installed PAS in acl_users this may fail. Plone
|
||||
# may still be in the way for migration purposes.
|
||||
users = ('admin') # We suppose there is at least a user.
|
||||
if not users:
|
||||
self.app.acl_users._doAddUser('admin', 'admin', ['Manager'], ())
|
||||
appyTool.log('Admin user "admin" created.')
|
||||
|
||||
|
@ -386,7 +396,7 @@ class ZopeInstaller:
|
|||
from OFS.Application import install_product
|
||||
import Products
|
||||
install_product(self.app, Products.__path__[1], 'ZCTextIndex', [], {})
|
||||
|
||||
|
||||
def install(self):
|
||||
self.logger.info('is being installed...')
|
||||
self.installDependencies()
|
||||
|
@ -399,6 +409,10 @@ class ZopeInstaller:
|
|||
self.installCatalog()
|
||||
self.installTool()
|
||||
self.installUi()
|
||||
# Perform migrations if required
|
||||
Migrator(self).run()
|
||||
# Update Appy version in the database
|
||||
self.app.config.appy().appyVersion = appy.version.short
|
||||
# Empty the fake REQUEST object, only used at Zope startup.
|
||||
del self.app.config.getProductConfig().fakeRequest.wrappers
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
165
gen/migrator.py
165
gen/migrator.py
|
@ -7,56 +7,125 @@ class Migrator:
|
|||
installation, we've detected a new Appy version.'''
|
||||
def __init__(self, installer):
|
||||
self.installer = installer
|
||||
self.logger = installer.logger
|
||||
self.app = installer.app
|
||||
|
||||
def migrateTo_0_7_1(self):
|
||||
'''Appy 0.7.1 has its own management of Ref fields. So we must
|
||||
update data structures that store Ref info on instances.'''
|
||||
ins = self.installer
|
||||
ins.info('Migrating to Appy 0.7.1...')
|
||||
allClassNames = [ins.tool.__class__.__name__] + ins.config.allClassNames
|
||||
for className in allClassNames:
|
||||
i = -1
|
||||
updated = 0
|
||||
ins.info('Analysing class "%s"...' % className)
|
||||
refFields = None
|
||||
for obj in ins.tool.executeQuery(className,\
|
||||
noSecurity=True)['objects']:
|
||||
i += 1
|
||||
if i == 0:
|
||||
# Get the Ref fields for objects of this class
|
||||
refFields = [f for f in obj.getAllAppyTypes() \
|
||||
if (f.type == 'Ref') and not f.isBack]
|
||||
if refFields:
|
||||
refNames = ', '.join([rf.name for rf in refFields])
|
||||
ins.info(' Ref fields found: %s' % refNames)
|
||||
bypassRoles = ('Authenticated', 'Member')
|
||||
bypassGroups = ('Administrators', 'Reviewers')
|
||||
def migrateUsers(self, ploneSite):
|
||||
'''Migrate users from Plone's acl_users to Zope acl_users with
|
||||
corresponding Appy objects.'''
|
||||
# First of all, remove the Plone-patched root acl_users by a standard
|
||||
# (hum, Appy-patched) Zope UserFolder.
|
||||
tool = self.app.config.appy()
|
||||
from AccessControl.User import manage_addUserFolder
|
||||
self.app.manage_delObjects(ids=['acl_users'])
|
||||
manage_addUserFolder(self.app)
|
||||
# Put an admin user into it
|
||||
newUsersDb = self.app.acl_users
|
||||
newUsersDb._doAddUser('admin', 'admin', ['Manager'], ())
|
||||
# Copy users from Plone acl_users to Zope acl_users
|
||||
for user in ploneSite.acl_users.getUsers():
|
||||
id = user.getId()
|
||||
userRoles = user.getRoles()
|
||||
for br in self.bypassRoles:
|
||||
if br in userRoles: userRoles.remove(br)
|
||||
userInfo = ploneSite.portal_membership.getMemberById(id)
|
||||
userName = userInfo.getProperty('fullname') or id
|
||||
userEmail = userInfo.getProperty('email') or ''
|
||||
appyUser = tool.create('users', login=id,
|
||||
password1='fake', password2='fake', roles=userRoles,
|
||||
name=userName, firstName=' ', email=userEmail)
|
||||
appyUser.title = appyUser.title.strip()
|
||||
# Set the correct password
|
||||
password = ploneSite.acl_users.source_users._user_passwords[id]
|
||||
newUsersDb.data[id].__ = password
|
||||
# Manage groups. Exclude not-used default Plone groups.
|
||||
for groupId in user.getGroups():
|
||||
if groupId in self.bypassGroups: continue
|
||||
if tool.count('Group', login=groupId):
|
||||
# The Appy group already exists, get it
|
||||
appyGroup = tool.search('Group', login=groupId)[0]
|
||||
else:
|
||||
# Create the group. Todo: get Plone group roles and title
|
||||
appyGroup = tool.create('groups', login=groupId,
|
||||
title=groupId)
|
||||
appyGroup.addUser(appyUser)
|
||||
|
||||
def reindexObject(self, obj):
|
||||
obj.reindex()
|
||||
i = 1
|
||||
for subObj in obj.objectValues():
|
||||
i += self.reindexObject(subObj)
|
||||
return i # The number of reindexed (sub-)object(s)
|
||||
|
||||
def migrateTo_0_8_0(self):
|
||||
'''Migrates a Plone-based (<= 0.7.1) Appy app to a Ploneless (0.8.0)
|
||||
Appy app.'''
|
||||
self.logger.info('Migrating to Appy 0.8.0...')
|
||||
# Find the Plone site. It must be at the root of the Zope tree.
|
||||
ploneSite = None
|
||||
for obj in self.app.objectValues():
|
||||
if obj.__class__.__name__ == 'PloneSite':
|
||||
ploneSite = obj
|
||||
break
|
||||
# As a preamble: delete translation objects from self.app.config: they
|
||||
# will be copied from the old tool.
|
||||
self.app.config.manage_delObjects(ids=self.app.config.objectIds())
|
||||
# Migrate data objects:
|
||||
# - from oldDataFolder to self.app.data
|
||||
# - from oldTool to self.app.config (excepted translation
|
||||
# objects that were re-created from i18n files).
|
||||
appName = self.app.config.getAppName()
|
||||
for oldFolderName in (appName, 'portal_%s' % appName.lower()):
|
||||
oldFolder = getattr(ploneSite, oldFolderName)
|
||||
objectIds = [id for id in oldFolder.objectIds()]
|
||||
cutted = oldFolder.manage_cutObjects(ids=objectIds)
|
||||
if oldFolderName == appName:
|
||||
destFolder = self.app.data
|
||||
else:
|
||||
destFolder = self.app.config
|
||||
destFolder.manage_pasteObjects(cutted)
|
||||
i = 0
|
||||
for obj in destFolder.objectValues():
|
||||
i += self.reindexObject(obj)
|
||||
self.logger.info('%d objects imported into %s.' % \
|
||||
(i, destFolder.getId()))
|
||||
if oldFolderName != appName:
|
||||
# Re-link objects copied into the self.app.config with the Tool
|
||||
# through Ref fields.
|
||||
tool = self.app.config.appy()
|
||||
pList = tool.o.getProductConfig().PersistentList
|
||||
for field in tool.fields:
|
||||
if field.type != 'Ref': continue
|
||||
n = field.name
|
||||
if n in ('users', 'groups'): continue
|
||||
uids = getattr(oldFolder, n)
|
||||
if uids:
|
||||
# Update the forward reference
|
||||
setattr(tool.o, n, pList(uids))
|
||||
# Update the back reference
|
||||
for obj in getattr(tool, n):
|
||||
backList = getattr(obj.o, field.back.name)
|
||||
backList.remove(oldFolder._at_uid)
|
||||
backList.append(tool.uid)
|
||||
self.logger.info('config.%s: linked %d object(s)' % \
|
||||
(n, len(uids)))
|
||||
else:
|
||||
ins.info(' No Ref field found.')
|
||||
break
|
||||
isUpdated = False
|
||||
for field in refFields:
|
||||
# Attr for storing UIDs of referred objects has moved
|
||||
# from _appy_[fieldName] to [fieldName].
|
||||
refs = getattr(obj, '_appy_%s' % field.name)
|
||||
if refs:
|
||||
isUpdated = True
|
||||
setattr(obj, field.name, refs)
|
||||
exec 'del obj._appy_%s' % field.name
|
||||
# Set the back references
|
||||
for refObject in field.getValue(obj):
|
||||
refObject.link(field.back.name, obj, back=True)
|
||||
if isUpdated: updated += 1
|
||||
if updated:
|
||||
ins.info(' %d/%d object(s) updated.' % (updated, i+1))
|
||||
self.logger.info('config.%s: no object to link.' % n)
|
||||
self.migrateUsers(ploneSite)
|
||||
self.logger.info('Migration done.')
|
||||
|
||||
def run(self):
|
||||
i = self.installer
|
||||
installedVersion = i.appyTool.appyVersion
|
||||
startTime = time.time()
|
||||
migrationRequired = False
|
||||
if not installedVersion or (installedVersion <= '0.7.0'):
|
||||
migrationRequired = True
|
||||
self.migrateTo_0_7_1()
|
||||
stopTime = time.time()
|
||||
if migrationRequired:
|
||||
i.info('Migration done in %d minute(s).'% ((stopTime-startTime)/60))
|
||||
if self.app.acl_users.__class__.__name__ == 'UserFolder':
|
||||
return # Already Ploneless
|
||||
tool = self.app.config.appy()
|
||||
appyVersion = tool.appyVersion
|
||||
if not appyVersion or (appyVersion < '0.8.0'):
|
||||
# Migration is required.
|
||||
startTime = time.time()
|
||||
self.migrateTo_0_8_0()
|
||||
stopTime = time.time()
|
||||
elapsed = (stopTime-startTime) / 60.0
|
||||
self.logger.info('Migration done in %d minute(s).' % elapsed)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -129,7 +129,7 @@ class ModelClass:
|
|||
class User(ModelClass):
|
||||
# In a ModelClass we need to declare attributes in the following list.
|
||||
_appy_attributes = ['title', 'name', 'firstName', 'login', 'password1',
|
||||
'password2', 'roles']
|
||||
'password2', 'email', 'roles']
|
||||
# All methods defined below are fake. Real versions are in the wrapper.
|
||||
title = gen.String(show=False, indexed=True)
|
||||
gm = {'group': 'main', 'multiplicity': (1,1), 'width': 25}
|
||||
|
@ -144,6 +144,7 @@ class User(ModelClass):
|
|||
password1 = gen.String(format=gen.String.PASSWORD, show=showPassword,
|
||||
validator=validatePassword, **gm)
|
||||
password2 = gen.String(format=gen.String.PASSWORD, show=showPassword, **gm)
|
||||
email = gen.String(group='main', width=25)
|
||||
gm['multiplicity'] = (0, None)
|
||||
roles = gen.String(validator=gen.Selection('getGrantableRoles'),
|
||||
indexed=True, **gm)
|
||||
|
@ -177,7 +178,7 @@ class Translation(ModelClass):
|
|||
def show(self, name): pass
|
||||
|
||||
# The Tool class ---------------------------------------------------------------
|
||||
# Here are the prefixes of the fields generated on the Tool.
|
||||
# Prefixes of the fields generated on the Tool.
|
||||
toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
|
||||
'enableAdvancedSearch', 'numberOfSearchColumns',
|
||||
'searchFields', 'optionalFields', 'showWorkflow',
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
</tal:operator>
|
||||
<tal:comment replace="nothing">The list of values</tal:comment>
|
||||
<select tal:attributes="name widgetName; size widget/height" multiple="multiple">
|
||||
<option tal:repeat="v python:tool.getPossibleValues(name, withTranslations=True, withBlankValue=False, className=contentType)"
|
||||
<option tal:repeat="v python:tool.getPossibleValues(name, withTranslations=True, withBlankValue=False, className=className)"
|
||||
tal:attributes="value python:v[0]; title python: v[1]"
|
||||
tal:content="python: tool.truncateValue(v[1], widget)">
|
||||
</option>
|
||||
|
|
|
@ -110,6 +110,7 @@ class ZopeUserPatches:
|
|||
def getRolesInContext(self, object):
|
||||
'''Return the list of global and local (to p_object) roles granted to
|
||||
this user (or to any of its groups).'''
|
||||
if isinstance(object, AbstractWrapper): object = object.o
|
||||
object = getattr(object, 'aq_inner', object)
|
||||
# Start with user global roles
|
||||
res = self.getRoles()
|
||||
|
@ -120,7 +121,8 @@ class ZopeUserPatches:
|
|||
groups = getattr(self, 'groups', ())
|
||||
for id, roles in localRoles.iteritems():
|
||||
if (id != userId) and (id not in groups): continue
|
||||
for role in roles: res.add(role)
|
||||
for role in roles:
|
||||
if role not in res: res.append(role)
|
||||
return res
|
||||
|
||||
def allowed(self, object, object_roles=None):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue