[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} &amp;#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