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