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
	
	 Gaetan Delannay
						Gaetan Delannay