[Checkins] SVN: GenericSetup/trunk/ merge -r73217:76823 from bbq
sprint branch
Rob Miller
ra at burningman.com
Wed Jun 20 15:24:39 EDT 2007
Log message for revision 76859:
merge -r73217:76823 from bbq sprint branch
* improved setup tool interface
* ported CPS's upgrade infrastructure, adding per-profile versions and
upgrades, and nested upgradeStep directives
* added support for metadata.xml
more detail available in CHANGES.txt
Changed:
U GenericSetup/trunk/CHANGES.txt
A GenericSetup/trunk/bbq_sprint-TODO.txt
A GenericSetup/trunk/doc/configurators.txt
U GenericSetup/trunk/interfaces.py
U GenericSetup/trunk/meta.zcml
A GenericSetup/trunk/metadata.py
U GenericSetup/trunk/registry.py
A GenericSetup/trunk/tests/test_profile_metadata.py
U GenericSetup/trunk/tests/test_tool.py
U GenericSetup/trunk/tests/test_zcml.py
A GenericSetup/trunk/tests/versioned_profile/
U GenericSetup/trunk/tool.py
A GenericSetup/trunk/upgrade.py
U GenericSetup/trunk/utils.py
A GenericSetup/trunk/www/setup_upgrades.zpt
U GenericSetup/trunk/www/sutImportSteps.zpt
U GenericSetup/trunk/www/sutProperties.zpt
A GenericSetup/trunk/www/upgradeStep.zpt
U GenericSetup/trunk/zcml.py
-=-
Modified: GenericSetup/trunk/CHANGES.txt
===================================================================
--- GenericSetup/trunk/CHANGES.txt 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/CHANGES.txt 2007-06-20 19:24:38 UTC (rev 76859)
@@ -13,6 +13,36 @@
- Made sure we register Acquisition free objects as utilities in the
components handler.
+ - Profiles now support version numbers; setup tool tracks profile
+ versions during upgrades.
+
+ - Added support for nested 'upgradeStep' directives; expanded upgrade
+ step registry into a real registry object and not just a dictionary.
+
+ - Added support for 'metadata.xml' in the profile (read during
+ profile registration) to register profile description, version,
+ and dependencies.
+
+ - Deprecated runImportStep and runAllImportSteps in favor of
+ runImportStepFromProfile and runAllImportStepsFromProfile.
+
+ - Merged CPS's upgradeStep ZCML directive, w/ corresponding tool support.
+
+ - Added a "last imported" date to the list of extension profiles,
+ and to the baseline profile.
+
+ - Renamed the "Properties" tab to "Profiles".
+
+ - Removed the 'create_report' decoy in the ZMI view methods: there was
+ never any UI for passing any value other than the default, anyway, and
+ the report objects are too useful to omit.
+
+ - Refactored the "Properties" tab to separate baseline profiles from
+ extension profiles, marking the option to reset the baseline as
+ potentially dangerous for sites which already have one. Allow
+ importing one or more extension profiles directly (all steps) from the
+ "Properties" tab.
+
- No longer read the toolset xml and update the toolset regustry on
import context change. Doing this only during the toolset step import
should be sufficient.
Copied: GenericSetup/trunk/bbq_sprint-TODO.txt (from rev 76823, GenericSetup/branches/tseaver-bbq_sprint/bbq_sprint-TODO.txt)
===================================================================
--- GenericSetup/trunk/bbq_sprint-TODO.txt (rev 0)
+++ GenericSetup/trunk/bbq_sprint-TODO.txt 2007-06-20 19:24:38 UTC (rev 76859)
@@ -0,0 +1,26 @@
+GenericSetup TODOs
+==================
+
+ - [X] Make it harder to overwrite the base profile on the tool.
+
+ - [X] Track import history of profiles.
+
+ - [X] Show the applied and available extension profiles.
+
+ - [X] Limit the displayed profiles to those registered for the tool's
+ container's interface(s).
+
+ - [X] Add support for extra profile metadata:
+
+ - Description
+
+ - Version ID
+
+ - Dependencies
+
+ - [X] Support upgrade steps on a per-profile basis.
+
+ - [ ] Improve profile upgrade version tracking.
+
+ - [ ] Provide default upgrade step implementations which will run
+ some or all import steps for the associated profile.
Copied: GenericSetup/trunk/doc/configurators.txt (from rev 76823, GenericSetup/branches/tseaver-bbq_sprint/doc/configurators.txt)
===================================================================
--- GenericSetup/trunk/doc/configurators.txt (rev 0)
+++ GenericSetup/trunk/doc/configurators.txt 2007-06-20 19:24:38 UTC (rev 76859)
@@ -0,0 +1,47 @@
+The Products.GenericSetup.utils.ImportConfiguratorBase class provides
+a convenient shortcut for defining how an XML file should be parsed
+and converted to a python dictionary. To use this, create a subclass
+of ImportConfiguratorBase and implement a _getImportMapping method
+which returns a dictionary. The returned dictionary should adhere to
+the following guidelines:
+
+- The utils module provides CONVERTER, DEFAULT, and KEY constants that
+ are to be used in the import mapping to define certain behaviours.
+
+- Any possible tag that you want your XML file format to support must
+ be listed as a top-level key in the import mapping dictionary.
+
+- The value of any key in the import mapping dictionary should be
+ another dictionary.
+
+- If an outer key represents a possible tag, then the nested
+ dictionary represents the possible "properties" of that tag, where a
+ property might be an attribute of the tag, the text content of the
+ tag, or possibly a nested tag.
+
+- If a key of the nested dictionary represents a nested tag, you will
+ also need a top-level key to represent that tag. The nested
+ representation of the tag is where you make statements about how the
+ tag itself should be represented, while the top-level key allows you
+ to express information about how the data that is further nested
+ within the nested tag should be represented.
+
+- A CONVERTER can be registered on a an element to change the way that
+ the element is represented in the generated data structure.
+ self._convertToUnique will cause the element to be represented as a
+ single item and not a tuple of items, for instance.
+
+- A KEY can be specified for an element. If it is specified then the
+ element's value will be stored in the resulting python dictionary
+ with the KEY value as the key. If KEY is None, then the value will
+ be stored in a tuple rather than as a value in a nested dictionary.
+ If KEY is omitted, then the name of the element will be used as the
+ key.
+
+- A DEFAULT value can be specified on an element, which will be used
+ as the value on that element if the element doesn't actually exist
+ in the XML file.
+
+- Reference examples for this syntax can be found in the
+ metadata.ProfileMetadata and the rolemap.RolemapImportConfigurator
+ classes.
Modified: GenericSetup/trunk/interfaces.py
===================================================================
--- GenericSetup/trunk/interfaces.py 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/interfaces.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -451,19 +451,33 @@
def getImportContextID():
""" Get the ID of the active import context.
+
+ DEPRECATED. The idea of a stateful active import context is
+ going away.
"""
- def applyContext( context, encoding=None ):
-
- """ Update the tool from the supplied context, without modifying its
- "permanent" ID.
+ def getBaselineContextID():
+ """ Get the ID of the base profile for this configuration.
"""
def setImportContext( context_id ):
""" Set the ID of the active import context and update the registries.
+
+ DEPRECATED. The idea of a stateful active import context is
+ going away.
"""
+ def setBaselineContext( context_id, encoding=None):
+ """ Specify the base profile for this configuration.
+ """
+
+ def applyContext( context, encoding=None ):
+
+ """ Update the tool from the supplied context, without modifying its
+ "permanent" ID.
+ """
+
def getImportStepRegistry():
""" Return the IImportStepRegistry for the tool.
@@ -479,10 +493,14 @@
""" Return the IToolsetRegistry for the tool.
"""
- def runImportStep( step_id, run_dependencies=True, purge_old=None ):
+ def runImportStepFromProfile(profile_id, step_id,
+ run_dependencies=True, purge_old=None):
- """ Execute a given setup step
+ """ Execute a given setup step from the given profile.
+ o 'profile_id' must be a valid ID of a registered profile;
+ otherwise, raise KeyError.
+
o 'step_id' is the ID of the step to run.
o If 'purge_old' is True, then run the step after purging any
@@ -500,10 +518,37 @@
step
"""
- def runAllImportSteps( purge_old=None ):
+ def runImportStep(step_id, run_dependencies=True, purge_old=None):
- """ Run all setup steps in dependency order.
+ """ Execute a given setup step from the current
+ _import_context_id context.
+ o 'step_id' is the ID of the step to run.
+
+ o If 'purge_old' is True, then run the step after purging any
+ "old" setup first (this is the responsibility of the step,
+ which must check the context we supply).
+
+ o If 'run_dependencies' is True, then run any out-of-date
+ dependency steps first.
+
+ o Return a mapping, with keys:
+
+ 'steps' -- a sequence of IDs of the steps run.
+
+ 'messages' -- a dictionary holding messages returned from each
+ step
+
+ DEPRECATED. Use runImportStepFromProfile instead.
+ """
+
+ def runAllImportStepsFromProfile(profile_id, purge_old=None):
+
+ """ Run all setup steps for the given profile in dependency order.
+
+ o 'profile_id' must be a valid ID of a registered profile;
+ otherwise, raise KeyError.
+
o If 'purge_old' is True, then run each step after purging any
"old" setup first (this is the responsibility of the step,
which must check the context we supply).
@@ -516,6 +561,25 @@
step
"""
+ def runAllImportSteps(purge_old=None):
+
+ """ Run all setup steps for the _import_context_id profile in
+ dependency order.
+
+ o If 'purge_old' is True, then run each step after purging any
+ "old" setup first (this is the responsibility of the step,
+ which must check the context we supply).
+
+ o Return a mapping, with keys:
+
+ 'steps' -- a sequence of IDs of the steps run.
+
+ 'messages' -- a dictionary holding messages returned from each
+ step
+
+ DEPRECATED. Use runAllImportStepsFromProfile instead.
+ """
+
def runExportStep( step_id ):
""" Generate a tarball containing artifacts from one export step.
@@ -569,7 +633,13 @@
(c.f: 'diff -wbB')
"""
+ def getProfileImportDate(profile_id):
+ """ Return the last date an extension was imported.
+ o The result will be a string, formated as IS0.
+ """
+
+
class IWriteLogger(Interface):
"""Write methods used by the python logging Logger.
Modified: GenericSetup/trunk/meta.zcml
===================================================================
--- GenericSetup/trunk/meta.zcml 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/meta.zcml 2007-06-20 19:24:38 UTC (rev 76859)
@@ -2,11 +2,34 @@
xmlns="http://namespaces.zope.org/zope"
xmlns:meta="http://namespaces.zope.org/meta">
- <meta:directive
- name="registerProfile"
- namespace="http://namespaces.zope.org/genericsetup"
- schema=".zcml.IRegisterProfileDirective"
- handler=".zcml.registerProfile"
- />
+ <meta:directives namespace="http://namespaces.zope.org/genericsetup">
+ <meta:directive
+ name="registerProfile"
+ schema=".zcml.IRegisterProfileDirective"
+ handler=".zcml.registerProfile"
+ />
+
+ <meta:directive
+ name="upgradeStep"
+ schema=".zcml.IUpgradeStepDirective"
+ handler=".zcml.upgradeStep"
+ />
+
+ <meta:complexDirective
+ name="upgradeSteps"
+ schema=".zcml.IUpgradeStepsDirective"
+ handler=".zcml.upgradeSteps"
+ >
+
+ <meta:subdirective
+ name="upgradeStep"
+ schema=".zcml.IUpgradeStepsStepSubDirective"
+ />
+
+ </meta:complexDirective>
+
+
+ </meta:directives>
+
</configure>
Copied: GenericSetup/trunk/metadata.py (from rev 76823, GenericSetup/branches/tseaver-bbq_sprint/metadata.py)
===================================================================
--- GenericSetup/trunk/metadata.py (rev 0)
+++ GenericSetup/trunk/metadata.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -0,0 +1,61 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+""" GenericSetup profile metadata
+
+$Id:$
+"""
+import os
+
+from utils import ImportConfiguratorBase
+from utils import CONVERTER, DEFAULT, KEY
+
+class ProfileMetadata( ImportConfiguratorBase ):
+ """ Extracts profile metadata from metadata.xml file.
+ """
+
+ def __init__( self, path, encoding=None ):
+
+ # don't call the base class __init__ b/c we don't have (or need)
+ # a site
+ self._path = path
+ self._encoding = encoding
+
+ def __call__( self ):
+
+ full_path = os.path.join( self._path, 'metadata.xml' )
+ if not os.path.exists( full_path ):
+ return {}
+
+ file = open( full_path, 'r' )
+ return self.parseXML( file.read() )
+
+ def _getImportMapping( self ):
+
+ return {
+ 'metadata':
+ {'description': { CONVERTER: self._convertToUnique },
+ 'version': { CONVERTER: self._convertToUnique },
+ 'dependencies': { CONVERTER: self._convertToUnique },
+ },
+ 'description':
+ { '#text': { KEY: None, DEFAULT: '' },
+ },
+ 'version':
+ { '#text': { KEY: None },
+ },
+ 'dependencies':
+ {'dependency': { KEY: None },},
+ 'dependency':
+ { '#text': { KEY: None },
+ },
+ }
Modified: GenericSetup/trunk/registry.py
===================================================================
--- GenericSetup/trunk/registry.py 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/registry.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -14,12 +14,14 @@
$Id$
"""
-
+import os
from xml.sax import parseString
from AccessControl import ClassSecurityInfo
from Acquisition import Implicit
from Globals import InitializeClass
+from App.FactoryDispatcher import ProductDispatcher
+import App.Product
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from zope.interface import implements
@@ -29,13 +31,13 @@
from interfaces import IToolsetRegistry
from interfaces import IProfileRegistry
from permissions import ManagePortal
+from metadata import ProfileMetadata
from utils import HandlerBase
from utils import _xmldir
from utils import _getDottedName
from utils import _resolveDottedName
from utils import _extractDocstring
-
class ImportStepRegistry( Implicit ):
""" Manage knowledge about steps to create / configure site.
@@ -550,7 +552,7 @@
self.clear()
- security.declareProtected( ManagePortal, '' )
+ security.declareProtected( ManagePortal, 'getProfileInfo' )
def getProfileInfo( self, profile_id, for_=None ):
""" See IProfileRegistry.
@@ -610,6 +612,32 @@
, 'for': for_
}
+ metadata = ProfileMetadata( path )()
+
+ version = metadata.get( 'version', None )
+ if version is None and product is not None:
+ prod_name = product.split('.')[-1]
+ prod_module = getattr(App.Product.Products, prod_name, None)
+ if prod_module is not None:
+ prod_path = prod_module.__path__[0]
+
+ # Retrieve version number from any suitable version.txt
+ for fname in ('version.txt', 'VERSION.txt', 'VERSION.TXT'):
+ try:
+ fpath = os.path.join( prod_path, fname )
+ fhandle = open( fpath, 'r' )
+ version = fhandle.read().strip()
+ fhandle.close()
+ break
+ except IOError:
+ continue
+
+ if version is not None:
+ metadata[ 'version' ] = version
+
+ # metadata.xml description trumps ZCML description... awkward
+ info.update( metadata )
+
self._profile_info[ profile_id ] = info
security.declarePrivate( 'clear' )
Copied: GenericSetup/trunk/tests/test_profile_metadata.py (from rev 76823, GenericSetup/branches/tseaver-bbq_sprint/tests/test_profile_metadata.py)
===================================================================
--- GenericSetup/trunk/tests/test_profile_metadata.py (rev 0)
+++ GenericSetup/trunk/tests/test_profile_metadata.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -0,0 +1,80 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+""" Unit tests for ProfileMetadata.
+
+$Id:$
+"""
+import unittest
+import os
+
+from Testing.ZopeTestCase import ZopeTestCase
+from Testing.ZopeTestCase import installProduct
+
+from Products.GenericSetup import profile_registry
+from Products.GenericSetup.metadata import ProfileMetadata
+
+desc = 'DESCRIPTION TEXT'
+version = 'VERSION'
+dep1 = 'DEPENDENCY 1'
+dep2 = 'DEPENDENCY 2'
+
+_METADATA_XML = """\
+<?xml version="1.0"?>
+<metadata>
+ <description>%s</description>
+ <version>%s</version>
+ <dependencies>
+ <dependency>%s</dependency>
+ <dependency>%s</dependency>
+ </dependencies>
+</metadata>
+""" % (desc, version, dep1, dep2)
+
+_METADATA_MAP = {
+ 'description': desc,
+ 'version': version,
+ 'dependencies': (dep1, dep2),
+ }
+
+class ProfileMetadataTests( ZopeTestCase ):
+
+ installProduct('GenericSetup')
+
+ def test_parseXML(self):
+ metadata = ProfileMetadata( '' )
+ parsed = metadata.parseXML( _METADATA_XML )
+ self.assertEqual(parsed, _METADATA_MAP)
+
+ def test_versionFromProduct(self):
+ profile_id = 'dummy_profile'
+ product_name = 'GenericSetup'
+ directory = os.path.split(__file__)[0]
+ path = os.path.join(directory, 'default_profile')
+ profile_registry.registerProfile( profile_id,
+ 'Dummy Profile',
+ 'This is a dummy profile',
+ path,
+ product=product_name)
+ profile_info = profile_registry.getProfileInfo('%s:%s' % (product_name,
+ profile_id))
+ product = getattr(self.app.Control_Panel.Products, product_name)
+ self.assertEqual(profile_info['version'], product.version)
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite( ProfileMetadataTests ),
+ ))
+
+if __name__ == '__main__':
+ from Products.GenericSetup.testing import run
+ run(test_suite())
Modified: GenericSetup/trunk/tests/test_tool.py
===================================================================
--- GenericSetup/trunk/tests/test_tool.py 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/tests/test_tool.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -14,7 +14,8 @@
$Id$
"""
-
+import copy
+import os
import unittest
import Testing
@@ -23,8 +24,15 @@
from Acquisition import aq_base
from OFS.Folder import Folder
+from Products.Five import zcml
+
+import Products.GenericSetup
from Products.GenericSetup import profile_registry
+from Products.GenericSetup.upgrade import listUpgradeSteps
+from Products.GenericSetup.upgrade import UpgradeStep
+from Products.GenericSetup.upgrade import _registerUpgradeStep
from Products.GenericSetup.testing import ExportImportZCMLLayer
+from Products.GenericSetup.tests.test_zcml import dummy_upgrade_handler
from common import BaseRegistryTests
from common import DummyExportContext
@@ -68,7 +76,7 @@
tool = self._makeOne('setup_tool')
- self.assertEqual( tool.getImportContextID(), '' )
+ self.assertEqual( tool.getBaselineContextID(), '' )
import_registry = tool.getImportStepRegistry()
self.assertEqual( len( import_registry.listSteps() ), 0 )
@@ -82,7 +90,7 @@
self.assertEqual( len( toolset_registry.listForbiddenTools() ), 0 )
self.assertEqual( len( toolset_registry.listRequiredTools() ), 0 )
- def test_getImportContextID( self ):
+ def test_getBaselineContextID( self ):
from Products.GenericSetup.tool import IMPORT_STEPS_XML
from Products.GenericSetup.tool import EXPORT_STEPS_XML
@@ -98,20 +106,20 @@
self._makeFile(TOOLSET_XML, _EMPTY_TOOLSET_XML)
profile_registry.registerProfile('foo', 'Foo', '', self._PROFILE_PATH)
- tool.setImportContext('profile-other:foo')
+ tool.setBaselineContext('profile-other:foo')
- self.assertEqual( tool.getImportContextID(), 'profile-other:foo' )
+ self.assertEqual( tool.getBaselineContextID(), 'profile-other:foo' )
- def test_setImportContext_invalid( self ):
+ def test_setBaselineContext_invalid( self ):
tool = self._makeOne('setup_tool')
self.assertRaises( KeyError
- , tool.setImportContext
+ , tool.setBaselineContext
, 'profile-foo'
)
- def test_setImportContext( self ):
+ def test_setBaselineContext( self ):
from Products.GenericSetup.tool import IMPORT_STEPS_XML
from Products.GenericSetup.tool import EXPORT_STEPS_XML
@@ -129,9 +137,9 @@
self._makeFile(TOOLSET_XML, _NORMAL_TOOLSET_XML)
profile_registry.registerProfile('foo', 'Foo', '', self._PROFILE_PATH)
- tool.setImportContext('profile-other:foo')
+ tool.setBaselineContext('profile-other:foo')
- self.assertEqual( tool.getImportContextID(), 'profile-other:foo' )
+ self.assertEqual( tool.getBaselineContextID(), 'profile-other:foo' )
import_registry = tool.getImportStepRegistry()
self.assertEqual( len( import_registry.listSteps() ), 1 )
@@ -164,7 +172,8 @@
tool = self._makeOne('setup_tool').__of__( site )
- self.assertRaises( ValueError, tool.runImportStep, 'nonesuch' )
+ self.assertRaises( ValueError, tool.runImportStepFromProfile,
+ '', 'nonesuch' )
def test_runImportStep_simple( self ):
@@ -176,7 +185,7 @@
registry = tool.getImportStepRegistry()
registry.registerStep( 'simple', '1', _uppercaseSiteTitle )
- result = tool.runImportStep( 'simple' )
+ result = tool.runImportStepFromProfile( '', 'simple' )
self.assertEqual( len( result[ 'steps' ] ), 1 )
@@ -198,7 +207,7 @@
registry.registerStep( 'dependent', '1'
, _uppercaseSiteTitle, ( 'dependable', ) )
- result = tool.runImportStep( 'dependent' )
+ result = tool.runImportStepFromProfile( '', 'dependent' )
self.assertEqual( len( result[ 'steps' ] ), 2 )
@@ -223,7 +232,8 @@
registry.registerStep( 'dependent', '1'
, _uppercaseSiteTitle, ( 'dependable', ) )
- result = tool.runImportStep( 'dependent', run_dependencies=False )
+ result = tool.runImportStepFromProfile( '', 'dependent',
+ run_dependencies=False )
self.assertEqual( len( result[ 'steps' ] ), 1 )
@@ -241,7 +251,7 @@
registry = tool.getImportStepRegistry()
registry.registerStep( 'purging', '1', _purgeIfRequired )
- result = tool.runImportStep( 'purging' )
+ result = tool.runImportStepFromProfile( '', 'purging' )
self.assertEqual( len( result[ 'steps' ] ), 1 )
self.assertEqual( result[ 'steps' ][ 0 ], 'purging' )
@@ -256,7 +266,8 @@
registry = tool.getImportStepRegistry()
registry.registerStep( 'purging', '1', _purgeIfRequired )
- result = tool.runImportStep( 'purging', purge_old=True )
+ result = tool.runImportStepFromProfile( '', 'purging',
+ purge_old=True )
self.assertEqual( len( result[ 'steps' ] ), 1 )
self.assertEqual( result[ 'steps' ][ 0 ], 'purging' )
@@ -271,7 +282,8 @@
registry = tool.getImportStepRegistry()
registry.registerStep( 'purging', '1', _purgeIfRequired )
- result = tool.runImportStep( 'purging', purge_old=False )
+ result = tool.runImportStepFromProfile( '', 'purging',
+ purge_old=False )
self.assertEqual( len( result[ 'steps' ] ), 1 )
self.assertEqual( result[ 'steps' ][ 0 ], 'purging' )
@@ -289,7 +301,8 @@
registry.registerStep( 'dependent', '1'
, _uppercaseSiteTitle, ( 'purging', ) )
- result = tool.runImportStep( 'dependent', purge_old=False )
+ result = tool.runImportStepFromProfile( '', 'dependent',
+ purge_old=False )
self.failIf( site.purged )
def test_runAllImportSteps_empty( self ):
@@ -297,13 +310,14 @@
site = self._makeSite()
tool = self._makeOne('setup_tool').__of__( site )
- result = tool.runAllImportSteps()
+ result = tool.runAllImportStepsFromProfile('')
self.assertEqual( len( result[ 'steps' ] ), 0 )
def test_runAllImportSteps_sorted_default_purge( self ):
TITLE = 'original title'
+ PROFILE_ID = 'testing'
site = self._makeSite( TITLE )
tool = self._makeOne('setup_tool').__of__( site )
@@ -315,7 +329,7 @@
registry.registerStep( 'purging', '1'
, _purgeIfRequired )
- result = tool.runAllImportSteps()
+ result = tool.runAllImportStepsFromProfile(PROFILE_ID)
self.assertEqual( len( result[ 'steps' ] ), 3 )
@@ -334,6 +348,31 @@
self.assertEqual( site.title, TITLE.replace( ' ', '_' ).upper() )
self.failUnless( site.purged )
+ prefix = 'import-all-%s' % PROFILE_ID
+ logged = [x for x in tool.objectIds('File') if x.startswith(prefix)]
+ self.assertEqual(len(logged), 1)
+
+ def test_runAllImportSteps_unicode_profile_id_creates_reports( self ):
+
+ TITLE = 'original title'
+ PROFILE_ID = u'testing'
+ site = self._makeSite( TITLE )
+ tool = self._makeOne('setup_tool').__of__( site )
+
+ registry = tool.getImportStepRegistry()
+ registry.registerStep( 'dependable', '1'
+ , _underscoreSiteTitle, ( 'purging', ) )
+ registry.registerStep( 'dependent', '1'
+ , _uppercaseSiteTitle, ( 'dependable', ) )
+ registry.registerStep( 'purging', '1'
+ , _purgeIfRequired )
+
+ tool.runAllImportStepsFromProfile(PROFILE_ID)
+
+ prefix = ('import-all-%s' % PROFILE_ID).encode('UTF-8')
+ logged = [x for x in tool.objectIds('File') if x.startswith(prefix)]
+ self.assertEqual(len(logged), 1)
+
def test_runAllImportSteps_sorted_explicit_purge( self ):
site = self._makeSite()
@@ -347,7 +386,7 @@
registry.registerStep( 'purging', '1'
, _purgeIfRequired )
- result = tool.runAllImportSteps( purge_old=True )
+ result = tool.runAllImportStepsFromProfile( '', purge_old=True )
self.assertEqual( len( result[ 'steps' ] ), 3 )
@@ -372,7 +411,7 @@
registry.registerStep( 'purging', '1'
, _purgeIfRequired )
- result = tool.runAllImportSteps( purge_old=False )
+ result = tool.runAllImportStepsFromProfile( '', purge_old=False )
self.assertEqual( len( result[ 'steps' ] ), 3 )
@@ -589,7 +628,128 @@
self.assertEqual( export_registry.getStep( 'one' ), ONE_FUNC )
+ def test_listContextInfos_empty(self):
+ site = self._makeSite()
+ site.setup_tool = self._makeOne('setup_tool')
+ tool = site.setup_tool
+ infos = tool.listContextInfos()
+ self.assertEqual(len(infos), 0)
+ def test_listContextInfos_with_snapshot(self):
+ site = self._makeSite()
+ site.setup_tool = self._makeOne('setup_tool')
+ tool = site.setup_tool
+ tool.createSnapshot('testing')
+ infos = tool.listContextInfos()
+ self.assertEqual(len(infos), 1)
+ info = infos[0]
+ self.assertEqual(info['id'], 'snapshot-testing')
+ self.assertEqual(info['title'], 'testing')
+ self.assertEqual(info['type'], 'snapshot')
+
+ def test_listContextInfos_with_registered_base_profile(self):
+ from Products.GenericSetup.interfaces import BASE
+ profile_registry.registerProfile('foo', 'Foo', '', self._PROFILE_PATH,
+ 'Foo', BASE)
+ site = self._makeSite()
+ site.setup_tool = self._makeOne('setup_tool')
+ tool = site.setup_tool
+ infos = tool.listContextInfos()
+ self.assertEqual(len(infos), 1)
+ info = infos[0]
+ self.assertEqual(info['id'], 'profile-Foo:foo')
+ self.assertEqual(info['title'], 'Foo')
+ self.assertEqual(info['type'], 'base')
+
+ def test_listContextInfos_with_registered_extension_profile(self):
+ from Products.GenericSetup.interfaces import EXTENSION
+ profile_registry.registerProfile('foo', 'Foo', '', self._PROFILE_PATH,
+ 'Foo', EXTENSION)
+ site = self._makeSite()
+ site.setup_tool = self._makeOne('setup_tool')
+ tool = site.setup_tool
+ infos = tool.listContextInfos()
+ self.assertEqual(len(infos), 1)
+ info = infos[0]
+ self.assertEqual(info['id'], 'profile-Foo:foo')
+ self.assertEqual(info['title'], 'Foo')
+ self.assertEqual(info['type'], 'extension')
+
+ def test_getProfileImportDate_nonesuch(self):
+ site = self._makeSite()
+ site.setup_tool = self._makeOne('setup_tool')
+ tool = site.setup_tool
+ self.assertEqual(tool.getProfileImportDate('nonesuch'), None)
+
+ def test_getProfileImportDate_simple_id(self):
+ from OFS.Image import File
+ site = self._makeSite()
+ site.setup_tool = self._makeOne('setup_tool')
+ tool = site.setup_tool
+ filename = 'import-all-foo-20070315123456.log'
+ tool._setObject(filename, File(filename, '', ''))
+ self.assertEqual(tool.getProfileImportDate('foo'),
+ '2007-03-15T12:34:56Z')
+
+ def test_getProfileImportDate_id_with_colon(self):
+ from OFS.Image import File
+ site = self._makeSite()
+ site.setup_tool = self._makeOne('setup_tool')
+ tool = site.setup_tool
+ filename = 'import-all-foo_bar-20070315123456.log'
+ tool._setObject(filename, File(filename, '', ''))
+ self.assertEqual(tool.getProfileImportDate('foo:bar'),
+ '2007-03-15T12:34:56Z')
+
+ def test_profileVersioning(self):
+ site = self._makeSite()
+ site.setup_tool = self._makeOne('setup_tool')
+ tool = site.setup_tool
+ profile_id = 'dummy_profile'
+ product_name = 'GenericSetup'
+ directory = os.path.split(__file__)[0]
+ path = os.path.join(directory, 'versioned_profile')
+
+ # register profile
+ orig_profile_reg = (profile_registry._profile_info.copy(),
+ profile_registry._profile_ids[:])
+ profile_registry.registerProfile(profile_id,
+ 'Dummy Profile',
+ 'This is a dummy profile',
+ path,
+ product=product_name)
+
+ # register upgrade step
+ from Products.GenericSetup.upgrade import _upgrade_registry
+ orig_upgrade_registry = copy.copy(_upgrade_registry._registry)
+ step = UpgradeStep("Upgrade", "GenericSetup:dummy_profile", '*', '1.1', '',
+ dummy_upgrade_handler,
+ None, "1")
+ _registerUpgradeStep(step)
+
+ # test initial states
+ profile_id = ':'.join((product_name, profile_id))
+ self.assertEqual(tool.getVersionForProfile(profile_id), '1.1')
+ self.assertEqual(tool.getLastVersionForProfile(profile_id),
+ 'unknown')
+
+ # run upgrade steps
+ request = site.REQUEST
+ request.form['profile_id'] = profile_id
+ steps = listUpgradeSteps(tool, profile_id, '1.0')
+ step_id = steps[0]['id']
+ request.form['upgrades'] = [step_id]
+ tool.manage_doUpgrades()
+ self.assertEqual(tool.getLastVersionForProfile(profile_id),
+ ('1', '1'))
+
+ # reset ugprade registry
+ _upgrade_registry._registry = orig_upgrade_registry
+
+ # reset profile registry
+ (profile_registry._profile_info,
+ profile_registry._profile_ids) = orig_profile_reg
+
_DEFAULT_STEP_REGISTRIES_EXPORT_XML = """\
<?xml version="1.0"?>
<export-steps>
@@ -653,6 +813,7 @@
Title=%s
"""
+
def _underscoreSiteTitle( context ):
site = context.getSite()
Modified: GenericSetup/trunk/tests/test_zcml.py
===================================================================
--- GenericSetup/trunk/tests/test_zcml.py 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/tests/test_zcml.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -18,8 +18,17 @@
import unittest
import Testing
from zope.testing import doctest
+from zope.testing.doctest import ELLIPSIS
+def dummy_upgrade_handler(context):
+ pass
+def b_dummy_upgrade_handler(context):
+ pass
+
+def c_dummy_upgrade_handler(context):
+ pass
+
def test_registerProfile():
"""
Use the genericsetup:registerProfile directive::
@@ -73,10 +82,157 @@
False
"""
+def test_registerUpgradeStep(self):
+ """
+ Use the genericsetup:upgradeStep directive::
+ >>> import Products.GenericSetup
+ >>> from Products.Five import zcml
+ >>> configure_zcml = '''
+ ... <configure
+ ... xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+ ... i18n_domain="foo">
+ ... <genericsetup:upgradeStep
+ ... title="Upgrade Foo Product"
+ ... description="Upgrades Foo from 1.0 to 1.1."
+ ... source="1.0"
+ ... destination="1.1"
+ ... handler="Products.GenericSetup.tests.test_zcml.dummy_upgrade_handler"
+ ... sortkey="1"
+ ... profile="default"
+ ... />
+ ... </configure>'''
+ >>> zcml.load_config('meta.zcml', Products.GenericSetup)
+ >>> zcml.load_string(configure_zcml)
+
+ Make sure the upgrade step is registered correctly::
+
+ >>> from Products.GenericSetup.upgrade import _upgrade_registry
+ >>> profile_steps = _upgrade_registry.getUpgradeStepsForProfile('default')
+ >>> keys = profile_steps.keys()
+ >>> len(keys)
+ 1
+ >>> step = profile_steps[keys[0]]
+ >>> step.source
+ ('1', '0')
+ >>> step.dest
+ ('1', '1')
+ >>> step.handler
+ <function dummy_upgrade_handler at ...>
+
+ Clean up and make sure the cleanup works::
+
+ >>> from zope.testing.cleanup import cleanUp
+ >>> cleanUp()
+ """
+
+
+def test_registerUpgradeSteps(self):
+ """
+ Use the nested genericsetup:upgradeSteps directive::
+
+ >>> import Products.GenericSetup
+ >>> from Products.Five import zcml
+ >>> configure_zcml = '''
+ ... <configure
+ ... xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+ ... i18n_domain="foo">
+ ... <genericsetup:upgradeSteps
+ ... profile="default"
+ ... source="1.0"
+ ... destination="1.1"
+ ... sortkey="2"
+ ... >
+ ... <genericsetup:upgradeStep
+ ... title="Foo Upgrade Step 1"
+ ... description="Does some Foo upgrade thing."
+ ... handler="Products.GenericSetup.tests.test_zcml.b_dummy_upgrade_handler"
+ ... />
+ ... <genericsetup:upgradeStep
+ ... title="Foo Upgrade Step 2"
+ ... description="Does another Foo upgrade thing."
+ ... handler="Products.GenericSetup.tests.test_zcml.c_dummy_upgrade_handler"
+ ... />
+ ... </genericsetup:upgradeSteps>
+ ... <genericsetup:upgradeSteps
+ ... profile="default"
+ ... source="1.0"
+ ... destination="1.1"
+ ... sortkey="1"
+ ... >
+ ... <genericsetup:upgradeStep
+ ... title="Bar Upgrade Step 1"
+ ... description="Does some Bar upgrade thing."
+ ... handler="Products.GenericSetup.tests.test_zcml.b_dummy_upgrade_handler"
+ ... />
+ ... <genericsetup:upgradeStep
+ ... title="Bar Upgrade Step 2"
+ ... description="Does another Bar upgrade thing."
+ ... handler="Products.GenericSetup.tests.test_zcml.c_dummy_upgrade_handler"
+ ... />
+ ... </genericsetup:upgradeSteps>
+ ... </configure>'''
+ >>> zcml.load_config('meta.zcml', Products.GenericSetup)
+ >>> zcml.load_string(configure_zcml)
+
+ Make sure the upgrade steps are registered correctly::
+
+ >>> from Products.GenericSetup.upgrade import _upgrade_registry
+ >>> from Products.GenericSetup.upgrade import listUpgradeSteps
+ >>> from Products.GenericSetup.tool import SetupTool
+ >>> tool = SetupTool('setup_tool')
+ >>> profile_steps = listUpgradeSteps(tool, 'default', '1.0')
+ >>> len(profile_steps)
+ 2
+ >>> steps = profile_steps[0]
+ >>> type(steps)
+ <type 'list'>
+ >>> len(steps)
+ 2
+ >>> step1, step2 = steps
+ >>> step1['source'] == step2['source'] == ('1', '0')
+ True
+ >>> step1['dest'] == step2['dest'] == ('1', '1')
+ True
+ >>> step1['step'].handler
+ <function b_dummy_upgrade_handler at ...>
+ >>> step1['title']
+ u'Bar Upgrade Step 1'
+ >>> step2['step'].handler
+ <function c_dummy_upgrade_handler at ...>
+ >>> step2['title']
+ u'Bar Upgrade Step 2'
+
+ First one listed should be second in the registry due to sortkey:
+
+ >>> steps = profile_steps[1]
+ >>> type(steps)
+ <type 'list'>
+ >>> len(steps)
+ 2
+ >>> step1, step2 = steps
+ >>> step1['source'] == step2['source'] == ('1', '0')
+ True
+ >>> step1['dest'] == step2['dest'] == ('1', '1')
+ True
+ >>> step1['step'].handler
+ <function b_dummy_upgrade_handler at ...>
+ >>> step1['title']
+ u'Foo Upgrade Step 1'
+ >>> step2['step'].handler
+ <function c_dummy_upgrade_handler at ...>
+ >>> step2['title']
+ u'Foo Upgrade Step 2'
+
+ Clean up and make sure the cleanup works::
+
+ >>> from zope.testing.cleanup import cleanUp
+ >>> cleanUp()
+ """
+
def test_suite():
return unittest.TestSuite((
- doctest.DocTestSuite(),
+ doctest.DocTestSuite(optionflags=ELLIPSIS),
))
if __name__ == '__main__':
Copied: GenericSetup/trunk/tests/versioned_profile (from rev 76823, GenericSetup/branches/tseaver-bbq_sprint/tests/versioned_profile)
Modified: GenericSetup/trunk/tool.py
===================================================================
--- GenericSetup/trunk/tool.py 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/tool.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -17,6 +17,8 @@
import os
import time
+import logging
+from warnings import warn
from cgi import escape
from AccessControl import ClassSecurityInfo
@@ -28,6 +30,7 @@
from zope.interface import implements
from zope.interface import implementedBy
+from interfaces import BASE
from interfaces import EXTENSION
from interfaces import ISetupTool
from interfaces import SKIPPED_FILES
@@ -43,6 +46,10 @@
from registry import ToolsetRegistry
from registry import _profile_registry
+from upgrade import listUpgradeSteps
+from upgrade import listProfilesWithUpgrades
+from upgrade import _upgrade_registry
+
from utils import _resolveDottedName
from utils import _wwwdir
@@ -113,6 +120,7 @@
else:
unwrapped = aq_base(existing)
if not isinstance(unwrapped, tool_class):
+
site._delObject(tool_id)
site._setObject(tool_id, tool_class())
@@ -141,8 +149,12 @@
meta_type = 'Generic Setup Tool'
+ _baseline_context_id = ''
+ # BBB _import_context_id is a vestige of a stateful import context
_import_context_id = ''
+ _profile_upgrade_versions = {}
+
security = ClassSecurityInfo()
def __init__(self, id):
@@ -163,21 +175,49 @@
""" See ISetupTool.
"""
- return 'ascii'
+ return 'utf-8'
security.declareProtected(ManagePortal, 'getImportContextID')
def getImportContextID(self):
""" See ISetupTool.
"""
+ warn('getImportContextId, and the very concept of a stateful '
+ 'active import context, is deprecated. You can find the '
+ 'base profile that was applied using getBaselineContextID.',
+ DeprecationWarning, stacklevel=2)
return self._import_context_id
+ security.declareProtected(ManagePortal, 'getBaselineContextID')
+ def getBaselineContextID(self):
+
+ """ See ISetupTool.
+ """
+ return self._baseline_context_id
+
security.declareProtected(ManagePortal, 'setImportContext')
def setImportContext(self, context_id, encoding=None):
+ """ See ISetupTool.
+ """
+ warn('setImportContext is deprecated. Use setBaselineContext to '
+ 'specify the baseline context, and/or runImportStepsFromContext '
+ 'to run the steps from a specific import context.',
+ DeprecationWarning, stacklevel=2)
+ self._import_context_id = context_id
+ context_type = BASE # snapshots are always baseline contexts
+ if context_id.startswith('profile-'):
+ profile_info = _profile_registry.getProfileInfo(context_id[8:])
+ context_type = profile_info['type']
+
+ if context_type == BASE:
+ self.setBaselineContext(context_id, encoding)
+
+ security.declareProtected(ManagePortal, 'setBaselineContext')
+ def setBaselineContext(self, context_id, encoding=None):
""" See ISetupTool.
"""
- self._import_context_id = context_id
+ self._baseline_context_id = context_id
context = self._getImportContext(context_id)
self.applyContext(context, encoding)
@@ -208,12 +248,12 @@
"""
return self._toolset_registry
- security.declareProtected(ManagePortal, 'runImportStep')
- def runImportStep(self, step_id, run_dependencies=True, purge_old=None):
-
+ security.declareProtected(ManagePortal, 'runImportStepFromProfile')
+ def runImportStepFromProfile(self, profile_id, step_id,
+ run_dependencies=True, purge_old=None):
""" See ISetupTool.
"""
- context = self._getImportContext(self._import_context_id, purge_old)
+ context = self._getImportContext(profile_id, purge_old)
info = self._import_registry.getStepMetadata(step_id)
@@ -240,17 +280,47 @@
return { 'steps' : steps, 'messages' : messages }
+ security.declareProtected(ManagePortal, 'runImportStep')
+ def runImportStep(self, step_id, run_dependencies=True, purge_old=None):
+
+ """ See ISetupTool.
+ """
+ warn('The runImportStep method is deprecated. Please use '
+ 'runImportStepFromProfile instead.',
+ DeprecationWarning, stacklevel=2)
+ return self.runImportStepFromProfile(self._import_context_id,
+ step_id,
+ run_dependencies,
+ purge_old,
+ )
+
+ security.declareProtected(ManagePortal, 'runAllImportStepsFromProfile')
+ def runAllImportStepsFromProfile(self, profile_id, purge_old=None):
+
+ """ See ISetupTool.
+ """
+ __traceback_info__ = profile_id
+
+ context = self._getImportContext(profile_id, purge_old)
+
+ result = self._runImportStepsFromContext(context, purge_old=purge_old)
+ prefix = 'import-all-%s' % profile_id.replace(':', '_')
+ name = self._mangleTimestampName(prefix, 'log')
+ self._createReport(name, result['steps'], result['messages'])
+ return result
+
security.declareProtected(ManagePortal, 'runAllImportSteps')
def runAllImportSteps(self, purge_old=None):
""" See ISetupTool.
"""
- __traceback_info__ = self._import_context_id
+ warn('The runAllImportSteps method is deprecated. Please use '
+ 'runAllImportStepsFromProfile instead.',
+ DeprecationWarning, stacklevel=2)
+ context_id = self._import_context_id
+ return self.runAllImportStepsFromProfile(self._import_context_id,
+ purge_old)
- context = self._getImportContext(self._import_context_id, purge_old)
-
- return self._runImportStepsFromContext(context, purge_old=purge_old)
-
security.declareProtected(ManagePortal, 'runExportStep')
def runExportStep(self, step_id):
@@ -361,7 +431,7 @@
# ZMI
#
manage_options = (Folder.manage_options[:1]
- + ({'label' : 'Properties',
+ + ({'label' : 'Profiles',
'action' : 'manage_tool'
},
{'label' : 'Import',
@@ -370,6 +440,9 @@
{'label' : 'Export',
'action' : 'manage_exportSteps'
},
+ {'label' : 'Upgrades',
+ 'action' : 'manage_upgrades'
+ },
{'label' : 'Snapshots',
'action' : 'manage_snapshots'
},
@@ -387,7 +460,7 @@
def manage_updateToolProperties(self, context_id, RESPONSE):
""" Update the tool's settings.
"""
- self.setImportContext(context_id)
+ self.setBaselineContext(context_id)
RESPONSE.redirect('%s/manage_tool?manage_tabs_message=%s'
% (self.absolute_url(), 'Properties+updated.'))
@@ -396,12 +469,7 @@
manage_importSteps = PageTemplateFile('sutImportSteps', _wwwdir)
security.declareProtected(ManagePortal, 'manage_importSelectedSteps')
- def manage_importSelectedSteps(self,
- ids,
- run_dependencies,
- RESPONSE,
- create_report=True,
- ):
+ def manage_importSelectedSteps(self, ids, run_dependencies):
""" Import the steps selected by the user.
"""
messages = {}
@@ -417,30 +485,54 @@
summary = 'Steps run: %s' % ', '.join(steps_run)
- if create_report:
- name = self._mangleTimestampName('import-selected', 'log')
- self._createReport(name, result['steps'], result['messages'])
+ name = self._mangleTimestampName('import-selected', 'log')
+ self._createReport(name, result['steps'], result['messages'])
return self.manage_importSteps(manage_tabs_message=summary,
messages=messages)
security.declareProtected(ManagePortal, 'manage_importSelectedSteps')
- def manage_importAllSteps(self, RESPONSE, create_report=True):
+ def manage_importAllSteps(self):
""" Import all steps.
"""
result = self.runAllImportSteps()
steps_run = 'Steps run: %s' % ', '.join(result['steps'])
- if create_report:
- name = self._mangleTimestampName('import-all', 'log')
- self._createReport(name, result['steps'], result['messages'])
+ name = self._mangleTimestampName('import-all', 'log')
+ self._createReport(name, result['steps'], result['messages'])
return self.manage_importSteps(manage_tabs_message=steps_run,
messages=result['messages'])
+ security.declareProtected(ManagePortal, 'manage_importExtensions')
+ def manage_importExtensions(self, RESPONSE, profile_ids=()):
+
+ """ Import all steps for the selected extension profiles.
+ """
+ detail = {}
+ if len(profile_ids) == 0:
+ message = 'Please select one or more extension profiles.'
+ RESPONSE.redirect('%s/manage_tool?manage_tabs_message=%s'
+ % (self.absolute_url(), message))
+ else:
+ message = 'Imported profiles: %s' % ', '.join(profile_ids)
+
+ for profile_id in profile_ids:
+
+ result = self.runAllImportStepsFromProfile(profile_id)
+
+ prefix = 'import-all-%s' % profile_id.replace(':', '_')
+ name = self._mangleTimestampName(prefix, 'log')
+ self._createReport(name, result['steps'], result['messages'])
+ for k, v in result['messages'].items():
+ detail['%s:%s' % (profile_id, k)] = v
+
+ return self.manage_importSteps(manage_tabs_message=message,
+ messages=detail)
+
security.declareProtected(ManagePortal, 'manage_importTarball')
- def manage_importTarball(self, tarball, RESPONSE, create_report=True):
+ def manage_importTarball(self, tarball):
""" Import steps from the uploaded tarball.
"""
if getattr(tarball, 'read', None) is not None:
@@ -455,9 +547,8 @@
purge_old=True)
steps_run = 'Steps run: %s' % ', '.join(result['steps'])
- if create_report:
- name = self._mangleTimestampName('import-all', 'log')
- self._createReport(name, result['steps'], result['messages'])
+ name = self._mangleTimestampName('import-all', 'log')
+ self._createReport(name, result['steps'], result['messages'])
return self.manage_importSteps(manage_tabs_message=steps_run,
messages=result['messages'])
@@ -491,6 +582,12 @@
'attachment; filename=%s' % result['filename'])
return result['tarball']
+ security.declareProtected(ManagePortal, 'manage_upgrades')
+ manage_upgrades = PageTemplateFile('setup_upgrades', _wwwdir)
+
+ security.declareProtected(ManagePortal, 'upgradeStepMacro')
+ upgradeStepMacro = PageTemplateFile('upgradeStep', _wwwdir)
+
security.declareProtected(ManagePortal, 'manage_snapshots')
manage_snapshots = PageTemplateFile('sutSnapshots', _wwwdir)
@@ -524,6 +621,7 @@
def listProfileInfo(self):
""" Return a list of mappings describing registered profiles.
+ Base profile is listed first, extensions are sorted.
o Keys include:
@@ -537,23 +635,62 @@
'product' -- name of the registering product
"""
- return _profile_registry.listProfileInfo()
+ base = []
+ ext = []
+ for info in _profile_registry.listProfileInfo():
+ if info.get('type', BASE) == BASE:
+ base.append(info)
+ else:
+ ext.append(info)
+ ext.sort(lambda x, y: cmp(x['id'], y['id']))
+ return base + ext
security.declareProtected(ManagePortal, 'listContextInfos')
def listContextInfos(self):
""" List registered profiles and snapshots.
"""
+ def readableType(x):
+ if x is BASE:
+ return 'base'
+ elif x is EXTENSION:
+ return 'extension'
+ return 'unknown'
- s_infos = [{ 'id': 'snapshot-%s' % info['id'],
- 'title': info['title'] }
+ s_infos = [{'id': 'snapshot-%s' % info['id'],
+ 'title': info['title'],
+ 'type': 'snapshot',
+ }
for info in self.listSnapshotInfo()]
- p_infos = [{ 'id': 'profile-%s' % info['id'],
- 'title': info['title'] }
- for info in self.listProfileInfo()]
+ p_infos = [{'id': 'profile-%s' % info['id'],
+ 'title': info['title'],
+ 'type': readableType(info['type']),
+ }
+ for info in self.listProfileInfo()]
return tuple(s_infos + p_infos)
+ security.declareProtected(ManagePortal, 'getProfileImportDate')
+ def getProfileImportDate(self, profile_id):
+ """ See ISetupTool.
+ """
+ prefix = ('import-all-%s-' % profile_id).replace(':', '_')
+ candidates = [x for x in self.objectIds('File')
+ if x.startswith(prefix)]
+ if len(candidates) == 0:
+ return None
+ candidates.sort()
+ last = candidates[-1]
+ stamp = last[len(prefix):-4]
+ assert(len(stamp) == 14)
+ return '%s-%s-%sT%s:%s:%sZ' % (stamp[0:4],
+ stamp[4:6],
+ stamp[6:8],
+ stamp[8:10],
+ stamp[10:12],
+ stamp[12:14],
+ )
+
security.declareProtected(ManagePortal, 'manage_createSnapshot')
def manage_createSnapshot(self, RESPONSE, snapshot_id=None):
@@ -609,7 +746,95 @@
ignore_blanks,
)
+ #
+ # Upgrades management
+ #
+ security.declareProtected(ManagePortal, 'getLastVersionForProfile')
+ def getLastVersionForProfile(self, profile_id):
+ """Return the last upgraded version for the specified profile.
+ """
+ version = self._profile_upgrade_versions.get(profile_id, 'unknown')
+ return version
+ security.declareProtected(ManagePortal, 'setLastVersionForProfile')
+ def setLastVersionForProfile(self, profile_id, version):
+ """Set the last upgraded version for the specified profile.
+ """
+ if isinstance(version, basestring):
+ version = tuple(version.split('.'))
+ prof_versions = self._profile_upgrade_versions.copy()
+ prof_versions[profile_id] = version
+ self._profile_upgrade_versions = prof_versions
+
+ security.declareProtected(ManagePortal, 'getVersionForProfile')
+ def getVersionForProfile(self, profile_id):
+ """Return the registered filesystem version for the specified
+ profile.
+ """
+ info = _profile_registry.getProfileInfo(profile_id)
+ return info.get('version', 'unknown')
+
+ security.declareProtected(ManagePortal, 'listProfilesWithUpgrades')
+ def listProfilesWithUpgrades(self):
+ return listProfilesWithUpgrades()
+
+ security.declarePrivate('_massageUpgradeInfo')
+ def _massageUpgradeInfo(self, info):
+ """Add a couple of data points to the upgrade info dictionary.
+ """
+ info = info.copy()
+ info['haspath'] = info['source'] and info['dest']
+ info['ssource'] = '.'.join(info['source'] or ('all',))
+ info['sdest'] = '.'.join(info['dest'] or ('all',))
+ return info
+
+ security.declareProtected(ManagePortal, 'listUpgrades')
+ def listUpgrades(self, profile_id, show_old=False):
+ """Get the list of available upgrades.
+ """
+ if show_old:
+ source = None
+ else:
+ source = self.getLastVersionForProfile(profile_id)
+ upgrades = listUpgradeSteps(self, profile_id, source)
+ res = []
+ for info in upgrades:
+ if type(info) == list:
+ subset = []
+ for subinfo in info:
+ subset.append(self._massageUpgradeInfo(subinfo))
+ res.append(subset)
+ else:
+ res.append(self._massageUpgradeInfo(info))
+ return res
+
+ security.declareProtected(ManagePortal, 'manage_doUpgrades')
+ def manage_doUpgrades(self, request=None):
+ """Perform all selected upgrade steps.
+ """
+ if request is None:
+ request = self.REQUEST
+ logger = logging.getLogger('GenericSetup')
+ steps_to_run = request.form.get('upgrades', [])
+ profile_id = request.get('profile_id', '')
+ for step_id in steps_to_run:
+ step = _upgrade_registry.getUpgradeStep(profile_id, step_id)
+ if step is not None:
+ step.doStep(self)
+ msg = "Ran upgrade step %s for profile %s" % (step.title,
+ profile_id)
+ logger.log(logging.INFO, msg)
+
+ # XXX should be a bit smarter about deciding when to up the
+ # profile version
+ profile_info = _profile_registry.getProfileInfo(profile_id)
+ version = profile_info.get('version', None)
+ if version is not None:
+ self.setLastVersionForProfile(profile_id, version)
+
+ url = self.absolute_url()
+ request.RESPONSE.redirect("%s/manage_upgrades?saved=%s" % (url, profile_id))
+
#
# Helper methods
#
@@ -807,6 +1032,10 @@
if isinstance(report, unicode):
report = report.encode('latin-1')
+ # BBB: ObjectManager won't allow unicode IDS
+ if isinstance(name, unicode):
+ name = name.encode('UTF-8')
+
file = File(id=name,
title='',
file=report,
Copied: GenericSetup/trunk/upgrade.py (from rev 76823, GenericSetup/branches/tseaver-bbq_sprint/upgrade.py)
===================================================================
--- GenericSetup/trunk/upgrade.py (rev 0)
+++ GenericSetup/trunk/upgrade.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -0,0 +1,176 @@
+##############################################################################
+#
+# Copyright (c) 2005 Nuxeo SARL <http://nuxeo.com>
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from BTrees.OOBTree import OOBTree
+
+from registry import _profile_registry
+
+class UpgradeRegistry(object):
+ """Registry of upgrade steps, by profile.
+
+ Registry keys are profile ids.
+
+ Each registry value is a nested mapping:
+ - id -> step for single steps
+ - id -> [ (id1, step1), (id2, step2) ] for nested steps
+ """
+ def __init__(self):
+ self._registry = OOBTree()
+
+ def __getitem__(self, key):
+ return self._registry.get(key)
+
+ def keys(self):
+ return self._registry.keys()
+
+ def clear(self):
+ self._registry.clear()
+
+ def getUpgradeStepsForProfile(self, profile_id):
+ """Return the upgrade steps mapping for a given profile, or
+ None if there are no steps registered for a profile matching
+ that id.
+ """
+ profile_steps = self._registry.get(profile_id, None)
+ if profile_steps is None:
+ self._registry[profile_id] = OOBTree()
+ profile_steps = self._registry.get(profile_id)
+ return profile_steps
+
+ def getUpgradeStep(self, profile_id, step_id):
+ """Returns the specified upgrade step for the specified
+ profile, or None if it doesn't exist.
+ """
+ profile_steps = self._registry.get(profile_id, None)
+ if profile_steps is not None:
+ step = profile_steps.get(step_id, None)
+ if step is None:
+ for key in profile_steps.keys():
+ if type(profile_steps[key]) == list:
+ subs = dict(profile_steps[key])
+ step = subs.get(step_id, None)
+ if step is not None:
+ break
+ elif type(step) == list:
+ subs = dict(step)
+ step = subs.get(step_id, None)
+ return step
+
+_upgrade_registry = UpgradeRegistry()
+
+class UpgradeStep(object):
+ """A step to upgrade a component.
+ """
+ def __init__(self, title, profile, source, dest, desc, handler,
+ checker=None, sortkey=0):
+ self.id = str(abs(hash('%s%s%s%s' % (title, source, dest, sortkey))))
+ self.title = title
+ if source == '*':
+ source = None
+ elif isinstance(source, basestring):
+ source = tuple(source.split('.'))
+ self.source = source
+ if dest == '*':
+ dest = None
+ elif isinstance(dest, basestring):
+ dest = tuple(dest.split('.'))
+ self.dest = dest
+ self.description = desc
+ self.handler = handler
+ self.checker = checker
+ self.sortkey = sortkey
+ self.profile = profile
+
+ def versionMatch(self, source):
+ return (source is None or
+ self.source is None or
+ source <= self.source)
+
+ def isProposed(self, tool, source):
+ """Check if a step can be applied.
+
+ False means already applied or does not apply.
+ True means can be applied.
+ """
+ checker = self.checker
+ if checker is None:
+ return self.versionMatch(source)
+ else:
+ return checker(tool)
+
+ def doStep(self, tool):
+ self.handler(tool)
+
+def _registerUpgradeStep(step):
+ profile_id = step.profile
+ profile_steps = _upgrade_registry.getUpgradeStepsForProfile(profile_id)
+ profile_steps[step.id] = step
+
+def _registerNestedUpgradeStep(step, outer_id):
+ profile_id = step.profile
+ profile_steps = _upgrade_registry.getUpgradeStepsForProfile(profile_id)
+ nested_steps = profile_steps.get(outer_id, [])
+ nested_steps.append((step.id, step))
+ profile_steps[outer_id] = nested_steps
+
+def _extractStepInfo(tool, id, step, source):
+ """Returns the info data structure for a given step.
+ """
+ proposed = step.isProposed(tool, source)
+ if (not proposed
+ and source is not None
+ and (step.source is None or source > step.source)):
+ return
+ info = {
+ 'id': id,
+ 'step': step,
+ 'title': step.title,
+ 'source': step.source,
+ 'dest': step.dest,
+ 'description': step.description,
+ 'proposed': proposed,
+ 'sortkey': step.sortkey,
+ }
+ return info
+
+def listProfilesWithUpgrades():
+ return _upgrade_registry.keys()
+
+def listUpgradeSteps(tool, profile_id, source):
+ """Lists upgrade steps available from a given version, for a given
+ profile id.
+ """
+ res = []
+ profile_steps = _upgrade_registry.getUpgradeStepsForProfile(profile_id)
+ for id, step in profile_steps.items():
+ if type(step) == UpgradeStep:
+ info = _extractStepInfo(tool, id, step, source)
+ if info is None:
+ continue
+ res.append(((step.source or '', step.sortkey, info['proposed']), info))
+ else: # nested steps
+ nested = []
+ outer_proposed = False
+ for inner_id, inner_step in step:
+ info = _extractStepInfo(tool, inner_id, inner_step, source)
+ if info is None:
+ continue
+ nested.append(info)
+ outer_proposed = outer_proposed or info['proposed']
+ if nested:
+ src = nested[0]['source']
+ sortkey = nested[0]['sortkey']
+ res.append(((src or '', sortkey, outer_proposed), nested))
+ res.sort()
+ res = [i[1] for i in res]
+ return res
Modified: GenericSetup/trunk/utils.py
===================================================================
--- GenericSetup/trunk/utils.py 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/utils.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -181,7 +181,9 @@
return self._extractNode(root)
def _extractNode(self, node):
-
+ """ Please see docs/configurator.txt for information about the
+ import mapping syntax.
+ """
nodes_map = self._getImportMapping()
if node.nodeName not in nodes_map:
nodes_map = self._getSharedImportMapping()
Copied: GenericSetup/trunk/www/setup_upgrades.zpt (from rev 76823, GenericSetup/branches/tseaver-bbq_sprint/www/setup_upgrades.zpt)
===================================================================
--- GenericSetup/trunk/www/setup_upgrades.zpt (rev 0)
+++ GenericSetup/trunk/www/setup_upgrades.zpt 2007-06-20 19:24:38 UTC (rev 76859)
@@ -0,0 +1,105 @@
+<html tal:define="profile_id request/saved | request/profile_id | nothing;
+ prof_w_upgrades context/listProfilesWithUpgrades">
+
+<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
+<h2 tal:replace="structure context/manage_tabs">TABS</h2>
+
+<strong tal:condition="python:request.form.has_key('saved')">
+ <span tal:replace="request/saved" /> profile saved.
+</strong>
+
+<h3>Upgrades</h3>
+
+<tal:choose-profile condition="prof_w_upgrades">
+ <form method="post" action="manage_upgrades">
+ <select name="profile_id">
+ <option tal:repeat="prof_id context/listProfilesWithUpgrades"
+ tal:content="prof_id"
+ tal:attributes="selected python:prof_id == profile_id"/>
+ </select>
+ <input type="submit" value="Choose Profile" />
+ </form>
+</tal:choose-profile>
+
+<strong tal:condition="not: prof_w_upgrades">
+ No profiles with registered upgrade steps.
+</strong>
+
+<tal:profile-specified condition="profile_id">
+
+<p class="form-help">
+ The profile "<span tal:replace="profile_id" />" is currently upgraded to version
+ <strong tal:define="version python:context.getLastVersionForProfile(profile_id)"
+ tal:content="python:test(same_type(version, tuple()), '.'.join(version), version)">
+ LAST UPGRADED VERSION
+ </strong>.
+</p>
+
+<p class="form-help">
+ The filesystem version for the "<span tal:replace="profile_id" />" profile is currently
+ <strong tal:content="python:context.getVersionForProfile(profile_id)">
+ CURRENT FILESYSTEM VERSION
+ </strong>.
+</p>
+
+<tal:block define="show_old request/show_old | python:0;
+ upgrades python:context.listUpgrades(profile_id, show_old=show_old)">
+
+<form method="post" action="manage_doUpgrades" tal:condition="upgrades">
+<p class="form-help">
+ Available upgrades:
+</p>
+<input type="hidden" name="show_old:int" value="VALUE"
+ tal:attributes="value show_old" />
+<input type="hidden" name="profile_id" value="VALUE"
+ tal:attributes="value profile_id" />
+<table>
+ <tr valign="top" tal:repeat="upgrade_info upgrades">
+
+ <tal:single condition="python:not same_type(upgrade_info, [])"
+ define="info upgrade_info">
+ <metal:insert-step use-macro="context/upgradeStepMacro/macros/upgrade-step" />
+ </tal:single>
+
+ <tal:multiple condition="python: same_type(upgrade_info, [])">
+ <table>
+ <tr>
+ <td colspan="5">Upgrade Step Group</td>
+ </tr>
+ <tr tal:repeat="info upgrade_info">
+ <td>-></td>
+ <metal:insert-step use-macro="context/upgradeStepMacro/macros/upgrade-step" />
+ </tr>
+ </table>
+ </tal:multiple>
+ </tr>
+
+ <tr valign="top">
+ <td colspan="4">
+ <input class="form-element" type="submit" value="Upgrade" />
+ </td>
+ </tr>
+</table>
+</form>
+
+<p tal:condition="not:upgrades">
+ No upgrade available.
+</p>
+
+<form method="post" action="manage_upgrades" tal:condition="not:show_old">
+<p class="form-help">
+ Show old upgrades:
+ <input type="submit" value="Show" />
+ <input type="hidden" name="show_old:int" value="1" />
+ <input type="hidden" name="profile_id" value="VALUE"
+ tal:attributes="value profile_id" />
+</p>
+</form>
+
+</tal:block>
+
+</tal:profile-specified>
+
+<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
+
+</html>
Modified: GenericSetup/trunk/www/sutImportSteps.zpt
===================================================================
--- GenericSetup/trunk/www/sutImportSteps.zpt 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/www/sutImportSteps.zpt 2007-06-20 19:24:38 UTC (rev 76859)
@@ -79,11 +79,15 @@
</td>
</tr>
</tbody>
+</table>
- <tbody tal:condition="options/messages | nothing">
+<table cellspacing="0" cellpadding="4"
+ tal:condition="options/messages | nothing">
+
<tr class="list-header">
<td colspan="4">Message Log</td>
</tr>
+
<tr valign="top"
tal:repeat="item options/messages/items">
<td tal:content="python: item[0]">STEP</td>
@@ -91,7 +95,6 @@
tal:content="structure python: item[1].replace('\n', '<br />')"
>MESSAGE</td>
</tr>
- </tbody>
</table>
</form>
Modified: GenericSetup/trunk/www/sutProperties.zpt
===================================================================
--- GenericSetup/trunk/www/sutProperties.zpt 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/www/sutProperties.zpt 2007-06-20 19:24:38 UTC (rev 76859)
@@ -1,37 +1,95 @@
<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
<h2 tal:replace="structure context/manage_tabs">TABS</h2>
+<style type="text/css">
+.warning {
+ color: red;
+ font-weight: bold;
+}
+.info {
+ color: #446688;
+ font-style: italic;
+ margin-left: 2em;
+}
+</style>
-<h3> Setup Tool Properties </h3>
+<div tal:define="contexts context/listContextInfos;
+ snaps python: [x for x in contexts if x['type'] == 'snapshot'];
+ bases python: [x for x in contexts if x['type'] == 'base'];
+ context_id context/getBaselineContextID;
+ context_id_display python: context_id or '(none)';
+ overwrite_style python: context_id != ''
+ and 'display: none' or 'display: block';
+ exts python: [x for x in contexts if x['type'] == 'extension'];
+ ">
+<h3> Setup Tool Profiles </h3>
-<form method="post" action="manage_updateToolProperties">
+<form method="post" action=".">
-<table>
+ <fieldset id="baseline_fs">
+ <legend>Baseline Profile</legend>
+
+ <p>Active baseline: <span tal:content="context_id_display">(none)</span>
+ <tal:whatever
+ tal:define="last python:context.getProfileImportDate(context_id);
+ ">
+ <span class="info"
+ tal:condition="last">
+ Last imported <tal:x tal:content="last">TIMESTAMP</tal:x></span>
+ </tal:whatever>
+ </p>
- <tr valign="top">
- <td>
- <div class="form-label">Active site configuration:</div>
- </td>
- <td>
- <select name="context_id"
- tal:define="context_id context/getImportContextID">
+ <div tal:condition="python: context_id != ''">
+ <script type="text/javascript" lang="JavaScript">
+ function showOverwrite(e) {
+ var overwrite = document.getElementById('overwrite');
+ overwrite.style.display = 'block';
+ }
+ </script>
+ <p style="color: red"> Changing the baseline profile is potentially a
+ dangerous operation.
+ <a href="#" onclick="showOverwrite(this); return false">Click here</a>
+ if you really need to do this.
+ </p>
+ </div>
+
+ <div id="overwrite"
+ tal:attributes="style overwrite_style">
+ <select name="context_id">
<option value="context-CONTEXT_ID"
- tal:repeat="context_info context/listContextInfos"
+ tal:repeat="context_info bases"
tal:attributes="selected python:context_id == context_info['id'];
value context_info/id"
tal:content="context_info/title"
>CONTEXT_TITLE</option>
</select>
- </td>
- </tr>
+ <input class="form-element" type="submit"
+ name="manage_updateToolProperties:method"
+ value="Update Base Profile" />
+ </div>
+ </fieldset>
- <tr valign="top">
- <td />
- <td>
- <input class="form-element" type="submit" value=" Update " />
- </td>
- </tr>
+ <fieldset id="extesions_fs">
+ <legend>Extension Profiles</legend>
+ <p tal:repeat="extension exts">
+ <tal:whatever
+ tal:define="fid string:extension_${extension/id};
+ last python:context.getProfileImportDate(extension['id']);
+ ">
+ <input type="checkbox" id="extension_0" name="profile_ids:list" value="waaa"
+ tal:attributes="id fid;
+ value extension/id;
+ "/>
+ <label tal:content="extension/title">TITLE</label>
+ <span class="info"
+ tal:condition="last">
+ Last imported <tal:x tal:content="last">TIMESTAMP</tal:x></span>
+ </tal:whatever>
+ </p>
+ <p><input type="submit" name="manage_importExtensions:method"
+ value="Import Selected Extensions" /></p>
+ </fieldset>
-</table>
</form>
+</div>
<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
Copied: GenericSetup/trunk/www/upgradeStep.zpt (from rev 76823, GenericSetup/branches/tseaver-bbq_sprint/www/upgradeStep.zpt)
===================================================================
--- GenericSetup/trunk/www/upgradeStep.zpt (rev 0)
+++ GenericSetup/trunk/www/upgradeStep.zpt 2007-06-20 19:24:38 UTC (rev 76859)
@@ -0,0 +1,24 @@
+<html>
+
+ <metal:upgrade-step define-macro="upgrade-step">
+ <td>
+ <input type="checkbox" name="upgrades:list"
+ value="VALUE" checked="CHECKED"
+ tal:attributes="value info/id;
+ checked python:info['proposed'] and not show_old;
+ "/>
+ </td>
+ <td>
+ <div tal:replace="info/title">INFO</div>
+ </td>
+ <td class="form-help">
+ <div tal:condition="info/haspath"
+ tal:content="structure string:(${info/ssource} &#8594; ${info/sdest})">PATH</div>
+ </td>
+ <td class="form-help">
+ <div tal:condition="not:info/proposed"
+ tal:replace="default">(done)</div>
+ </td>
+ </metal:upgrade-step>
+
+</html>
Modified: GenericSetup/trunk/zcml.py
===================================================================
--- GenericSetup/trunk/zcml.py 2007-06-20 19:20:03 UTC (rev 76858)
+++ GenericSetup/trunk/zcml.py 2007-06-20 19:24:38 UTC (rev 76859)
@@ -23,7 +23,9 @@
from interfaces import BASE
from registry import _profile_registry
+from upgrade import _upgrade_registry
+#### genericsetup:registerProfile
class IRegisterProfileDirective(Interface):
@@ -81,6 +83,99 @@
)
+#### genericsetup:upgradeStep
+
+import zope.schema
+from upgrade import UpgradeStep
+from upgrade import _registerUpgradeStep
+from upgrade import _registerNestedUpgradeStep
+
+class IUpgradeStepsDirective(Interface):
+ """
+ Define multiple upgrade steps without repeating all of the parameters
+ """
+ source = zope.schema.ASCII(
+ title=u"Source version",
+ required=False)
+
+ destination = zope.schema.ASCII(
+ title=u"Destination version",
+ required=False)
+
+ sortkey = zope.schema.Int(
+ title=u"Sort key",
+ required=False)
+
+ profile = zope.schema.TextLine(
+ title=u"GenericSetup profile id",
+ required=True)
+
+class IUpgradeStepsStepSubDirective(Interface):
+ """
+ Subdirective to IUpgradeStepsDirective
+ """
+ title = zope.schema.TextLine(
+ title=u"Title",
+ required=True)
+
+ description = zope.schema.TextLine(
+ title=u"Upgrade step description",
+ required=True)
+
+ handler = GlobalObject(
+ title=u"Upgrade handler",
+ required=True)
+
+ checker = GlobalObject(
+ title=u"Upgrade checker",
+ required=False)
+
+class IUpgradeStepDirective(IUpgradeStepsDirective, IUpgradeStepsStepSubDirective):
+ """
+ Define multiple upgrade steps without repeating all of the parameters
+ """
+
+
+def upgradeStep(_context, title, profile, handler, description=None, source='*',
+ destination='*', sortkey=0, checker=None):
+ step = UpgradeStep(title, profile, source, destination, description, handler,
+ checker, sortkey)
+ _context.action(
+ discriminator = ('upgradeStep', source, destination, handler, sortkey),
+ callable = _registerUpgradeStep,
+ args = (step,),
+ )
+
+class upgradeSteps(object):
+ """
+ Allows nested upgrade steps.
+ """
+ def __init__(self, _context, profile, source='*', destination='*', sortkey=0):
+ self.profile = profile
+ self.source = source
+ self.dest = destination
+ self.sortkey = sortkey
+ self.id = None
+
+ def upgradeStep(self, _context, title, description, handler, checker=None):
+ step = UpgradeStep(title, self.profile, self.source, self.dest, description,
+ handler, checker, self.sortkey)
+ if self.id is None:
+ self.id = str(abs(hash('%s%s%s%s' % (title, self.source, self.dest,
+ self.sortkey))))
+ _context.action(
+ discriminator = ('upgradeStep', self.source, self.dest, handler,
+ self.sortkey),
+ callable = _registerNestedUpgradeStep,
+ args = (step, self.id),
+ )
+
+ def __call__(self):
+ return ()
+
+
+#### cleanup
+
def cleanUp():
global _profile_regs
for profile_id in _profile_regs:
@@ -88,6 +183,9 @@
_profile_registry._profile_ids.remove(profile_id)
_profile_regs = []
+ _upgrade_registry.clear()
+
+
from zope.testing.cleanup import addCleanUp
addCleanUp(cleanUp)
del addCleanUp
More information about the Checkins
mailing list