diff --git a/bin/generate.py b/bin/generate.py index a5e126a..b45bedd 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -80,8 +80,9 @@ class GeneratorScript: if options.debian: app = args[0] appDir = os.path.dirname(app) + appName = os.path.basename(app) # Get the app version from zope/version.txt - f = file(os.path.join(app, 'zope', 'version.txt')) + f = file(os.path.join(app, 'zope', appName, 'version.txt')) version = f.read() f.close() version = version[:version.find('build')-1] diff --git a/bin/new.py b/bin/new.py index 545be2b..8860628 100644 --- a/bin/new.py +++ b/bin/new.py @@ -6,6 +6,7 @@ import os, os.path, sys, shutil, re from optparse import OptionParser from appy.shared.utils import cleanFolder, copyFolder +from appy.shared.packaging import ooStart, zopeConf # ------------------------------------------------------------------------------ class NewError(Exception): pass @@ -34,7 +35,7 @@ exec "$ZDCTL" -C "$CONFIG_FILE" "$@" ''' # runzope template file for a pure Zope instance ------------------------------- -runZope = '''#! /bin/sh +runZope = '''#!/bin/sh INSTANCE_HOME="%s" CONFIG_FILE="$INSTANCE_HOME/etc/zope.conf" ZOPE_RUN="/usr/lib/zope2.12/bin/runzope" @@ -42,46 +43,6 @@ export INSTANCE_HOME exec "$ZOPE_RUN" -C "$CONFIG_FILE" "$@" ''' -# zope.conf template file for a pure Zope instance ----------------------------- -zopeConf = '''# Zope configuration. -%%define INSTANCE %s -%%define HTTPPORT 8080 -%%define ZOPE_USER zope - -instancehome $INSTANCE -effective-user $ZOPE_USER - - level info - - path $INSTANCE/log/event.log - level info - - - - level WARN - - path $INSTANCE/log/Z2.log - format %%(message)s - - - - address $HTTPPORT - - - - path $INSTANCE/var/Data.fs - - mount-point / - - - - name temporary storage for sessioning - - mount-point /temp_folder - container-class Products.TemporaryFolder.TemporaryContainer - -''' - # zopectl template for a Plone (4) Zope instance ------------------------------- zopeCtlPlone = '''#!/bin/sh PYTHON="%s" @@ -153,14 +114,14 @@ class ZopeInstanceCreator: os.chmod('bin/runzope', 0744) # Make it executable by owner. # Create bin/startoo f = file('bin/startoo', 'w') - f.write('#!/bin/sh\nsoffice -invisible -headless -nofirststartwizard '\ - '"-accept=socket,host=localhost,port=2002;urp;"&\n') + f.write(ooStart) f.close() os.chmod('bin/startoo', 0744) # Make it executable by owner. # Create etc/zope.conf os.mkdir('etc') f = file('etc/zope.conf', 'w') - f.write(zopeConf % self.instancePath) + f.write(zopeConf % (self.instancePath, '%s/var' % self.instancePath, + '%s/log' % self.instancePath, '')) f.close() # Create other folders for name in ('Extensions', 'log', 'Products', 'var'): os.mkdir(name) diff --git a/bin/publish.py b/bin/publish.py index ffb74d1..7f86ed5 100644 --- a/bin/publish.py +++ b/bin/publish.py @@ -424,7 +424,7 @@ class Publisher: f.write(toc) f.close() - privateScripts = ('publish.py', 'zip.py', 'runOpenOffice.sh') + privateScripts = ('publish.py', 'zip.py', 'startoo.sh') def prepareGenFolder(self, minimalist=False): '''Creates the basic structure of the temp folder where the appy website will be generated.''' diff --git a/bin/runOpenOffice.sh b/bin/startoo.sh similarity index 100% rename from bin/runOpenOffice.sh rename to bin/startoo.sh diff --git a/bin/zopectl.py b/bin/zopectl.py new file mode 100644 index 0000000..e79c13c --- /dev/null +++ b/bin/zopectl.py @@ -0,0 +1,23 @@ +# ------------------------------------------------------------------------------ +import sys, os, os.path +import Zope2.Startup.zopectl as zctl + +# ------------------------------------------------------------------------------ +class ZopeRunner: + '''This class allows to run a Appy/Zope instance.''' + + def run(self): + # Check that an arg has been given (start, stop, fg, run) + if not sys.argv[3].strip(): + print 'Argument required.' + sys.exit(-1) + # Identify the name of the application for which Zope must run. + app = os.path.splitext(os.path.basename(sys.argv[2]))[0].lower() + # Launch Zope. + options = zctl.ZopeCtlOptions() + options.realize(None) + options.program = ['/usr/bin/%srun' % app] + c = zctl.ZopeCmd(options) + c.onecmd(" ".join(options.args)) + return min(c._exitstatus, 1) +# ------------------------------------------------------------------------------ diff --git a/gen/generator.py b/gen/generator.py index 927d78f..970df46 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -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__) diff --git a/gen/installer.py b/gen/installer.py index ca74531..ef43db8 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -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 # ------------------------------------------------------------------------------ diff --git a/gen/migrator.py b/gen/migrator.py index 29d8386..2089b01 100644 --- a/gen/migrator.py +++ b/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) # ------------------------------------------------------------------------------ diff --git a/gen/model.py b/gen/model.py index 0fafbc6..50d136f 100644 --- a/gen/model.py +++ b/gen/model.py @@ -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', diff --git a/gen/ui/widgets/string.pt b/gen/ui/widgets/string.pt index f308afb..5107f00 100644 --- a/gen/ui/widgets/string.pt +++ b/gen/ui/widgets/string.pt @@ -100,7 +100,7 @@ The list of values