[Checkins] SVN: GenericSetup/branches/wichert-events/ Cut a branch where we can experiment with events

Wichert Akkerman wichert at wiggy.net
Sun Nov 4 09:51:56 EST 2007


Log message for revision 81473:
  Cut a branch where we can experiment with events

Changed:
  A   GenericSetup/branches/wichert-events/
  D   GenericSetup/branches/wichert-events/CHANGES.txt
  A   GenericSetup/branches/wichert-events/CHANGES.txt
  D   GenericSetup/branches/wichert-events/meta.zcml
  A   GenericSetup/branches/wichert-events/meta.zcml
  D   GenericSetup/branches/wichert-events/registry.py
  A   GenericSetup/branches/wichert-events/registry.py
  A   GenericSetup/branches/wichert-events/tests/test_stepzcml.py
  D   GenericSetup/branches/wichert-events/tests/test_zcml.py
  A   GenericSetup/branches/wichert-events/tests/test_zcml.py
  D   GenericSetup/branches/wichert-events/tool.py
  A   GenericSetup/branches/wichert-events/tool.py
  D   GenericSetup/branches/wichert-events/utils.py
  A   GenericSetup/branches/wichert-events/utils.py
  D   GenericSetup/branches/wichert-events/www/sutExportSteps.zpt
  A   GenericSetup/branches/wichert-events/www/sutExportSteps.zpt
  D   GenericSetup/branches/wichert-events/www/sutImportSteps.zpt
  A   GenericSetup/branches/wichert-events/www/sutImportSteps.zpt
  D   GenericSetup/branches/wichert-events/zcml.py
  A   GenericSetup/branches/wichert-events/zcml.py

-=-
Copied: GenericSetup/branches/wichert-events (from rev 81470, GenericSetup/trunk)

Deleted: GenericSetup/branches/wichert-events/CHANGES.txt
===================================================================
--- GenericSetup/trunk/CHANGES.txt	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/CHANGES.txt	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,209 +0,0 @@
-GenericSetup Product Changelog
-
-  GenericSetup 1.3.3 (unreleased)
-
-    - tool: Fixed toolset import handler not to initialize tools again, when
-      they already exist in the site.
-
-  GenericSetup 1.3.2 (unreleased)
-
-    - Ignore import and export step handlers that we can not resolve.
-
-    - Restore the import context after running steps from a profile
-      so we do not break on nested calls.
-
-    - components: Provide log output when purging utilities or adapters.
-
-    - components: Fixed an undefined variable name in a log message.
-
-
-  GenericSetup 1.3.1 (2007/08/08)
-
-    - components: correct the object path for the site root to be the
-      empty string.
-
-    - components: Made output more diff friendly.
-
-    - utils: Added warnings to old code.
-      ImportConfiguratorBase and ExportConfiguratorBase will become deprecated
-      as soon as GenericSetup itself no longer uses them. HandlerBase is now
-      deprecated.
-
-    - components: Added 'components_xmlconfig.html' form.
-      This view allows to inspect and edit component registrations. It is also
-      available under the ZMI tab 'manage_components'.
-
-
-  GenericSetup 1.3 (2007/07/26)
-
-    - components: Removed non-functional support for registering objects in
-      nested folders. We only support objects available in the component
-      registry's parent now. The component registry needs to be either
-      acquisition wrapped or have a __parent__ pointer to get to the parent.
-
-
-  GenericSetup 1.3-beta (2007/07/12)
-
-    - Guard against situations where encoded text may be compared by the
-      differ.
-      (http://www.zope.org/Collectors/CMF/471)
-
-    - Extend the ZCatalog import/export mechanism to allow removal of 
-      metadata columns in addition to adding them.
-      (http://www.zope.org/Collectors/CMF/483)
-
-    - 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.
-
-    - testing: The test base classes no longer set up any ZCML.
-      This change is not backwards compatible. If you are using these base
-      classes for testing custom handlers, you have to add the necessary ZCML
-      setup and tear down. Using test layers is recommended.
-
-    - Added support for importing/exporting Zope 3 component registries
-      by folding in Hanno Schlichting's GSLocalAddons product.
-
-
-  GenericSetup 1.2-beta (2006/09/20)
-
-    - tool:  Added support for uploading a tarball on the "Import" tab
-      (i.e., one produced on the export tab).
-
-    - docs: Added SampleSite demo product.
-
-    - ProfileRegistry: Added 'registerProfile' ZCML directive.
-      Using the old registerProfile method in initialize() is now deprecated.
-      See doc/profiles.txt for details.
-
-    - ProfileRegistry: 'product' should now be the module name.
-      For backwards compatibility 'product' is still first looked up in
-      Products before searching the default module search path.
-
-    - ZCTextIndex handler: Fixed 'indexed_attr' import.
-      (http://www.zope.org/Collectors/CMF/436)
-
-    - docs: Added 'Registering Profiles' section to profiles.txt.
-
-    - Added support for PageTemplate import/export, modeled closely after
-      existing PythonScript support
-
-    - The dependency sorting was highly reliant on steps being added in the
-      right order to work. If import step A depends on import step B which 
-      depends on step C, and step C gets processed early, and they were 
-      processed in the order A, C, B, then the dependency order would be 
-      incorrect. This is now fixed by keeping tack of steps with unresolved
-      dependencies, and trying again after inserting everything else.
-
-  GenericSetup 1.1 (2006/04/16)
-
-    - ZCatalog handler: Implemented the 'remove' directive for indexes.
-      This allows to write extension profiles that remove or replace indexes.
-
-    - getExportStepRegistry had the wrong security declaration
-
-  GenericSetup 1.1-beta2 (2006/03/26)
-
-    No changes - tag created to coincide with CMF 2.0.0-beta2
-
-  GenericSetup 1.1-beta (2006/03/08)
-
-    - Allowed subclasses of DAVAwareFileAdapter to override the filename
-      in which the file is stored.
-
-    - Added a doc directory including some basic documentation.
-
-    - Made GenericSetup a standalone package independent of the CMF
-
-    - Added 'for_' argument to profile registry operations.
-      A profile may be registered and queried as appropriate to a specific
-      site interface;  the default value, 'None', indicates that the profile
-      is relevant to any site.  Note that this is essentially an adapter
-      lookup;  perhaps we should reimplement it so.
-
-    - Forward ported changes from GenericSetup 0.11 and 0.12 (which were
-      created in a separate repository).
-
-    - A sequence property with the purge="False" attribute will not be
-      purged, but merged (the sequences are treated as sets, which means
-      that duplicates are removed). This is useful in extension profiles.
-
-    - Don't export or purge read-only properties. Correctly purge
-      non-deletable int/float properties.
-
-    - Correctly quote XML on export.
-
-  GenericSetup 1.0 (2005/09/23)
-
-    - CVS tag:  GenericSetup-1_0
-
-    - Forward-ported i18n support from CMF 1.5 branch.
-
-    - Forward ported BBB for old instances that stored properties as
-      lists from CMFSetup.
-
-    - Forward ported fix for tools with non unique IDs from CMFSetup.
-
-  GenericSetup-0.12 (2005/08/29)
-
-    - CVS tag:  GenericSetup-0_12
-
-    - Import requests now create reports (by default) which record any
-      status messages generated by the profile's steps.
-
-  GenericSetup-0.11 (2005/08/23)
-
-    - CVS tag:  GenericSetup-0_11
-
-    - Added report of messages generated by import to the "Import" tab.
-
-    - Consolidated ISetupContext implementation into base class,
-      'SetupContextBase'.
-
-    - Added 'note', 'listNotes', and 'clearNotes'  methods to ISetupContext,
-      to allow plugins to record information about the state of the operation.
-
-  GenericSetup 0.10 (2005/08/11)
-
-    - CVS tag:  GenericSetup-0_10
-
-    - Added TarballImportContext, including full test suite.
-
-  GenericSetup 0.9 (2005/08/08)
-
-    - CVS tag:  GenericSetup-0_9
-
-    - Initial version, cut down from CMFSetup-1.5.3

Copied: GenericSetup/branches/wichert-events/CHANGES.txt (from rev 81471, GenericSetup/trunk/CHANGES.txt)
===================================================================
--- GenericSetup/branches/wichert-events/CHANGES.txt	                        (rev 0)
+++ GenericSetup/branches/wichert-events/CHANGES.txt	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,211 @@
+GenericSetup Product Changelog
+
+  GenericSetup 1.4 (unreleased)
+  
+    - Use zcml to register import and export steps.
+
+    - tool: Fixed toolset import handler not to initialize tools again, when
+      they already exist in the site.
+
+  GenericSetup 1.3.2 (unreleased)
+
+    - Ignore import and export step handlers that we can not resolve.
+
+    - Restore the import context after running steps from a profile
+      so we do not break on nested calls.
+
+    - components: Provide log output when purging utilities or adapters.
+
+    - components: Fixed an undefined variable name in a log message.
+
+
+  GenericSetup 1.3.1 (2007/08/08)
+
+    - components: correct the object path for the site root to be the
+      empty string.
+
+    - components: Made output more diff friendly.
+
+    - utils: Added warnings to old code.
+      ImportConfiguratorBase and ExportConfiguratorBase will become deprecated
+      as soon as GenericSetup itself no longer uses them. HandlerBase is now
+      deprecated.
+
+    - components: Added 'components_xmlconfig.html' form.
+      This view allows to inspect and edit component registrations. It is also
+      available under the ZMI tab 'manage_components'.
+
+
+  GenericSetup 1.3 (2007/07/26)
+
+    - components: Removed non-functional support for registering objects in
+      nested folders. We only support objects available in the component
+      registry's parent now. The component registry needs to be either
+      acquisition wrapped or have a __parent__ pointer to get to the parent.
+
+
+  GenericSetup 1.3-beta (2007/07/12)
+
+    - Guard against situations where encoded text may be compared by the
+      differ.
+      (http://www.zope.org/Collectors/CMF/471)
+
+    - Extend the ZCatalog import/export mechanism to allow removal of 
+      metadata columns in addition to adding them.
+      (http://www.zope.org/Collectors/CMF/483)
+
+    - 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.
+
+    - testing: The test base classes no longer set up any ZCML.
+      This change is not backwards compatible. If you are using these base
+      classes for testing custom handlers, you have to add the necessary ZCML
+      setup and tear down. Using test layers is recommended.
+
+    - Added support for importing/exporting Zope 3 component registries
+      by folding in Hanno Schlichting's GSLocalAddons product.
+
+
+  GenericSetup 1.2-beta (2006/09/20)
+
+    - tool:  Added support for uploading a tarball on the "Import" tab
+      (i.e., one produced on the export tab).
+
+    - docs: Added SampleSite demo product.
+
+    - ProfileRegistry: Added 'registerProfile' ZCML directive.
+      Using the old registerProfile method in initialize() is now deprecated.
+      See doc/profiles.txt for details.
+
+    - ProfileRegistry: 'product' should now be the module name.
+      For backwards compatibility 'product' is still first looked up in
+      Products before searching the default module search path.
+
+    - ZCTextIndex handler: Fixed 'indexed_attr' import.
+      (http://www.zope.org/Collectors/CMF/436)
+
+    - docs: Added 'Registering Profiles' section to profiles.txt.
+
+    - Added support for PageTemplate import/export, modeled closely after
+      existing PythonScript support
+
+    - The dependency sorting was highly reliant on steps being added in the
+      right order to work. If import step A depends on import step B which 
+      depends on step C, and step C gets processed early, and they were 
+      processed in the order A, C, B, then the dependency order would be 
+      incorrect. This is now fixed by keeping tack of steps with unresolved
+      dependencies, and trying again after inserting everything else.
+
+  GenericSetup 1.1 (2006/04/16)
+
+    - ZCatalog handler: Implemented the 'remove' directive for indexes.
+      This allows to write extension profiles that remove or replace indexes.
+
+    - getExportStepRegistry had the wrong security declaration
+
+  GenericSetup 1.1-beta2 (2006/03/26)
+
+    No changes - tag created to coincide with CMF 2.0.0-beta2
+
+  GenericSetup 1.1-beta (2006/03/08)
+
+    - Allowed subclasses of DAVAwareFileAdapter to override the filename
+      in which the file is stored.
+
+    - Added a doc directory including some basic documentation.
+
+    - Made GenericSetup a standalone package independent of the CMF
+
+    - Added 'for_' argument to profile registry operations.
+      A profile may be registered and queried as appropriate to a specific
+      site interface;  the default value, 'None', indicates that the profile
+      is relevant to any site.  Note that this is essentially an adapter
+      lookup;  perhaps we should reimplement it so.
+
+    - Forward ported changes from GenericSetup 0.11 and 0.12 (which were
+      created in a separate repository).
+
+    - A sequence property with the purge="False" attribute will not be
+      purged, but merged (the sequences are treated as sets, which means
+      that duplicates are removed). This is useful in extension profiles.
+
+    - Don't export or purge read-only properties. Correctly purge
+      non-deletable int/float properties.
+
+    - Correctly quote XML on export.
+
+  GenericSetup 1.0 (2005/09/23)
+
+    - CVS tag:  GenericSetup-1_0
+
+    - Forward-ported i18n support from CMF 1.5 branch.
+
+    - Forward ported BBB for old instances that stored properties as
+      lists from CMFSetup.
+
+    - Forward ported fix for tools with non unique IDs from CMFSetup.
+
+  GenericSetup-0.12 (2005/08/29)
+
+    - CVS tag:  GenericSetup-0_12
+
+    - Import requests now create reports (by default) which record any
+      status messages generated by the profile's steps.
+
+  GenericSetup-0.11 (2005/08/23)
+
+    - CVS tag:  GenericSetup-0_11
+
+    - Added report of messages generated by import to the "Import" tab.
+
+    - Consolidated ISetupContext implementation into base class,
+      'SetupContextBase'.
+
+    - Added 'note', 'listNotes', and 'clearNotes'  methods to ISetupContext,
+      to allow plugins to record information about the state of the operation.
+
+  GenericSetup 0.10 (2005/08/11)
+
+    - CVS tag:  GenericSetup-0_10
+
+    - Added TarballImportContext, including full test suite.
+
+  GenericSetup 0.9 (2005/08/08)
+
+    - CVS tag:  GenericSetup-0_9
+
+    - Initial version, cut down from CMFSetup-1.5.3

Deleted: GenericSetup/branches/wichert-events/meta.zcml
===================================================================
--- GenericSetup/trunk/meta.zcml	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/meta.zcml	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,35 +0,0 @@
-<configure
-    xmlns="http://namespaces.zope.org/zope"
-    xmlns:meta="http://namespaces.zope.org/meta">
-
-  <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/branches/wichert-events/meta.zcml (from rev 81471, GenericSetup/trunk/meta.zcml)
===================================================================
--- GenericSetup/branches/wichert-events/meta.zcml	                        (rev 0)
+++ GenericSetup/branches/wichert-events/meta.zcml	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,53 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta">
+
+  <meta:directives namespace="http://namespaces.zope.org/genericsetup">
+
+    <meta:directive
+        name="registerProfile"
+        schema=".zcml.IRegisterProfileDirective"
+        handler=".zcml.registerProfile"
+        />
+
+    <meta:directive
+        name="exportStep"
+        schema=".zcml.IExportStepDirective"
+        handler=".zcml.exportStep"
+        />
+    <meta:complexDirective
+        name="importStep"
+        schema=".zcml.IImportStepDirective"
+        handler=".zcml.importStep"
+        >
+
+      <meta:subdirective
+          name="depends"
+          schema=".zcml.IImportStepDependsDirective"
+          />
+
+    </meta:complexDirective>
+
+    <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>

Deleted: GenericSetup/branches/wichert-events/registry.py
===================================================================
--- GenericSetup/trunk/registry.py	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/registry.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,797 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 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.
-#
-##############################################################################
-""" Classes:  ImportStepRegistry, ExportStepRegistry
-
-$Id$
-"""
-import os
-from xml.sax import parseString
-from xml.sax.handler import ContentHandler
-
-from AccessControl import ClassSecurityInfo
-from Acquisition import Implicit
-from Globals import InitializeClass
-import App.Product
-from Products.PageTemplates.PageTemplateFile import PageTemplateFile
-from zope.interface import implements
-
-from interfaces import BASE
-from interfaces import IImportStepRegistry
-from interfaces import IExportStepRegistry
-from interfaces import IToolsetRegistry
-from interfaces import IProfileRegistry
-from permissions import ManagePortal
-from metadata import ProfileMetadata
-from utils import _xmldir
-from utils import _getDottedName
-from utils import _resolveDottedName
-from utils import _extractDocstring
-
-
-class BaseStepRegistry( Implicit ):
-
-    security = ClassSecurityInfo()
-
-    def __init__( self ):
-
-        self.clear()
-
-    security.declareProtected( ManagePortal, 'listSteps' )
-    def listSteps( self ):
-
-        """ Return a list of registered step IDs.
-        """
-        return self._registered.keys()
-
-    security.declareProtected( ManagePortal, 'getStepMetadata' )
-    def getStepMetadata( self, key, default=None ):
-
-        """ Return a mapping of metadata for the step identified by 'key'.
-
-        o Return 'default' if no such step is registered.
-
-        o The 'handler' metadata is available via 'getStep'.
-        """
-        result = {}
-
-        info = self._registered.get( key )
-
-        if info is None:
-            return default
-
-        result = info.copy()
-        result['invalid'] =  _resolveDottedName( result[ 'handler' ] ) is None
-
-        return result
-
-    security.declareProtected( ManagePortal, 'listStepMetadata' )
-    def listStepMetadata( self ):
-
-        """ Return a sequence of mappings describing registered steps.
-
-        o Mappings will be ordered alphabetically.
-        """
-        step_ids = self.listSteps()
-        step_ids.sort()
-        return [ self.getStepMetadata( x ) for x in step_ids ]
-
-    security.declareProtected( ManagePortal, 'generateXML' )
-    def generateXML( self ):
-
-        """ Return a round-trippable XML representation of the registry.
-
-        o 'handler' values are serialized using their dotted names.
-        """
-        return self._exportTemplate()
-
-    security.declarePrivate( 'getStep' )
-    def getStep( self, key, default=None ):
-
-        """ Return the I(Export|Import)Plugin registered for 'key'.
-
-        o Return 'default' if no such step is registered.
-        """
-        marker = object()
-        info = self._registered.get( key, marker )
-
-        if info is marker:
-            return default
-
-        return _resolveDottedName( info[ 'handler' ] )
-
-    security.declarePrivate( 'clear' )
-    def clear( self ):
-
-        self._registered = {}
-
-InitializeClass( BaseStepRegistry )
-
-class ImportStepRegistry( BaseStepRegistry ):
-
-    """ Manage knowledge about steps to create / configure site.
-
-    o Steps are composed together to define a site profile.
-    """
-    implements(IImportStepRegistry)
-
-    security = ClassSecurityInfo()
-
-
-    security.declareProtected( ManagePortal, 'sortSteps' )
-    def sortSteps( self ):
-
-        """ Return a sequence of registered step IDs
-
-        o Sequence is sorted topologically by dependency, with the dependent
-          steps *after* the steps they depend on.
-        """
-        return self._computeTopologicalSort()
-
-    security.declareProtected( ManagePortal, 'checkComplete' )
-    def checkComplete( self ):
-
-        """ Return a sequence of ( node, edge ) tuples for unsatisifed deps.
-        """
-        result = []
-        seen = {}
-
-        graph = self._computeTopologicalSort()
-
-        for node in graph:
-
-            dependencies = self.getStepMetadata( node )[ 'dependencies' ]
-
-            for dependency in dependencies:
-
-                if seen.get( dependency ) is None:
-                    result.append( ( node, dependency ) )
-
-            seen[ node ] = 1
-
-        return result
-
-    security.declarePrivate( 'registerStep' )
-    def registerStep( self
-                    , id
-                    , version
-                    , handler
-                    , dependencies=()
-                    , title=None
-                    , description=None
-                    ):
-        """ Register a setup step.
-
-        o 'id' is a unique name for this step,
-
-        o 'version' is a string for comparing versions, it is preferred to
-          be a yyyy/mm/dd-ii formatted string (date plus two-digit
-          ordinal).  when comparing two version strings, the version with
-          the lower sort order is considered the older version.
-
-          - Newer versions of a step supplant older ones.
-
-          - Attempting to register an older one after a newer one results
-            in a KeyError.
-
-        o 'handler' is the dottoed name of a handler which should implement
-           IImportPlugin.
-
-        o 'dependencies' is a tuple of step ids which have to run before
-          this step in order to be able to run at all. Registration of
-          steps that have unmet dependencies are deferred until the
-          dependencies have been registered.
-
-        o 'title' is a one-line UI description for this step.
-          If None, the first line of the documentation string of the handler
-          is used, or the id if no docstring can be found.
-
-        o 'description' is a one-line UI description for this step.
-          If None, the remaining line of the documentation string of
-          the handler is used, or default to ''.
-        """
-        already = self.getStepMetadata( id )
-
-        if already and already[ 'version' ] > version:
-            raise KeyError( 'Existing registration for step %s, version %s'
-                          % ( id, already[ 'version' ] ) )
-
-        if not isinstance(handler, str):
-            handler = _getDottedName( handler )
-
-        if title is None or description is None:
-
-            method = _resolveDottedName(handler)
-            if method is None:
-                t,d = id, ''
-            else:
-                t, d = _extractDocstring( method, id, '' )
-
-            title = title or t
-            description = description or d
-
-        info = { 'id'           : id
-               , 'version'      : version
-               , 'handler'      : handler
-               , 'dependencies' : dependencies
-               , 'title'        : title
-               , 'description'  : description
-               }
-
-        self._registered[ id ] = info
-
-    security.declarePrivate( 'parseXML' )
-    def parseXML( self, text, encoding=None ):
-
-        """ Parse 'text'.
-        """
-        reader = getattr( text, 'read', None )
-
-        if reader is not None:
-            text = reader()
-
-        parser = _ImportStepRegistryParser( encoding )
-        parseString( text, parser )
-
-        return parser._parsed
-
-    #
-    #   Helper methods
-    #
-    security.declarePrivate( '_computeTopologicalSort' )
-    def _computeTopologicalSort( self ):
-
-        result = []
-
-        graph = [ ( x[ 'id' ], x[ 'dependencies' ] )
-                    for x in self._registered.values() ]
-
-        unresolved = []
-
-        while 1:
-            for node, edges in graph:
-    
-                after = -1
-                resolved = 0
-    
-                for edge in edges:
-    
-                    if edge in result:
-                        resolved += 1
-                        after = max( after, result.index( edge ) )
-                
-                if len(edges) > resolved:
-                    unresolved.append((node, edges))
-                else:
-                    result.insert( after + 1, node )
-
-            if not unresolved:
-                break
-            if len(unresolved) == len(graph):
-                # Nothing was resolved in this loop. There must be circular or
-                # missing dependencies. Just add them to the end. We can't
-                # raise an error, because checkComplete relies on this method.
-                for node, edges in unresolved:
-                    result.append(node)
-                break
-            graph = unresolved
-            unresolved = []
-        
-        return result
-
-    security.declarePrivate( '_exportTemplate' )
-    _exportTemplate = PageTemplateFile( 'isrExport.xml', _xmldir )
-
-InitializeClass( ImportStepRegistry )
-
-
-class ExportStepRegistry( BaseStepRegistry ):
-
-    """ Registry of known site-configuration export steps.
-
-    o Each step is registered with a unique id.
-
-    o When called, with the portal object passed in as an argument,
-      the step must return a sequence of three-tuples,
-      ( 'data', 'content_type', 'filename' ), one for each file exported
-      by the step.
-
-      - 'data' is a string containing the file data;
-
-      - 'content_type' is the MIME type of the data;
-
-      - 'filename' is a suggested filename for use when downloading.
-
-    """
-    implements(IExportStepRegistry)
-
-    security = ClassSecurityInfo()
-
-    security.declarePrivate( 'registerStep' )
-    def registerStep( self, id, handler, title=None, description=None ):
-
-        """ Register an export step.
-
-        o 'id' is the unique identifier for this step
-
-        o 'handler' is the dottoed name of a handler which should implement
-           IImportPlugin.
-
-        o 'title' is a one-line UI description for this step.
-          If None, the first line of the documentation string of the step
-          is used, or the id if no docstring can be found.
-
-        o 'description' is a one-line UI description for this step.
-          If None, the remaining line of the documentation string of
-          the step is used, or default to ''.
-        """
-        if not isinstance(handler, str):
-            handler = _getDottedName( handler )
-
-        if title is None or description is None:
-
-            method = _resolveDottedName(handler)
-            if method is None:
-                t,d = id, ''
-            else:
-                t, d = _extractDocstring( method, id, '' )
-
-            title = title or t
-            description = description or d
-
-        info = { 'id'           : id
-               , 'handler'      : handler
-               , 'title'        : title
-               , 'description'  : description
-               }
-
-        self._registered[ id ] = info
-
-    security.declarePrivate( 'parseXML' )
-    def parseXML( self, text, encoding=None ):
-
-        """ Parse 'text'.
-        """
-        reader = getattr( text, 'read', None )
-
-        if reader is not None:
-            text = reader()
-
-        parser = _ExportStepRegistryParser( encoding )
-        parseString( text, parser )
-
-        return parser._parsed
-
-    #
-    #   Helper methods
-    #
-    security.declarePrivate( '_exportTemplate' )
-    _exportTemplate = PageTemplateFile( 'esrExport.xml', _xmldir )
-
-InitializeClass( ExportStepRegistry )
-
-class ToolsetRegistry( Implicit ):
-
-    """ Track required / forbidden tools.
-    """
-    implements(IToolsetRegistry)
-
-    security = ClassSecurityInfo()
-    security.setDefaultAccess( 'allow' )
-
-    def __init__( self ):
-
-        self.clear()
-
-    #
-    #   Toolset API
-    #
-    security.declareProtected( ManagePortal, 'listForbiddenTools' )
-    def listForbiddenTools( self ):
-
-        """ See IToolsetRegistry.
-        """
-        result = list( self._forbidden )
-        result.sort()
-        return result
-
-    security.declareProtected( ManagePortal, 'addForbiddenTool' )
-    def addForbiddenTool( self, tool_id ):
-
-        """ See IToolsetRegistry.
-        """
-        if tool_id in self._forbidden:
-            return
-
-        if self._required.get( tool_id ) is not None:
-            raise ValueError, 'Tool %s is required!' % tool_id
-
-        self._forbidden.append( tool_id )
-
-    security.declareProtected( ManagePortal, 'listRequiredTools' )
-    def listRequiredTools( self ):
-
-        """ See IToolsetRegistry.
-        """
-        result = list( self._required.keys() )
-        result.sort()
-        return result
-
-    security.declareProtected( ManagePortal, 'getRequiredToolInfo' )
-    def getRequiredToolInfo( self, tool_id ):
-
-        """ See IToolsetRegistry.
-        """
-        return self._required[ tool_id ]
-
-    security.declareProtected( ManagePortal, 'listRequiredToolInfo' )
-    def listRequiredToolInfo( self ):
-
-        """ See IToolsetRegistry.
-        """
-        return [ self.getRequiredToolInfo( x )
-                        for x in self.listRequiredTools() ]
-
-    security.declareProtected( ManagePortal, 'addRequiredTool' )
-    def addRequiredTool( self, tool_id, dotted_name ):
-
-        """ See IToolsetRegistry.
-        """
-        if tool_id in self._forbidden:
-            raise ValueError, "Forbidden tool ID: %s" % tool_id
-
-        self._required[ tool_id ] = { 'id' : tool_id
-                                    , 'class' : dotted_name
-                                    }
-
-    security.declareProtected( ManagePortal, 'generateXML' )
-    def generateXML( self ):
-
-        """ Pseudo API.
-        """
-        return self._toolsetConfig()
-
-    security.declareProtected( ManagePortal, 'parseXML' )
-    def parseXML( self, text, encoding=None ):
-
-        """ Pseudo-API
-        """
-        reader = getattr( text, 'read', None )
-
-        if reader is not None:
-            text = reader()
-
-        parser = _ToolsetParser( encoding )
-        parseString( text, parser )
-
-        for tool_id in parser._forbidden:
-            self.addForbiddenTool( tool_id )
-
-        for tool_id, dotted_name in parser._required.items():
-            self.addRequiredTool( tool_id, dotted_name )
-
-    security.declarePrivate( 'clear' )
-    def clear( self ):
-
-        self._forbidden = []
-        self._required = {}
-
-    #
-    #   Helper methods.
-    #
-    security.declarePrivate( '_toolsetConfig' )
-    _toolsetConfig = PageTemplateFile( 'tscExport.xml'
-                                     , _xmldir
-                                     , __name__='toolsetConfig'
-                                     )
-
-InitializeClass( ToolsetRegistry )
-
-
-class ProfileRegistry( Implicit ):
-
-    """ Track registered profiles.
-    """
-    implements(IProfileRegistry)
-
-    security = ClassSecurityInfo()
-    security.setDefaultAccess( 'allow' )
-
-    def __init__( self ):
-
-        self.clear()
-
-    security.declareProtected( ManagePortal, 'getProfileInfo' )
-    def getProfileInfo( self, profile_id, for_=None ):
-
-        """ See IProfileRegistry.
-        """
-        result = self._profile_info[ profile_id ]
-        if for_ is not None:
-            if not issubclass( for_, result['for'] ):
-                raise KeyError, profile_id
-        return result.copy()
-
-    security.declareProtected( ManagePortal, 'listProfiles' )
-    def listProfiles( self, for_=None ):
-
-        """ See IProfileRegistry.
-        """
-        result = []
-        for profile_id in self._profile_ids:
-            info = self.getProfileInfo( profile_id )
-            if for_ is None or issubclass( for_, info['for'] ):
-                result.append( profile_id )
-        return tuple( result )
-
-    security.declareProtected( ManagePortal, 'listProfileInfo' )
-    def listProfileInfo( self, for_=None ):
-
-        """ See IProfileRegistry.
-        """
-        candidates = [ self.getProfileInfo( id )
-                        for id in self.listProfiles() ]
-        return [ x for x in candidates if for_ is None or x['for'] is None or
-                 issubclass( for_, x['for'] ) ]
-
-    security.declareProtected( ManagePortal, 'registerProfile' )
-    def registerProfile( self
-                       , name
-                       , title
-                       , description
-                       , path
-                       , product=None
-                       , profile_type=BASE
-                       , for_=None
-                       ):
-        """ See IProfileRegistry.
-        """
-        profile_id = '%s:%s' % (product or 'other', name)
-        if self._profile_info.get( profile_id ) is not None:
-            raise KeyError, 'Duplicate profile ID: %s' % profile_id
-
-        self._profile_ids.append( profile_id )
-
-        info = { 'id' : profile_id
-               , 'title' : title
-               , 'description' : description
-               , 'path' : path
-               , 'product' : product
-               , 'type': profile_type
-               , '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' )
-    def clear( self ):
-
-        self._profile_info = {}
-        self._profile_ids = []
-
-InitializeClass( ProfileRegistry )
-
-_profile_registry = ProfileRegistry()
-
-
-#
-#   XML parser
-#
-
-class _HandlerBase(ContentHandler):
-
-    _MARKER = object()
-
-    def _extract(self, attrs, key):
-        result = attrs.get(key, self._MARKER)
-
-        if result is self._MARKER:
-            return None
-
-        return self._encode(result)
-
-    def _encode(self, content):
-        if self._encoding is None:
-            return content
-
-        return content.encode(self._encoding)
-
-
-class _ImportStepRegistryParser(_HandlerBase):
-
-    security = ClassSecurityInfo()
-    security.declareObjectPrivate()
-    security.setDefaultAccess( 'deny' )
-
-    def __init__( self, encoding ):
-
-        self._encoding = encoding
-        self._started = False
-        self._pending = None
-        self._parsed = []
-
-    def startElement( self, name, attrs ):
-
-        if name == 'import-steps':
-
-            if self._started:
-                raise ValueError, 'Duplicated setup-steps element: %s' % name
-
-            self._started = True
-
-        elif name == 'import-step':
-
-            if self._pending is not None:
-                raise ValueError, 'Cannot nest setup-step elements'
-
-            self._pending = dict( [ ( k, self._extract( attrs, k ) )
-                                    for k in attrs.keys() ] )
-
-            self._pending[ 'dependencies' ] = []
-
-        elif name == 'dependency':
-
-            if not self._pending:
-                raise ValueError, 'Dependency outside of step'
-
-            depended = self._extract( attrs, 'step' )
-            self._pending[ 'dependencies' ].append( depended )
-
-        else:
-            raise ValueError, 'Unknown element %s' % name
-
-    def characters( self, content ):
-
-        if self._pending is not None:
-            content = self._encode( content )
-            self._pending.setdefault( 'description', [] ).append( content )
-
-    def endElement(self, name):
-
-        if name == 'import-steps':
-            pass
-
-        elif name == 'import-step':
-
-            if self._pending is None:
-                raise ValueError, 'No pending step!'
-
-            deps = tuple( self._pending[ 'dependencies' ] )
-            self._pending[ 'dependencies' ] = deps
-
-            desc = ''.join( self._pending[ 'description' ] )
-            self._pending[ 'description' ] = desc
-
-            self._parsed.append( self._pending )
-            self._pending = None
-
-InitializeClass( _ImportStepRegistryParser )
-
-
-class _ExportStepRegistryParser(_HandlerBase):
-
-    security = ClassSecurityInfo()
-    security.declareObjectPrivate()
-    security.setDefaultAccess( 'deny' )
-
-    def __init__( self, encoding ):
-
-        self._encoding = encoding
-        self._started = False
-        self._pending = None
-        self._parsed = []
-
-    def startElement( self, name, attrs ):
-
-        if name == 'export-steps':
-
-            if self._started:
-                raise ValueError, 'Duplicated export-steps element: %s' % name
-
-            self._started = True
-
-        elif name == 'export-step':
-
-            if self._pending is not None:
-                raise ValueError, 'Cannot nest export-step elements'
-
-            self._pending = dict( [ ( k, self._extract( attrs, k ) )
-                                    for k in attrs.keys() ] )
-
-        else:
-            raise ValueError, 'Unknown element %s' % name
-
-    def characters( self, content ):
-
-        if self._pending is not None:
-            content = self._encode( content )
-            self._pending.setdefault( 'description', [] ).append( content )
-
-    def endElement(self, name):
-
-        if name == 'export-steps':
-            pass
-
-        elif name == 'export-step':
-
-            if self._pending is None:
-                raise ValueError, 'No pending step!'
-
-            desc = ''.join( self._pending[ 'description' ] )
-            self._pending[ 'description' ] = desc
-
-            self._parsed.append( self._pending )
-            self._pending = None
-
-InitializeClass( _ExportStepRegistryParser )
-
-
-class _ToolsetParser(_HandlerBase):
-
-    security = ClassSecurityInfo()
-    security.declareObjectPrivate()
-    security.setDefaultAccess( 'deny' )
-
-    def __init__( self, encoding ):
-
-        self._encoding = encoding
-        self._required = {}
-        self._forbidden = []
-
-    def startElement( self, name, attrs ):
-
-        if name == 'tool-setup':
-            pass
-
-        elif name == 'forbidden':
-
-            tool_id = self._extract( attrs, 'tool_id' )
-
-            if tool_id not in self._forbidden:
-                self._forbidden.append( tool_id )
-
-        elif name == 'required':
-
-            tool_id = self._extract( attrs, 'tool_id' )
-            dotted_name = self._extract( attrs, 'class' )
-            self._required[ tool_id ] = dotted_name
-
-        else:
-            raise ValueError, 'Unknown element %s' % name
-
-InitializeClass( _ToolsetParser )

Copied: GenericSetup/branches/wichert-events/registry.py (from rev 81471, GenericSetup/trunk/registry.py)
===================================================================
--- GenericSetup/branches/wichert-events/registry.py	                        (rev 0)
+++ GenericSetup/branches/wichert-events/registry.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,770 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+""" Classes:  ImportStepRegistry, ExportStepRegistry
+
+$Id$
+"""
+import os
+from xml.sax import parseString
+from xml.sax.handler import ContentHandler
+
+from AccessControl import ClassSecurityInfo
+from Acquisition import Implicit
+from Globals import InitializeClass
+import App.Product
+from Products.PageTemplates.PageTemplateFile import PageTemplateFile
+from zope.interface import implements
+
+from interfaces import BASE
+from interfaces import IImportStepRegistry
+from interfaces import IExportStepRegistry
+from interfaces import IToolsetRegistry
+from interfaces import IProfileRegistry
+from permissions import ManagePortal
+from metadata import ProfileMetadata
+from utils import _xmldir
+from utils import _getDottedName
+from utils import _resolveDottedName
+from utils import _extractDocstring
+from utils import _computeTopologicalSort
+
+
+class BaseStepRegistry( Implicit ):
+
+    security = ClassSecurityInfo()
+
+    def __init__( self ):
+
+        self.clear()
+
+    security.declareProtected( ManagePortal, 'listSteps' )
+    def listSteps( self ):
+
+        """ Return a list of registered step IDs.
+        """
+        return self._registered.keys()
+
+    security.declareProtected( ManagePortal, 'getStepMetadata' )
+    def getStepMetadata( self, key, default=None ):
+
+        """ Return a mapping of metadata for the step identified by 'key'.
+
+        o Return 'default' if no such step is registered.
+
+        o The 'handler' metadata is available via 'getStep'.
+        """
+        result = {}
+
+        info = self._registered.get( key )
+
+        if info is None:
+            return default
+
+        result = info.copy()
+        result['invalid'] =  _resolveDottedName( result[ 'handler' ] ) is None
+
+        return result
+
+    security.declareProtected( ManagePortal, 'listStepMetadata' )
+    def listStepMetadata( self ):
+
+        """ Return a sequence of mappings describing registered steps.
+
+        o Mappings will be ordered alphabetically.
+        """
+        step_ids = self.listSteps()
+        step_ids.sort()
+        return [ self.getStepMetadata( x ) for x in step_ids ]
+
+    security.declareProtected( ManagePortal, 'generateXML' )
+    def generateXML( self ):
+
+        """ Return a round-trippable XML representation of the registry.
+
+        o 'handler' values are serialized using their dotted names.
+        """
+        return self._exportTemplate()
+
+    security.declarePrivate( 'getStep' )
+    def getStep( self, key, default=None ):
+
+        """ Return the I(Export|Import)Plugin registered for 'key'.
+
+        o Return 'default' if no such step is registered.
+        """
+        marker = object()
+        info = self._registered.get( key, marker )
+
+        if info is marker:
+            return default
+
+        return _resolveDottedName( info[ 'handler' ] )
+
+    security.declarePrivate( 'unregisterStep' )
+    def unregisterStep( self, id ):
+        del self._registered[id]
+
+    security.declarePrivate( 'clear' )
+    def clear( self ):
+
+        self._registered = {}
+
+InitializeClass( BaseStepRegistry )
+
+class ImportStepRegistry( BaseStepRegistry ):
+
+    """ Manage knowledge about steps to create / configure site.
+
+    o Steps are composed together to define a site profile.
+    """
+    implements(IImportStepRegistry)
+
+    security = ClassSecurityInfo()
+
+
+    security.declareProtected( ManagePortal, 'sortSteps' )
+    def sortSteps( self ):
+
+        """ Return a sequence of registered step IDs
+
+        o Sequence is sorted topologically by dependency, with the dependent
+          steps *after* the steps they depend on.
+        """
+        return self._computeTopologicalSort()
+
+    security.declareProtected( ManagePortal, 'checkComplete' )
+    def checkComplete( self ):
+
+        """ Return a sequence of ( node, edge ) tuples for unsatisifed deps.
+        """
+        result = []
+        seen = {}
+
+        graph = self._computeTopologicalSort()
+
+        for node in graph:
+
+            dependencies = self.getStepMetadata( node )[ 'dependencies' ]
+
+            for dependency in dependencies:
+
+                if seen.get( dependency ) is None:
+                    result.append( ( node, dependency ) )
+
+            seen[ node ] = 1
+
+        return result
+
+    security.declarePrivate( 'registerStep' )
+    def registerStep( self
+                    , id
+                    , version
+                    , handler
+                    , dependencies=()
+                    , title=None
+                    , description=None
+                    ):
+        """ Register a setup step.
+
+        o 'id' is a unique name for this step,
+
+        o 'version' is a string for comparing versions, it is preferred to
+          be a yyyy/mm/dd-ii formatted string (date plus two-digit
+          ordinal).  when comparing two version strings, the version with
+          the lower sort order is considered the older version.
+
+          - Newer versions of a step supplant older ones.
+
+          - Attempting to register an older one after a newer one results
+            in a KeyError.
+
+        o 'handler' is the dottoed name of a handler which should implement
+           IImportPlugin.
+
+        o 'dependencies' is a tuple of step ids which have to run before
+          this step in order to be able to run at all. Registration of
+          steps that have unmet dependencies are deferred until the
+          dependencies have been registered.
+
+        o 'title' is a one-line UI description for this step.
+          If None, the first line of the documentation string of the handler
+          is used, or the id if no docstring can be found.
+
+        o 'description' is a one-line UI description for this step.
+          If None, the remaining line of the documentation string of
+          the handler is used, or default to ''.
+        """
+        already = self.getStepMetadata( id )
+
+        if already and already[ 'version' ] > version:
+            raise KeyError( 'Existing registration for step %s, version %s'
+                          % ( id, already[ 'version' ] ) )
+
+        if not isinstance(handler, str):
+            handler = _getDottedName( handler )
+
+        if title is None or description is None:
+
+            method = _resolveDottedName(handler)
+            if method is None:
+                t,d = id, ''
+            else:
+                t, d = _extractDocstring( method, id, '' )
+
+            title = title or t
+            description = description or d
+
+        info = { 'id'           : id
+               , 'version'      : version
+               , 'handler'      : handler
+               , 'dependencies' : dependencies
+               , 'title'        : title
+               , 'description'  : description
+               }
+
+        self._registered[ id ] = info
+
+    security.declarePrivate( 'parseXML' )
+    def parseXML( self, text, encoding=None ):
+
+        """ Parse 'text'.
+        """
+        reader = getattr( text, 'read', None )
+
+        if reader is not None:
+            text = reader()
+
+        parser = _ImportStepRegistryParser( encoding )
+        parseString( text, parser )
+
+        return parser._parsed
+
+    #
+    #   Helper methods
+    #
+    security.declarePrivate( '_computeTopologicalSort' )
+    def _computeTopologicalSort( self ):
+        return _computeTopologicalSort(self._registered.values())
+
+
+    security.declarePrivate( '_exportTemplate' )
+    _exportTemplate = PageTemplateFile( 'isrExport.xml', _xmldir )
+
+InitializeClass( ImportStepRegistry )
+
+_import_step_registry = ImportStepRegistry()
+
+class ExportStepRegistry( BaseStepRegistry ):
+
+    """ Registry of known site-configuration export steps.
+
+    o Each step is registered with a unique id.
+
+    o When called, with the portal object passed in as an argument,
+      the step must return a sequence of three-tuples,
+      ( 'data', 'content_type', 'filename' ), one for each file exported
+      by the step.
+
+      - 'data' is a string containing the file data;
+
+      - 'content_type' is the MIME type of the data;
+
+      - 'filename' is a suggested filename for use when downloading.
+
+    """
+    implements(IExportStepRegistry)
+
+    security = ClassSecurityInfo()
+
+    security.declarePrivate( 'registerStep' )
+    def registerStep( self, id, handler, title=None, description=None ):
+
+        """ Register an export step.
+
+        o 'id' is the unique identifier for this step
+
+        o 'handler' is the dottoed name of a handler which should implement
+           IImportPlugin.
+
+        o 'title' is a one-line UI description for this step.
+          If None, the first line of the documentation string of the step
+          is used, or the id if no docstring can be found.
+
+        o 'description' is a one-line UI description for this step.
+          If None, the remaining line of the documentation string of
+          the step is used, or default to ''.
+        """
+        if not isinstance(handler, str):
+            handler = _getDottedName( handler )
+
+        if title is None or description is None:
+
+            method = _resolveDottedName(handler)
+            if method is None:
+                t,d = id, ''
+            else:
+                t, d = _extractDocstring( method, id, '' )
+
+            title = title or t
+            description = description or d
+
+        info = { 'id'           : id
+               , 'handler'      : handler
+               , 'title'        : title
+               , 'description'  : description
+               }
+
+        self._registered[ id ] = info
+
+    security.declarePrivate( 'parseXML' )
+    def parseXML( self, text, encoding=None ):
+
+        """ Parse 'text'.
+        """
+        reader = getattr( text, 'read', None )
+
+        if reader is not None:
+            text = reader()
+
+        parser = _ExportStepRegistryParser( encoding )
+        parseString( text, parser )
+
+        return parser._parsed
+
+    #
+    #   Helper methods
+    #
+    security.declarePrivate( '_exportTemplate' )
+    _exportTemplate = PageTemplateFile( 'esrExport.xml', _xmldir )
+
+InitializeClass( ExportStepRegistry )
+
+_export_step_registry = ExportStepRegistry()
+
+
+class ToolsetRegistry( Implicit ):
+
+    """ Track required / forbidden tools.
+    """
+    implements(IToolsetRegistry)
+
+    security = ClassSecurityInfo()
+    security.setDefaultAccess( 'allow' )
+
+    def __init__( self ):
+
+        self.clear()
+
+    #
+    #   Toolset API
+    #
+    security.declareProtected( ManagePortal, 'listForbiddenTools' )
+    def listForbiddenTools( self ):
+
+        """ See IToolsetRegistry.
+        """
+        result = list( self._forbidden )
+        result.sort()
+        return result
+
+    security.declareProtected( ManagePortal, 'addForbiddenTool' )
+    def addForbiddenTool( self, tool_id ):
+
+        """ See IToolsetRegistry.
+        """
+        if tool_id in self._forbidden:
+            return
+
+        if self._required.get( tool_id ) is not None:
+            raise ValueError, 'Tool %s is required!' % tool_id
+
+        self._forbidden.append( tool_id )
+
+    security.declareProtected( ManagePortal, 'listRequiredTools' )
+    def listRequiredTools( self ):
+
+        """ See IToolsetRegistry.
+        """
+        result = list( self._required.keys() )
+        result.sort()
+        return result
+
+    security.declareProtected( ManagePortal, 'getRequiredToolInfo' )
+    def getRequiredToolInfo( self, tool_id ):
+
+        """ See IToolsetRegistry.
+        """
+        return self._required[ tool_id ]
+
+    security.declareProtected( ManagePortal, 'listRequiredToolInfo' )
+    def listRequiredToolInfo( self ):
+
+        """ See IToolsetRegistry.
+        """
+        return [ self.getRequiredToolInfo( x )
+                        for x in self.listRequiredTools() ]
+
+    security.declareProtected( ManagePortal, 'addRequiredTool' )
+    def addRequiredTool( self, tool_id, dotted_name ):
+
+        """ See IToolsetRegistry.
+        """
+        if tool_id in self._forbidden:
+            raise ValueError, "Forbidden tool ID: %s" % tool_id
+
+        self._required[ tool_id ] = { 'id' : tool_id
+                                    , 'class' : dotted_name
+                                    }
+
+    security.declareProtected( ManagePortal, 'generateXML' )
+    def generateXML( self ):
+
+        """ Pseudo API.
+        """
+        return self._toolsetConfig()
+
+    security.declareProtected( ManagePortal, 'parseXML' )
+    def parseXML( self, text, encoding=None ):
+
+        """ Pseudo-API
+        """
+        reader = getattr( text, 'read', None )
+
+        if reader is not None:
+            text = reader()
+
+        parser = _ToolsetParser( encoding )
+        parseString( text, parser )
+
+        for tool_id in parser._forbidden:
+            self.addForbiddenTool( tool_id )
+
+        for tool_id, dotted_name in parser._required.items():
+            self.addRequiredTool( tool_id, dotted_name )
+
+    security.declarePrivate( 'clear' )
+    def clear( self ):
+
+        self._forbidden = []
+        self._required = {}
+
+    #
+    #   Helper methods.
+    #
+    security.declarePrivate( '_toolsetConfig' )
+    _toolsetConfig = PageTemplateFile( 'tscExport.xml'
+                                     , _xmldir
+                                     , __name__='toolsetConfig'
+                                     )
+
+InitializeClass( ToolsetRegistry )
+
+
+class ProfileRegistry( Implicit ):
+
+    """ Track registered profiles.
+    """
+    implements(IProfileRegistry)
+
+    security = ClassSecurityInfo()
+    security.setDefaultAccess( 'allow' )
+
+    def __init__( self ):
+
+        self.clear()
+
+    security.declareProtected( ManagePortal, 'getProfileInfo' )
+    def getProfileInfo( self, profile_id, for_=None ):
+
+        """ See IProfileRegistry.
+        """
+        result = self._profile_info[ profile_id ]
+        if for_ is not None:
+            if not issubclass( for_, result['for'] ):
+                raise KeyError, profile_id
+        return result.copy()
+
+    security.declareProtected( ManagePortal, 'listProfiles' )
+    def listProfiles( self, for_=None ):
+
+        """ See IProfileRegistry.
+        """
+        result = []
+        for profile_id in self._profile_ids:
+            info = self.getProfileInfo( profile_id )
+            if for_ is None or issubclass( for_, info['for'] ):
+                result.append( profile_id )
+        return tuple( result )
+
+    security.declareProtected( ManagePortal, 'listProfileInfo' )
+    def listProfileInfo( self, for_=None ):
+
+        """ See IProfileRegistry.
+        """
+        candidates = [ self.getProfileInfo( id )
+                        for id in self.listProfiles() ]
+        return [ x for x in candidates if for_ is None or x['for'] is None or
+                 issubclass( for_, x['for'] ) ]
+
+    security.declareProtected( ManagePortal, 'registerProfile' )
+    def registerProfile( self
+                       , name
+                       , title
+                       , description
+                       , path
+                       , product=None
+                       , profile_type=BASE
+                       , for_=None
+                       ):
+        """ See IProfileRegistry.
+        """
+        profile_id = '%s:%s' % (product or 'other', name)
+        if self._profile_info.get( profile_id ) is not None:
+            raise KeyError, 'Duplicate profile ID: %s' % profile_id
+
+        self._profile_ids.append( profile_id )
+
+        info = { 'id' : profile_id
+               , 'title' : title
+               , 'description' : description
+               , 'path' : path
+               , 'product' : product
+               , 'type': profile_type
+               , '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' )
+    def clear( self ):
+
+        self._profile_info = {}
+        self._profile_ids = []
+
+InitializeClass( ProfileRegistry )
+
+_profile_registry = ProfileRegistry()
+
+
+#
+#   XML parser
+#
+
+class _HandlerBase(ContentHandler):
+
+    _MARKER = object()
+
+    def _extract(self, attrs, key):
+        result = attrs.get(key, self._MARKER)
+
+        if result is self._MARKER:
+            return None
+
+        return self._encode(result)
+
+    def _encode(self, content):
+        if self._encoding is None:
+            return content
+
+        return content.encode(self._encoding)
+
+
+class _ImportStepRegistryParser(_HandlerBase):
+
+    security = ClassSecurityInfo()
+    security.declareObjectPrivate()
+    security.setDefaultAccess( 'deny' )
+
+    def __init__( self, encoding ):
+
+        self._encoding = encoding
+        self._started = False
+        self._pending = None
+        self._parsed = []
+
+    def startElement( self, name, attrs ):
+
+        if name == 'import-steps':
+
+            if self._started:
+                raise ValueError, 'Duplicated setup-steps element: %s' % name
+
+            self._started = True
+
+        elif name == 'import-step':
+
+            if self._pending is not None:
+                raise ValueError, 'Cannot nest setup-step elements'
+
+            self._pending = dict( [ ( k, self._extract( attrs, k ) )
+                                    for k in attrs.keys() ] )
+
+            self._pending[ 'dependencies' ] = []
+
+        elif name == 'dependency':
+
+            if not self._pending:
+                raise ValueError, 'Dependency outside of step'
+
+            depended = self._extract( attrs, 'step' )
+            self._pending[ 'dependencies' ].append( depended )
+
+        else:
+            raise ValueError, 'Unknown element %s' % name
+
+    def characters( self, content ):
+
+        if self._pending is not None:
+            content = self._encode( content )
+            self._pending.setdefault( 'description', [] ).append( content )
+
+    def endElement(self, name):
+
+        if name == 'import-steps':
+            pass
+
+        elif name == 'import-step':
+
+            if self._pending is None:
+                raise ValueError, 'No pending step!'
+
+            deps = tuple( self._pending[ 'dependencies' ] )
+            self._pending[ 'dependencies' ] = deps
+
+            desc = ''.join( self._pending[ 'description' ] )
+            self._pending[ 'description' ] = desc
+
+            self._parsed.append( self._pending )
+            self._pending = None
+
+InitializeClass( _ImportStepRegistryParser )
+
+
+class _ExportStepRegistryParser(_HandlerBase):
+
+    security = ClassSecurityInfo()
+    security.declareObjectPrivate()
+    security.setDefaultAccess( 'deny' )
+
+    def __init__( self, encoding ):
+
+        self._encoding = encoding
+        self._started = False
+        self._pending = None
+        self._parsed = []
+
+    def startElement( self, name, attrs ):
+
+        if name == 'export-steps':
+
+            if self._started:
+                raise ValueError, 'Duplicated export-steps element: %s' % name
+
+            self._started = True
+
+        elif name == 'export-step':
+
+            if self._pending is not None:
+                raise ValueError, 'Cannot nest export-step elements'
+
+            self._pending = dict( [ ( k, self._extract( attrs, k ) )
+                                    for k in attrs.keys() ] )
+
+        else:
+            raise ValueError, 'Unknown element %s' % name
+
+    def characters( self, content ):
+
+        if self._pending is not None:
+            content = self._encode( content )
+            self._pending.setdefault( 'description', [] ).append( content )
+
+    def endElement(self, name):
+
+        if name == 'export-steps':
+            pass
+
+        elif name == 'export-step':
+
+            if self._pending is None:
+                raise ValueError, 'No pending step!'
+
+            desc = ''.join( self._pending[ 'description' ] )
+            self._pending[ 'description' ] = desc
+
+            self._parsed.append( self._pending )
+            self._pending = None
+
+InitializeClass( _ExportStepRegistryParser )
+
+
+class _ToolsetParser(_HandlerBase):
+
+    security = ClassSecurityInfo()
+    security.declareObjectPrivate()
+    security.setDefaultAccess( 'deny' )
+
+    def __init__( self, encoding ):
+
+        self._encoding = encoding
+        self._required = {}
+        self._forbidden = []
+
+    def startElement( self, name, attrs ):
+
+        if name == 'tool-setup':
+            pass
+
+        elif name == 'forbidden':
+
+            tool_id = self._extract( attrs, 'tool_id' )
+
+            if tool_id not in self._forbidden:
+                self._forbidden.append( tool_id )
+
+        elif name == 'required':
+
+            tool_id = self._extract( attrs, 'tool_id' )
+            dotted_name = self._extract( attrs, 'class' )
+            self._required[ tool_id ] = dotted_name
+
+        else:
+            raise ValueError, 'Unknown element %s' % name
+
+InitializeClass( _ToolsetParser )

Copied: GenericSetup/branches/wichert-events/tests/test_stepzcml.py (from rev 81471, GenericSetup/trunk/tests/test_stepzcml.py)
===================================================================
--- GenericSetup/branches/wichert-events/tests/test_stepzcml.py	                        (rev 0)
+++ GenericSetup/branches/wichert-events/tests/test_stepzcml.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,72 @@
+##############################################################################
+#
+# 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 import/export step zcml module.
+"""
+import unittest
+from Products.GenericSetup.zcml import cleanUpImportSteps
+import Products.GenericSetup
+from Products.GenericSetup.registry import _import_step_registry
+from Products.GenericSetup.testing import ExportImportZCMLLayer
+from Products.Five import zcml
+
+EMPTY_ZCML = '''<configure xmlns:genericsetup="http://namespaces.zope.org/genericsetup">
+</configure>'''
+
+ONE_STEP_ZCML = '''<configure xmlns:genericsetup="http://namespaces.zope.org/genericsetup" i18n_domain="genericsetup">
+<genericsetup:importStep
+    name="Products.GenericSetup.teststep"
+    title="step title"
+    description="step description"
+    handler="Products.GenericSetup.initialize"
+    version="1.0"
+    />
+</configure>'''
+
+class ImportStepTests(unittest.TestCase):
+    layer = ExportImportZCMLLayer
+
+    def setUp(self):
+        zcml.load_config('meta.zcml', Products.GenericSetup)
+
+    def tearDown(self):
+        cleanUpImportSteps()
+
+    def testEmptyImport(self):
+        zcml.load_string(EMPTY_ZCML)
+        self.assertEqual(_import_step_registry._registered, {})
+
+    def testOneStepImport(self):
+        zcml.load_string(ONE_STEP_ZCML)
+        self.assertEqual(_import_step_registry._registered.keys(),
+            [ u'Products.GenericSetup.teststep'  ])
+        info = _import_step_registry._registered[ u'Products.GenericSetup.teststep' ]
+        self.assertEqual( info['description'],
+                u'step description' )
+        self.assertEqual( info['title'],
+                u'step title' )
+        self.assertEqual( info['handler'],
+                'Products.GenericSetup.initialize')
+        self.assertEqual( info['version'],
+                u'1.0' )
+        self.assertEqual( info['id'],
+                u'Products.GenericSetup.teststep' )
+
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(ImportStepTests),
+        ))
+
+if __name__ == '__main__':
+    from Products.GenericSetup.testing import run
+    run(test_suite())

Deleted: GenericSetup/branches/wichert-events/tests/test_zcml.py
===================================================================
--- GenericSetup/trunk/tests/test_zcml.py	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/tests/test_zcml.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,239 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2006 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 zcml module.
-
-$Id$
-"""
-
-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::
-
-      >>> import Products.GenericSetup
-      >>> from Products.Five import zcml
-      >>> configure_zcml = '''
-      ... <configure
-      ...     xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
-      ...     i18n_domain="foo">
-      ...   <genericsetup:registerProfile
-      ...       name="default"
-      ...       title="Install Foo Extension"
-      ...       description="Adds foo support."
-      ...       provides="Products.GenericSetup.interfaces.EXTENSION"
-      ...       />
-      ... </configure>'''
-      >>> zcml.load_config('meta.zcml', Products.GenericSetup)
-      >>> zcml.load_string(configure_zcml)
-
-    Make sure the profile is registered correctly::
-
-      >>> from Products.GenericSetup.registry import _profile_registry
-      >>> profile_id = 'Products.GenericSetup:default'
-      >>> profile_id in _profile_registry._profile_ids
-      True
-      >>> info = _profile_registry._profile_info[profile_id]
-      >>> info['id']
-      u'Products.GenericSetup:default'
-      >>> info['title']
-      u'Install Foo Extension'
-      >>> info['description']
-      u'Adds foo support.'
-      >>> info['path']
-      u'profiles/default'
-      >>> info['product']
-      'Products.GenericSetup'
-      >>> from Products.GenericSetup.interfaces import EXTENSION
-      >>> info['type'] is EXTENSION
-      True
-      >>> info['for'] is None
-      True
-
-    Clean up and make sure the cleanup works::
-
-      >>> from zope.testing.cleanup import cleanUp
-      >>> cleanUp()
-      >>> profile_id in _profile_registry._profile_ids
-      False
-      >>> profile_id in _profile_registry._profile_info
-      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(optionflags=ELLIPSIS),
-        ))
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='test_suite')

Copied: GenericSetup/branches/wichert-events/tests/test_zcml.py (from rev 81471, GenericSetup/trunk/tests/test_zcml.py)
===================================================================
--- GenericSetup/branches/wichert-events/tests/test_zcml.py	                        (rev 0)
+++ GenericSetup/branches/wichert-events/tests/test_zcml.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,335 @@
+##############################################################################
+#
+# Copyright (c) 2006-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 zcml module.
+
+$Id$
+"""
+
+import unittest
+import Testing
+from zope.testing import doctest
+from zope.testing.doctest import ELLIPSIS
+
+from Products.GenericSetup.testing import ExportImportZCMLLayer
+from Products.GenericSetup.zcml import cleanUpImportSteps
+from Products.GenericSetup.zcml import cleanUpExportSteps
+from Products.GenericSetup.registry import _import_step_registry
+from Products.GenericSetup.registry import _export_step_registry
+from Products.Five import zcml
+
+def dummy_importstep_handler(context):
+    pass
+
+def dummy_exportstep_handler(context):
+    pass
+
+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::
+
+      >>> import Products.GenericSetup
+      >>> from Products.Five import zcml
+      >>> configure_zcml = '''
+      ... <configure
+      ...     xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+      ...     i18n_domain="foo">
+      ...   <genericsetup:registerProfile
+      ...       name="default"
+      ...       title="Install Foo Extension"
+      ...       description="Adds foo support."
+      ...       provides="Products.GenericSetup.interfaces.EXTENSION"
+      ...       />
+      ... </configure>'''
+      >>> zcml.load_config('meta.zcml', Products.GenericSetup)
+      >>> zcml.load_string(configure_zcml)
+
+    Make sure the profile is registered correctly::
+
+      >>> from Products.GenericSetup.registry import _profile_registry
+      >>> profile_id = 'Products.GenericSetup:default'
+      >>> profile_id in _profile_registry._profile_ids
+      True
+      >>> info = _profile_registry._profile_info[profile_id]
+      >>> info['id']
+      u'Products.GenericSetup:default'
+      >>> info['title']
+      u'Install Foo Extension'
+      >>> info['description']
+      u'Adds foo support.'
+      >>> info['path']
+      u'profiles/default'
+      >>> info['product']
+      'Products.GenericSetup'
+      >>> from Products.GenericSetup.interfaces import EXTENSION
+      >>> info['type'] is EXTENSION
+      True
+      >>> info['for'] is None
+      True
+
+    Clean up and make sure the cleanup works::
+
+      >>> from zope.testing.cleanup import cleanUp
+      >>> cleanUp()
+      >>> profile_id in _profile_registry._profile_ids
+      False
+      >>> profile_id in _profile_registry._profile_info
+      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()
+    """
+
+
+class ImportStepTests(unittest.TestCase):
+    layer = ExportImportZCMLLayer
+
+    def tearDown(self):
+        cleanUpImportSteps()
+
+    def testNoDependencies(self):
+        zcml.load_string("""<configure
+                              xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+                              i18n_domain="genericsetup">
+                             <genericsetup:importStep
+                                 name="name"
+                                 title="title"
+                                 description="description"
+                                 handler="Products.GenericSetup.tests.test_zcml.dummy_importstep_handler"
+                                 version="version">
+                             </genericsetup:importStep>
+                            </configure>""")
+        from Products.GenericSetup.zcml import _import_step_regs
+        self.assertEqual(_import_step_regs, [u'name'])
+        self.assertEqual( _import_step_registry.listSteps(), [u'name'])
+        data=_import_step_registry.getStepMetadata(u'name')
+        self.assertEqual(data["handler"],
+                'Products.GenericSetup.tests.test_zcml.dummy_importstep_handler')
+        self.assertEqual(data["description"], u"description")
+        self.assertEqual(data["version"], u"version")
+        self.assertEqual(data["title"], u"title")
+        self.assertEqual(data["dependencies"], ())
+        self.assertEqual(data["id"], u"name")
+
+
+    def testWithDependency(self):
+        zcml.load_string("""<configure
+                              xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+                              i18n_domain="genericsetup">
+                             <genericsetup:importStep
+                                 name="name"
+                                 title="title"
+                                 description="description"
+                                 handler="Products.GenericSetup.tests.test_zcml.dummy_importstep_handler"
+                                 version="version">
+                                <depends name="something.else"/>
+                             </genericsetup:importStep>
+                            </configure>""")
+        data=_import_step_registry.getStepMetadata(u'name')
+        self.assertEqual(data["dependencies"], (u"something.else",))
+
+
+
+class ExportStepTests(unittest.TestCase):
+    layer = ExportImportZCMLLayer
+
+    def tearDown(self):
+        cleanUpExportSteps()
+
+    def testRegistration(self):
+        zcml.load_string("""<configure
+                              xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+                              i18n_domain="genericsetup">
+                             <genericsetup:exportStep
+                                 name="name"
+                                 title="title"
+                                 description="description"
+                                 handler="Products.GenericSetup.tests.test_zcml.dummy_exportstep_handler"
+                                 />
+                              </configure>
+                              """)
+        from Products.GenericSetup.zcml import _export_step_regs
+        self.assertEqual(_export_step_regs, [u'name'])
+        self.assertEqual( _export_step_registry.listSteps(), [u'name'])
+        data=_export_step_registry.getStepMetadata(u'name')
+        self.assertEqual(data["handler"],
+                'Products.GenericSetup.tests.test_zcml.dummy_exportstep_handler')
+        self.assertEqual(data["description"], u"description")
+        self.assertEqual(data["title"], u"title")
+        self.assertEqual(data["id"], u"name")
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(optionflags=ELLIPSIS))
+    suite.addTest(unittest.makeSuite(ImportStepTests))
+    suite.addTest(unittest.makeSuite(ExportStepTests))
+    return suite
+
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Deleted: GenericSetup/branches/wichert-events/tool.py
===================================================================
--- GenericSetup/trunk/tool.py	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/tool.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,1097 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 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.
-#
-##############################################################################
-""" Classes:  SetupTool
-
-$Id$
-"""
-
-import os
-import time
-import logging
-from warnings import warn
-from cgi import escape
-
-from AccessControl import ClassSecurityInfo
-from Acquisition import aq_base
-from Globals import InitializeClass
-from OFS.Folder import Folder
-from OFS.Image import File
-from Products.PageTemplates.PageTemplateFile import PageTemplateFile
-from ZODB.POSException import ConflictError
-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
-from permissions import ManagePortal
-from context import DirectoryImportContext
-from context import SnapshotImportContext
-from context import TarballExportContext
-from context import TarballImportContext
-from context import SnapshotExportContext
-from differ import ConfigDiff
-from registry import ImportStepRegistry
-from registry import ExportStepRegistry
-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 _getDottedName
-from utils import _resolveDottedName
-from utils import _wwwdir
-
-IMPORT_STEPS_XML = 'import_steps.xml'
-EXPORT_STEPS_XML = 'export_steps.xml'
-TOOLSET_XML = 'toolset.xml'
-
-def exportStepRegistries(context):
-
-    """ Built-in handler for exporting import / export step registries.
-    """
-    setup_tool = context.getSetupTool()
-    logger = context.getLogger('registries')
-
-    import_steps_xml = setup_tool.getImportStepRegistry().generateXML()
-    context.writeDataFile('import_steps.xml', import_steps_xml, 'text/xml')
-
-    export_steps_xml = setup_tool.getExportStepRegistry().generateXML()
-    context.writeDataFile('export_steps.xml', export_steps_xml, 'text/xml')
-
-    logger.info('Step registries exported.')
-
-def importToolset(context):
-
-    """ Import required / forbidden tools from XML file.
-    """
-    site = context.getSite()
-    encoding = context.getEncoding()
-    logger = context.getLogger('toolset')
-
-    xml = context.readDataFile(TOOLSET_XML)
-    if xml is None:
-        logger.info('Nothing to import.')
-        return
-
-    setup_tool = context.getSetupTool()
-    toolset = setup_tool.getToolsetRegistry()
-
-    toolset.parseXML(xml, encoding)
-
-    existing_ids = site.objectIds()
-    existing_values = site.objectValues()
-
-    for tool_id in toolset.listForbiddenTools():
-
-        if tool_id in existing_ids:
-            site._delObject(tool_id)
-
-    for info in toolset.listRequiredToolInfo():
-
-        tool_id = str(info['id'])
-        tool_class = _resolveDottedName(info['class'])
-
-        existing = getattr(aq_base(site), tool_id, None)
-        # Don't even initialize the tool again, if it already exists.
-        if existing is None:
-            try:
-                new_tool = tool_class()
-            except TypeError:
-                new_tool = tool_class(tool_id)
-            else:
-                try:
-                    new_tool._setId(tool_id)
-                except (ConflictError, KeyboardInterrupt):
-                    raise
-                except:
-                    # XXX: ImmutableId raises result of calling MessageDialog
-                    pass
-
-            site._setObject(tool_id, new_tool)
-        else:
-            unwrapped = aq_base(existing)
-            if not isinstance(unwrapped, tool_class):
-                site._delObject(tool_id)
-                site._setObject(tool_id, tool_class())
-
-    logger.info('Toolset imported.')
-
-def exportToolset(context):
-
-    """ Export required / forbidden tools to XML file.
-    """
-    setup_tool = context.getSetupTool()
-    toolset = setup_tool.getToolsetRegistry()
-    logger = context.getLogger('toolset')
-
-    xml = toolset.generateXML()
-    context.writeDataFile(TOOLSET_XML, xml, 'text/xml')
-
-    logger.info('Toolset exported.')
-
-
-class SetupTool(Folder):
-
-    """ Profile-based site configuration manager.
-    """
-
-    implements(ISetupTool)
-
-    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):
-        self.id = str(id)
-        self._import_registry = ImportStepRegistry()
-        self._export_registry = ExportStepRegistry()
-        self._export_registry.registerStep('step_registries',
-                                           _getDottedName(exportStepRegistries),
-                                           'Export import / export steps.',
-                                          )
-        self._toolset_registry = ToolsetRegistry()
-
-    #
-    #   ISetupTool API
-    #
-    security.declareProtected(ManagePortal, 'getEncoding')
-    def getEncoding(self):
-
-        """ See ISetupTool.
-        """
-        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 runImportStepFromProfile '
-             '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._baseline_context_id = context_id
-        self.applyContextById(context_id, encoding)
-
-
-    security.declareProtected(ManagePortal, 'applyContextById')
-    def applyContextById(self, context_id, encoding=None):
-        context = self._getImportContext(context_id)
-        self.applyContext(context, encoding)
-
-
-    security.declareProtected(ManagePortal, 'applyContext')
-    def applyContext(self, context, encoding=None):
-        self._updateImportStepsRegistry(context, encoding)
-        self._updateExportStepsRegistry(context, encoding)
-
-    security.declareProtected(ManagePortal, 'getImportStepRegistry')
-    def getImportStepRegistry(self):
-
-        """ See ISetupTool.
-        """
-        return self._import_registry
-
-    security.declareProtected(ManagePortal, 'getExportStepRegistry')
-    def getExportStepRegistry(self):
-
-        """ See ISetupTool.
-        """
-        return self._export_registry
-
-    security.declareProtected(ManagePortal, 'getToolsetRegistry')
-    def getToolsetRegistry(self):
-
-        """ See ISetupTool.
-        """
-        return self._toolset_registry
-
-    security.declareProtected(ManagePortal, 'runImportStepFromProfile')
-    def runImportStepFromProfile(self, profile_id, step_id,
-                                 run_dependencies=True, purge_old=None):
-        """ See ISetupTool.
-        """
-        old_context = self._import_context_id
-        context = self._getImportContext(profile_id, purge_old)
-
-        self.applyContext(context)
-
-        info = self._import_registry.getStepMetadata(step_id)
-
-        if info is None:
-            self._import_context_id = old_context
-            raise ValueError, 'No such import step: %s' % step_id
-
-        dependencies = info.get('dependencies', ())
-
-        messages = {}
-        steps = []
-        if run_dependencies:
-            for dependency in dependencies:
-
-                if dependency not in steps:
-                    message = self._doRunImportStep(dependency, context)
-                    messages[dependency] = message or ''
-                    steps.append(dependency)
-
-        message = self._doRunImportStep(step_id, context)
-        message_list = filter(None, [message])
-        message_list.extend( ['%s: %s' % x[1:] for x in context.listNotes()] )
-        messages[step_id] = '\n'.join(message_list)
-        steps.append(step_id)
-
-        self._import_context_id = old_context
-
-        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
-
-        old_context = self._import_context_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'])
-
-        self._import_context_id = old_context
-
-        return result
-
-    security.declareProtected(ManagePortal, 'runAllImportSteps')
-    def runAllImportSteps(self, purge_old=None):
-
-        """ See ISetupTool.
-        """
-        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)
-
-    security.declareProtected(ManagePortal, 'runExportStep')
-    def runExportStep(self, step_id):
-
-        """ See ISetupTool.
-        """
-        return self._doRunExportSteps([step_id])
-
-    security.declareProtected(ManagePortal, 'runAllExportSteps')
-    def runAllExportSteps(self):
-
-        """ See ISetupTool.
-        """
-        return self._doRunExportSteps(self._export_registry.listSteps())
-
-    security.declareProtected(ManagePortal, 'createSnapshot')
-    def createSnapshot(self, snapshot_id):
-
-        """ See ISetupTool.
-        """
-        context = SnapshotExportContext(self, snapshot_id)
-        messages = {}
-        steps = self._export_registry.listSteps()
-
-        for step_id in steps:
-
-            handler = self._export_registry.getStep(step_id)
-
-            if handler is None:
-                logger = logging.getLogger('GenericSetup')
-                logger.error('Step %s has an invalid handler' % step_id)
-                continue
-
-            messages[step_id] = handler(context)
-
-
-        return { 'steps' : steps
-               , 'messages' : messages
-               , 'url' : context.getSnapshotURL()
-               , 'snapshot' : context.getSnapshotFolder()
-               }
-
-    security.declareProtected(ManagePortal, 'compareConfigurations')
-    def compareConfigurations(self,
-                              lhs_context,
-                              rhs_context,
-                              missing_as_empty=False,
-                              ignore_blanks=False,
-                              skip=SKIPPED_FILES,
-                             ):
-        """ See ISetupTool.
-        """
-        differ = ConfigDiff(lhs_context,
-                            rhs_context,
-                            missing_as_empty,
-                            ignore_blanks,
-                            skip,
-                           )
-
-        return differ.compare()
-
-    security.declareProtected(ManagePortal, 'markupComparison')
-    def markupComparison(self, lines):
-
-        """ See ISetupTool.
-        """
-        result = []
-
-        for line in lines.splitlines():
-
-            if line.startswith('** '):
-
-                if line.find('File') > -1:
-                    if line.find('replaced') > -1:
-                        result.append(('file-to-dir', line))
-                    elif line.find('added') > -1:
-                        result.append(('file-added', line))
-                    else:
-                        result.append(('file-removed', line))
-                else:
-                    if line.find('replaced') > -1:
-                        result.append(('dir-to-file', line))
-                    elif line.find('added') > -1:
-                        result.append(('dir-added', line))
-                    else:
-                        result.append(('dir-removed', line))
-
-            elif line.startswith('@@'):
-                result.append(('diff-range', line))
-
-            elif line.startswith(' '):
-                result.append(('diff-context', line))
-
-            elif line.startswith('+'):
-                result.append(('diff-added', line))
-
-            elif line.startswith('-'):
-                result.append(('diff-removed', line))
-
-            elif line == '\ No newline at end of file':
-                result.append(('diff-context', line))
-
-            else:
-                result.append(('diff-header', line))
-
-        return '<pre>\n%s\n</pre>' % (
-            '\n'.join([('<span class="%s">%s</span>' % (cl, escape(l)))
-                                  for cl, l in result]))
-
-    #
-    #   ZMI
-    #
-    manage_options = (Folder.manage_options[:1]
-                    + ({'label' : 'Profiles',
-                        'action' : 'manage_tool'
-                       },
-                       {'label' : 'Import',
-                        'action' : 'manage_importSteps'
-                       },
-                       {'label' : 'Export',
-                        'action' : 'manage_exportSteps'
-                       },
-                       {'label' : 'Upgrades',
-                        'action' : 'manage_upgrades'
-                        },
-                       {'label' : 'Snapshots',
-                        'action' : 'manage_snapshots'
-                       },
-                       {'label' : 'Comparison',
-                        'action' : 'manage_showDiff'
-                       },
-                      )
-                    + Folder.manage_options[3:] # skip "View", "Properties"
-                     )
-
-    security.declareProtected(ManagePortal, 'manage_tool')
-    manage_tool = PageTemplateFile('sutProperties', _wwwdir)
-
-    security.declareProtected(ManagePortal, 'manage_updateToolProperties')
-    def manage_updateToolProperties(self, context_id, RESPONSE):
-        """ Update the tool's settings.
-        """
-        self.setBaselineContext(context_id)
-
-        RESPONSE.redirect('%s/manage_tool?manage_tabs_message=%s'
-                         % (self.absolute_url(), 'Properties+updated.'))
-
-    security.declareProtected(ManagePortal, 'manage_importSteps')
-    manage_importSteps = PageTemplateFile('sutImportSteps', _wwwdir)
-
-    security.declareProtected(ManagePortal, 'manage_importSelectedSteps')
-    def manage_importSelectedSteps(self, ids, run_dependencies, context_id=None):
-        """ Import the steps selected by the user.
-        """
-        messages = {}
-        if not ids:
-            summary = 'No steps selected.'
-
-        else:
-            if context_id is None:
-                context_id = self.getBaselineContextID()
-            steps_run = []
-            for step_id in ids:
-                result = self.runImportStepFromProfile(context_id,
-                                                       step_id,
-                                                       run_dependencies)
-                steps_run.extend(result['steps'])
-                messages.update(result['messages'])
-
-            summary = 'Steps run: %s' % ', '.join(steps_run)
-
-            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, context_id=None):
-
-        """ Import all steps.
-        """
-        if context_id is None:
-            context_id = self.getBaselineContextID()
-        result = self.runAllImportStepsFromProfile(context_id, purge_old=None)
-
-        steps_run = 'Steps run: %s' % ', '.join(result['steps'])
-
-        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)
-
-                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):
-        """ Import steps from the uploaded tarball.
-        """
-        if getattr(tarball, 'read', None) is not None:
-            tarball = tarball.read()
-
-        context = TarballImportContext(tool=self,
-                                       archive_bits=tarball,
-                                       encoding='UTF8',
-                                       should_purge=True,
-                                      )
-        result = self._runImportStepsFromContext(context,
-                                                 purge_old=True)
-        steps_run = 'Steps run: %s' % ', '.join(result['steps'])
-
-        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_exportSteps')
-    manage_exportSteps = PageTemplateFile('sutExportSteps', _wwwdir)
-
-    security.declareProtected(ManagePortal, 'manage_exportSelectedSteps')
-    def manage_exportSelectedSteps(self, ids, RESPONSE):
-
-        """ Export the steps selected by the user.
-        """
-        if not ids:
-            RESPONSE.redirect('%s/manage_exportSteps?manage_tabs_message=%s'
-                             % (self.absolute_url(), 'No+steps+selected.'))
-
-        result = self._doRunExportSteps(ids)
-        RESPONSE.setHeader('Content-type', 'application/x-gzip')
-        RESPONSE.setHeader('Content-disposition',
-                           'attachment; filename=%s' % result['filename'])
-        return result['tarball']
-
-    security.declareProtected(ManagePortal, 'manage_exportAllSteps')
-    def manage_exportAllSteps(self, RESPONSE):
-
-        """ Export all steps.
-        """
-        result = self.runAllExportSteps()
-        RESPONSE.setHeader('Content-type', 'application/x-gzip')
-        RESPONSE.setHeader('Content-disposition',
-                           '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)
-
-    security.declareProtected(ManagePortal, 'listSnapshotInfo')
-    def listSnapshotInfo(self):
-
-        """ Return a list of mappings describing available snapshots.
-
-        o Keys include:
-
-          'id' -- snapshot ID
-
-          'title' -- snapshot title or ID
-
-          'url' -- URL of the snapshot folder
-        """
-        result = []
-        snapshots = self._getOb('snapshots', None)
-
-        if snapshots:
-
-            for id, folder in snapshots.objectItems('Folder'):
-
-                result.append({ 'id' : id
-                               , 'title' : folder.title_or_id()
-                               , 'url' : folder.absolute_url()
-                               })
-        return result
-
-    security.declareProtected(ManagePortal, 'listProfileInfo')
-    def listProfileInfo(self):
-
-        """ Return a list of mappings describing registered profiles.
-        Base profile is listed first, extensions are sorted.
-
-        o Keys include:
-
-          'id' -- profile ID
-
-          'title' -- profile title or ID
-
-          'description' -- description of the profile
-
-          'path' -- path to the profile within its product
-
-          'product' -- name of the registering product
-        """
-        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'],
-                     'type': 'snapshot',
-                   }
-                    for info in self.listSnapshotInfo()]
-        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):
-
-        """ Create a snapshot with the given ID.
-
-        o If no ID is passed, generate one.
-        """
-        if snapshot_id is None:
-            snapshot_id = self._mangleTimestampName('snapshot')
-
-        self.createSnapshot(snapshot_id)
-
-        RESPONSE.redirect('%s/manage_snapshots?manage_tabs_message=%s'
-                         % (self.absolute_url(), 'Snapshot+created.'))
-        return ""
-
-    security.declareProtected(ManagePortal, 'manage_showDiff')
-    manage_showDiff = PageTemplateFile('sutCompare', _wwwdir)
-
-    def manage_downloadDiff(self,
-                            lhs,
-                            rhs,
-                            missing_as_empty,
-                            ignore_blanks,
-                            RESPONSE,
-                           ):
-        """ Crack request vars and call compareConfigurations.
-
-        o Return the result as a 'text/plain' stream, suitable for framing.
-        """
-        comparison = self.manage_compareConfigurations(lhs,
-                                                       rhs,
-                                                       missing_as_empty,
-                                                       ignore_blanks,
-                                                      )
-        RESPONSE.setHeader('Content-Type', 'text/plain')
-        return _PLAINTEXT_DIFF_HEADER % (lhs, rhs, comparison)
-
-    security.declareProtected(ManagePortal, 'manage_compareConfigurations')
-    def manage_compareConfigurations(self,
-                                     lhs,
-                                     rhs,
-                                     missing_as_empty,
-                                     ignore_blanks,
-                                    ):
-        """ Crack request vars and call compareConfigurations.
-        """
-        lhs_context = self._getImportContext(lhs)
-        rhs_context = self._getImportContext(rhs)
-
-        return self.compareConfigurations(lhs_context,
-                                          rhs_context,
-                                          missing_as_empty,
-                                          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
-    #
-    security.declarePrivate('_getProductPath')
-    def _getProductPath(self, product_name):
-
-        """ Return the absolute path of the product's directory.
-        """
-        try:
-            # BBB: for GenericSetup 1.1 style product names
-            product = __import__('Products.%s' % product_name
-                                , globals(), {}, ['initialize'])
-        except ImportError:
-            try:
-                product = __import__(product_name
-                                    , globals(), {}, ['initialize'])
-            except ImportError:
-                raise ValueError('Not a valid product name: %s'
-                                 % product_name)
-
-        return product.__path__[0]
-
-    security.declarePrivate('_getImportContext')
-    def _getImportContext(self, context_id, should_purge=None):
-
-        """ Crack ID and generate appropriate import context.
-        """
-        encoding = self.getEncoding()
-
-        if context_id.startswith('profile-'):
-
-            context_id = context_id[len('profile-'):]
-            info = _profile_registry.getProfileInfo(context_id)
-
-            if info.get('product'):
-                path = os.path.join(self._getProductPath(info['product'])
-                                   , info['path'])
-            else:
-                path = info['path']
-            if should_purge is None:
-                should_purge = (info.get('type') != EXTENSION)
-            return DirectoryImportContext(self, path, should_purge, encoding)
-
-        # else snapshot
-        context_id = context_id[len('snapshot-'):]
-        if should_purge is None:
-            should_purge = True
-        return SnapshotImportContext(self, context_id, should_purge, encoding)
-
-    security.declarePrivate('_updateImportStepsRegistry')
-    def _updateImportStepsRegistry(self, context, encoding):
-
-        """ Update our import steps registry from our profile.
-        """
-        if context is None:
-            context = self._getImportContext(self._import_context_id)
-        xml = context.readDataFile(IMPORT_STEPS_XML)
-        if xml is None:
-            return
-
-        info_list = self._import_registry.parseXML(xml, encoding)
-
-        for step_info in info_list:
-
-            id = step_info['id']
-            version = step_info['version']
-            handler = step_info['handler']
-            dependencies = tuple(step_info.get('dependencies', ()))
-            title = step_info.get('title', id)
-            description = ''.join(step_info.get('description', []))
-
-            self._import_registry.registerStep(id=id,
-                                               version=version,
-                                               handler=handler,
-                                               dependencies=dependencies,
-                                               title=title,
-                                               description=description,
-                                              )
-
-    security.declarePrivate('_updateExportStepsRegistry')
-    def _updateExportStepsRegistry(self, context, encoding):
-
-        """ Update our export steps registry from our profile.
-        """
-        if context is None:
-            context = self._getImportContext(self._import_context_id)
-        xml = context.readDataFile(EXPORT_STEPS_XML)
-        if xml is None:
-            return
-
-        info_list = self._export_registry.parseXML(xml, encoding)
-
-        for step_info in info_list:
-
-            id = step_info['id']
-            handler = step_info['handler']
-            title = step_info.get('title', id)
-            description = ''.join(step_info.get('description', []))
-
-            self._export_registry.registerStep(id=id,
-                                               handler=handler,
-                                               title=title,
-                                               description=description,
-                                              )
-
-    security.declarePrivate('_doRunImportStep')
-    def _doRunImportStep(self, step_id, context):
-
-        """ Run a single import step, using a pre-built context.
-        """
-        __traceback_info__ = step_id
-        marker = object()
-
-        handler = self._import_registry.getStep(step_id)
-
-        if handler is marker:
-            raise ValueError('Invalid import step: %s' % step_id)
-
-        if handler is None:
-            msg = 'Step %s has an invalid import handler' % step_id
-            logger = logging.getLogger('GenericSetup')
-            logger.error(msg)
-            return 'ERROR: ' + msg
-
-        return handler(context)
-
-    security.declarePrivate('_doRunExportSteps')
-    def _doRunExportSteps(self, steps):
-
-        """ See ISetupTool.
-        """
-        context = TarballExportContext(self)
-        messages = {}
-        marker = object()
-
-        for step_id in steps:
-
-            handler = self._export_registry.getStep(step_id, marker)
-
-            if handler is marker:
-                raise ValueError('Invalid export step: %s' % step_id)
-
-            if handler is None:
-                msg = 'Step %s has an invalid import handler' % step_id
-                logger = logging.getLogger('GenericSetup')
-                logger.error(msg)
-                messages[step_id] = msg
-            else:
-                messages[step_id] = handler(context)
-
-        return { 'steps' : steps
-               , 'messages' : messages
-               , 'tarball' : context.getArchive()
-               , 'filename' : context.getArchiveFilename()
-               }
-
-    security.declarePrivate('_runImportStepsFromContext')
-    def _runImportStepsFromContext(self, context, steps=None, purge_old=None):
-        self.applyContext(context)
-
-        if steps is None:
-            steps = self._import_registry.sortSteps()
-        messages = {}
-
-        for step in steps:
-            message = self._doRunImportStep(step, context)
-            message_list = filter(None, [message])
-            message_list.extend( ['%s: %s' % x[1:]
-                                  for x in context.listNotes()] )
-            messages[step] = '\n'.join(message_list)
-            context.clearNotes()
-
-        return { 'steps' : steps, 'messages' : messages }
-
-    security.declarePrivate('_mangleTimestampName')
-    def _mangleTimestampName(self, prefix, ext=None):
-
-        """ Create a mangled ID using a timestamp.
-        """
-        timestamp = time.gmtime()
-        items = (prefix,) + timestamp[:6]
-
-        if ext is None:
-            fmt = '%s-%4d%02d%02d%02d%02d%02d'
-        else:
-            fmt = '%s-%4d%02d%02d%02d%02d%02d.%s'
-            items += (ext,)
-
-        return fmt % items
-
-    security.declarePrivate('_createReport')
-    def _createReport(self, name, steps, messages):
-
-        """ Record the results of a run.
-        """
-        lines = []
-        # Create report
-        for step in steps:
-            lines.append('=' * 65)
-            lines.append('Step: %s' % step)
-            lines.append('=' * 65)
-            msg = messages[step]
-            lines.extend(msg.split('\n'))
-            lines.append('')
-
-        report = '\n'.join(lines)
-        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,
-                    content_type='text/plain'
-                   )
-        self._setObject(name, file)
-
-InitializeClass(SetupTool)
-
-_PLAINTEXT_DIFF_HEADER ="""\
-Comparing configurations: '%s' and '%s'
-
-%s"""
-
-_TOOL_ID = 'setup_tool'
-
-addSetupToolForm = PageTemplateFile('toolAdd.zpt', _wwwdir)
-
-def addSetupTool(dispatcher, RESPONSE):
-    """
-    """
-    dispatcher._setObject(_TOOL_ID, SetupTool(_TOOL_ID))
-
-    RESPONSE.redirect('%s/manage_main' % dispatcher.absolute_url())

Copied: GenericSetup/branches/wichert-events/tool.py (from rev 81471, GenericSetup/trunk/tool.py)
===================================================================
--- GenericSetup/branches/wichert-events/tool.py	                        (rev 0)
+++ GenericSetup/branches/wichert-events/tool.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,1141 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+""" Classes:  SetupTool
+
+$Id$
+"""
+
+import os
+import time
+import logging
+from warnings import warn
+from cgi import escape
+
+from AccessControl import ClassSecurityInfo
+from Acquisition import aq_base
+from Globals import InitializeClass
+from OFS.Folder import Folder
+from OFS.Image import File
+from Products.PageTemplates.PageTemplateFile import PageTemplateFile
+from ZODB.POSException import ConflictError
+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
+from permissions import ManagePortal
+from context import DirectoryImportContext
+from context import SnapshotImportContext
+from context import TarballExportContext
+from context import TarballImportContext
+from context import SnapshotExportContext
+from differ import ConfigDiff
+from registry import ImportStepRegistry
+from registry import ExportStepRegistry
+from registry import ToolsetRegistry
+from registry import _profile_registry
+from registry import _import_step_registry
+from registry import _export_step_registry
+
+from upgrade import listUpgradeSteps
+from upgrade import listProfilesWithUpgrades
+from upgrade import _upgrade_registry
+
+from utils import _getDottedName
+from utils import _resolveDottedName
+from utils import _wwwdir
+from utils import _computeTopologicalSort
+
+IMPORT_STEPS_XML = 'import_steps.xml'
+EXPORT_STEPS_XML = 'export_steps.xml'
+TOOLSET_XML = 'toolset.xml'
+
+def exportStepRegistries(context):
+
+    """ Built-in handler for exporting import / export step registries.
+    """
+    setup_tool = context.getSetupTool()
+    logger = context.getLogger('registries')
+
+    import_steps_xml = setup_tool.getImportStepRegistry().generateXML()
+    context.writeDataFile('import_steps.xml', import_steps_xml, 'text/xml')
+
+    export_steps_xml = setup_tool.getExportStepRegistry().generateXML()
+    context.writeDataFile('export_steps.xml', export_steps_xml, 'text/xml')
+
+    logger.info('Step registries exported.')
+
+def importToolset(context):
+
+    """ Import required / forbidden tools from XML file.
+    """
+    site = context.getSite()
+    encoding = context.getEncoding()
+    logger = context.getLogger('toolset')
+
+    xml = context.readDataFile(TOOLSET_XML)
+    if xml is None:
+        logger.info('Nothing to import.')
+        return
+
+    setup_tool = context.getSetupTool()
+    toolset = setup_tool.getToolsetRegistry()
+
+    toolset.parseXML(xml, encoding)
+
+    existing_ids = site.objectIds()
+    existing_values = site.objectValues()
+
+    for tool_id in toolset.listForbiddenTools():
+
+        if tool_id in existing_ids:
+            site._delObject(tool_id)
+
+    for info in toolset.listRequiredToolInfo():
+
+        tool_id = str(info['id'])
+        tool_class = _resolveDottedName(info['class'])
+
+        existing = getattr(aq_base(site), tool_id, None)
+        # Don't even initialize the tool again, if it already exists.
+        if existing is None:
+            try:
+                new_tool = tool_class()
+            except TypeError:
+                new_tool = tool_class(tool_id)
+            else:
+                try:
+                    new_tool._setId(tool_id)
+                except (ConflictError, KeyboardInterrupt):
+                    raise
+                except:
+                    # XXX: ImmutableId raises result of calling MessageDialog
+                    pass
+
+            site._setObject(tool_id, new_tool)
+        else:
+            unwrapped = aq_base(existing)
+            if not isinstance(unwrapped, tool_class):
+                site._delObject(tool_id)
+                site._setObject(tool_id, tool_class())
+
+    logger.info('Toolset imported.')
+
+def exportToolset(context):
+
+    """ Export required / forbidden tools to XML file.
+    """
+    setup_tool = context.getSetupTool()
+    toolset = setup_tool.getToolsetRegistry()
+    logger = context.getLogger('toolset')
+
+    xml = toolset.generateXML()
+    context.writeDataFile(TOOLSET_XML, xml, 'text/xml')
+
+    logger.info('Toolset exported.')
+
+
+class SetupTool(Folder):
+
+    """ Profile-based site configuration manager.
+    """
+
+    implements(ISetupTool)
+
+    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):
+        self.id = str(id)
+        self._import_registry = ImportStepRegistry()
+        self._export_registry = ExportStepRegistry()
+        self._export_registry.registerStep('step_registries',
+                                           _getDottedName(exportStepRegistries),
+                                           'Export import / export steps.',
+                                          )
+        self._toolset_registry = ToolsetRegistry()
+
+    #
+    #   ISetupTool API
+    #
+    security.declareProtected(ManagePortal, 'getEncoding')
+    def getEncoding(self):
+
+        """ See ISetupTool.
+        """
+        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 runImportStepFromProfile '
+             '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._baseline_context_id = context_id
+        self.applyContextById(context_id, encoding)
+
+
+    security.declareProtected(ManagePortal, 'applyContextById')
+    def applyContextById(self, context_id, encoding=None):
+        context = self._getImportContext(context_id)
+        self.applyContext(context, encoding)
+
+
+    security.declareProtected(ManagePortal, 'applyContext')
+    def applyContext(self, context, encoding=None):
+        self._updateImportStepsRegistry(context, encoding)
+        self._updateExportStepsRegistry(context, encoding)
+
+    security.declareProtected(ManagePortal, 'getImportStepRegistry')
+    def getImportStepRegistry(self):
+
+        """ See ISetupTool.
+        """
+        return self._import_registry
+
+    security.declareProtected(ManagePortal, 'getExportStepRegistry')
+    def getExportStepRegistry(self):
+
+        """ See ISetupTool.
+        """
+        return self._export_registry
+
+
+    security.declareProtected(ManagePortal, 'getExportStep')
+    def getExportStep(self, step, default=None):
+        """Simple wrapper to query both the global and local step registry."""
+        res=_export_step_registry.getStep(step, default)
+        if res is not default:
+            return res
+        return self._export_registry.getStep(step, default)
+
+
+    security.declareProtected(ManagePortal, 'listExportSteps')
+    def listExportSteps(self):
+        steps = _export_step_registry._registered.keys() + \
+                self._export_registry._registered.keys()
+        return steps
+
+
+    security.declareProtected(ManagePortal, 'getImportStep')
+    def getImportStep(self, step, default=None):
+        """Simple wrapper to query both the global and local step registry."""
+        res=_import_step_registry.getStep(step, default)
+        if res is not default:
+            return res
+        return self._import_registry.getStep(step, default)
+
+
+    security.declareProtected(ManagePortal, 'getSortedImportSteps')
+    def getSortedImportSteps(self):
+        steps = _import_step_registry._registered.values() + \
+                self._import_registry._registered.values()
+        return _computeTopologicalSort(steps)
+    
+    security.declareProtected(ManagePortal, 'getImportStep')
+    def getImportStepMetadata(self, step, default=None):
+        """Simple wrapper to query both the global and local step registry."""
+        res=_import_step_registry.getStepMetadata(step, default)
+        if res is not default:
+            return res
+        return self._import_registry.getStepMetadata(step, default)
+
+
+    security.declareProtected(ManagePortal, 'getToolsetRegistry')
+    def getToolsetRegistry(self):
+
+        """ See ISetupTool.
+        """
+        return self._toolset_registry
+
+    security.declareProtected(ManagePortal, 'runImportStepFromProfile')
+    def runImportStepFromProfile(self, profile_id, step_id,
+                                 run_dependencies=True, purge_old=None):
+        """ See ISetupTool.
+        """
+        old_context = self._import_context_id
+        context = self._getImportContext(profile_id, purge_old)
+
+        self.applyContext(context)
+
+        info = self.getImportStepMetadata(step_id)
+
+        if info is None:
+            self._import_context_id = old_context
+            raise ValueError, 'No such import step: %s' % step_id
+
+        dependencies = info.get('dependencies', ())
+
+        messages = {}
+        steps = []
+        if run_dependencies:
+            for dependency in dependencies:
+
+                if dependency not in steps:
+                    message = self._doRunImportStep(dependency, context)
+                    messages[dependency] = message or ''
+                    steps.append(dependency)
+
+        message = self._doRunImportStep(step_id, context)
+        message_list = filter(None, [message])
+        message_list.extend( ['%s: %s' % x[1:] for x in context.listNotes()] )
+        messages[step_id] = '\n'.join(message_list)
+        steps.append(step_id)
+
+        self._import_context_id = old_context
+
+        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
+
+        old_context = self._import_context_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'])
+
+        self._import_context_id = old_context
+
+        return result
+
+    security.declareProtected(ManagePortal, 'runAllImportSteps')
+    def runAllImportSteps(self, purge_old=None):
+
+        """ See ISetupTool.
+        """
+        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)
+
+    security.declareProtected(ManagePortal, 'runExportStep')
+    def runExportStep(self, step_id):
+
+        """ See ISetupTool.
+        """
+        return self._doRunExportSteps([step_id])
+
+    security.declareProtected(ManagePortal, 'runAllExportSteps')
+    def runAllExportSteps(self):
+
+        """ See ISetupTool.
+        """
+        return self._doRunExportSteps(self.listExportSteps())
+
+    security.declareProtected(ManagePortal, 'createSnapshot')
+    def createSnapshot(self, snapshot_id):
+
+        """ See ISetupTool.
+        """
+        context = SnapshotExportContext(self, snapshot_id)
+        messages = {}
+        steps = self.listExportSteps()
+
+        for step_id in steps:
+
+            handler = self._export_registry.getStep(step_id)
+
+            if handler is None:
+                logger = logging.getLogger('GenericSetup')
+                logger.error('Step %s has an invalid handler' % step_id)
+                continue
+
+            messages[step_id] = handler(context)
+
+
+        return { 'steps' : steps
+               , 'messages' : messages
+               , 'url' : context.getSnapshotURL()
+               , 'snapshot' : context.getSnapshotFolder()
+               }
+
+    security.declareProtected(ManagePortal, 'compareConfigurations')
+    def compareConfigurations(self,
+                              lhs_context,
+                              rhs_context,
+                              missing_as_empty=False,
+                              ignore_blanks=False,
+                              skip=SKIPPED_FILES,
+                             ):
+        """ See ISetupTool.
+        """
+        differ = ConfigDiff(lhs_context,
+                            rhs_context,
+                            missing_as_empty,
+                            ignore_blanks,
+                            skip,
+                           )
+
+        return differ.compare()
+
+    security.declareProtected(ManagePortal, 'markupComparison')
+    def markupComparison(self, lines):
+
+        """ See ISetupTool.
+        """
+        result = []
+
+        for line in lines.splitlines():
+
+            if line.startswith('** '):
+
+                if line.find('File') > -1:
+                    if line.find('replaced') > -1:
+                        result.append(('file-to-dir', line))
+                    elif line.find('added') > -1:
+                        result.append(('file-added', line))
+                    else:
+                        result.append(('file-removed', line))
+                else:
+                    if line.find('replaced') > -1:
+                        result.append(('dir-to-file', line))
+                    elif line.find('added') > -1:
+                        result.append(('dir-added', line))
+                    else:
+                        result.append(('dir-removed', line))
+
+            elif line.startswith('@@'):
+                result.append(('diff-range', line))
+
+            elif line.startswith(' '):
+                result.append(('diff-context', line))
+
+            elif line.startswith('+'):
+                result.append(('diff-added', line))
+
+            elif line.startswith('-'):
+                result.append(('diff-removed', line))
+
+            elif line == '\ No newline at end of file':
+                result.append(('diff-context', line))
+
+            else:
+                result.append(('diff-header', line))
+
+        return '<pre>\n%s\n</pre>' % (
+            '\n'.join([('<span class="%s">%s</span>' % (cl, escape(l)))
+                                  for cl, l in result]))
+
+    #
+    #   ZMI
+    #
+    manage_options = (Folder.manage_options[:1]
+                    + ({'label' : 'Profiles',
+                        'action' : 'manage_tool'
+                       },
+                       {'label' : 'Import',
+                        'action' : 'manage_importSteps'
+                       },
+                       {'label' : 'Export',
+                        'action' : 'manage_exportSteps'
+                       },
+                       {'label' : 'Upgrades',
+                        'action' : 'manage_upgrades'
+                        },
+                       {'label' : 'Snapshots',
+                        'action' : 'manage_snapshots'
+                       },
+                       {'label' : 'Comparison',
+                        'action' : 'manage_showDiff'
+                       },
+                      )
+                    + Folder.manage_options[3:] # skip "View", "Properties"
+                     )
+
+    security.declareProtected(ManagePortal, 'manage_tool')
+    manage_tool = PageTemplateFile('sutProperties', _wwwdir)
+
+    security.declareProtected(ManagePortal, 'manage_updateToolProperties')
+    def manage_updateToolProperties(self, context_id, RESPONSE):
+        """ Update the tool's settings.
+        """
+        self.setBaselineContext(context_id)
+
+        RESPONSE.redirect('%s/manage_tool?manage_tabs_message=%s'
+                         % (self.absolute_url(), 'Properties+updated.'))
+
+    security.declareProtected(ManagePortal, 'manage_importSteps')
+    manage_importSteps = PageTemplateFile('sutImportSteps', _wwwdir)
+
+    security.declareProtected(ManagePortal, 'manage_importSelectedSteps')
+    def manage_importSelectedSteps(self, ids, run_dependencies, context_id=None):
+        """ Import the steps selected by the user.
+        """
+        messages = {}
+        if not ids:
+            summary = 'No steps selected.'
+
+        else:
+            if context_id is None:
+                context_id = self.getBaselineContextID()
+            steps_run = []
+            for step_id in ids:
+                result = self.runImportStepFromProfile(context_id,
+                                                       step_id,
+                                                       run_dependencies)
+                steps_run.extend(result['steps'])
+                messages.update(result['messages'])
+
+            summary = 'Steps run: %s' % ', '.join(steps_run)
+
+            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, context_id=None):
+
+        """ Import all steps.
+        """
+        if context_id is None:
+            context_id = self.getBaselineContextID()
+        result = self.runAllImportStepsFromProfile(context_id, purge_old=None)
+
+        steps_run = 'Steps run: %s' % ', '.join(result['steps'])
+
+        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)
+
+                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):
+        """ Import steps from the uploaded tarball.
+        """
+        if getattr(tarball, 'read', None) is not None:
+            tarball = tarball.read()
+
+        context = TarballImportContext(tool=self,
+                                       archive_bits=tarball,
+                                       encoding='UTF8',
+                                       should_purge=True,
+                                      )
+        result = self._runImportStepsFromContext(context,
+                                                 purge_old=True)
+        steps_run = 'Steps run: %s' % ', '.join(result['steps'])
+
+        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_exportSteps')
+    manage_exportSteps = PageTemplateFile('sutExportSteps', _wwwdir)
+
+    security.declareProtected(ManagePortal, 'manage_exportSelectedSteps')
+    def manage_exportSelectedSteps(self, ids, RESPONSE):
+
+        """ Export the steps selected by the user.
+        """
+        if not ids:
+            RESPONSE.redirect('%s/manage_exportSteps?manage_tabs_message=%s'
+                             % (self.absolute_url(), 'No+steps+selected.'))
+
+        result = self._doRunExportSteps(ids)
+        RESPONSE.setHeader('Content-type', 'application/x-gzip')
+        RESPONSE.setHeader('Content-disposition',
+                           'attachment; filename=%s' % result['filename'])
+        return result['tarball']
+
+    security.declareProtected(ManagePortal, 'manage_exportAllSteps')
+    def manage_exportAllSteps(self, RESPONSE):
+
+        """ Export all steps.
+        """
+        result = self.runAllExportSteps()
+        RESPONSE.setHeader('Content-type', 'application/x-gzip')
+        RESPONSE.setHeader('Content-disposition',
+                           '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)
+
+    security.declareProtected(ManagePortal, 'listSnapshotInfo')
+    def listSnapshotInfo(self):
+
+        """ Return a list of mappings describing available snapshots.
+
+        o Keys include:
+
+          'id' -- snapshot ID
+
+          'title' -- snapshot title or ID
+
+          'url' -- URL of the snapshot folder
+        """
+        result = []
+        snapshots = self._getOb('snapshots', None)
+
+        if snapshots:
+
+            for id, folder in snapshots.objectItems('Folder'):
+
+                result.append({ 'id' : id
+                               , 'title' : folder.title_or_id()
+                               , 'url' : folder.absolute_url()
+                               })
+        return result
+
+    security.declareProtected(ManagePortal, 'listProfileInfo')
+    def listProfileInfo(self):
+
+        """ Return a list of mappings describing registered profiles.
+        Base profile is listed first, extensions are sorted.
+
+        o Keys include:
+
+          'id' -- profile ID
+
+          'title' -- profile title or ID
+
+          'description' -- description of the profile
+
+          'path' -- path to the profile within its product
+
+          'product' -- name of the registering product
+        """
+        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'],
+                     'type': 'snapshot',
+                   }
+                    for info in self.listSnapshotInfo()]
+        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):
+
+        """ Create a snapshot with the given ID.
+
+        o If no ID is passed, generate one.
+        """
+        if snapshot_id is None:
+            snapshot_id = self._mangleTimestampName('snapshot')
+
+        self.createSnapshot(snapshot_id)
+
+        RESPONSE.redirect('%s/manage_snapshots?manage_tabs_message=%s'
+                         % (self.absolute_url(), 'Snapshot+created.'))
+        return ""
+
+    security.declareProtected(ManagePortal, 'manage_showDiff')
+    manage_showDiff = PageTemplateFile('sutCompare', _wwwdir)
+
+    def manage_downloadDiff(self,
+                            lhs,
+                            rhs,
+                            missing_as_empty,
+                            ignore_blanks,
+                            RESPONSE,
+                           ):
+        """ Crack request vars and call compareConfigurations.
+
+        o Return the result as a 'text/plain' stream, suitable for framing.
+        """
+        comparison = self.manage_compareConfigurations(lhs,
+                                                       rhs,
+                                                       missing_as_empty,
+                                                       ignore_blanks,
+                                                      )
+        RESPONSE.setHeader('Content-Type', 'text/plain')
+        return _PLAINTEXT_DIFF_HEADER % (lhs, rhs, comparison)
+
+    security.declareProtected(ManagePortal, 'manage_compareConfigurations')
+    def manage_compareConfigurations(self,
+                                     lhs,
+                                     rhs,
+                                     missing_as_empty,
+                                     ignore_blanks,
+                                    ):
+        """ Crack request vars and call compareConfigurations.
+        """
+        lhs_context = self._getImportContext(lhs)
+        rhs_context = self._getImportContext(rhs)
+
+        return self.compareConfigurations(lhs_context,
+                                          rhs_context,
+                                          missing_as_empty,
+                                          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
+    #
+    security.declarePrivate('_getProductPath')
+    def _getProductPath(self, product_name):
+
+        """ Return the absolute path of the product's directory.
+        """
+        try:
+            # BBB: for GenericSetup 1.1 style product names
+            product = __import__('Products.%s' % product_name
+                                , globals(), {}, ['initialize'])
+        except ImportError:
+            try:
+                product = __import__(product_name
+                                    , globals(), {}, ['initialize'])
+            except ImportError:
+                raise ValueError('Not a valid product name: %s'
+                                 % product_name)
+
+        return product.__path__[0]
+
+    security.declarePrivate('_getImportContext')
+    def _getImportContext(self, context_id, should_purge=None):
+
+        """ Crack ID and generate appropriate import context.
+        """
+        encoding = self.getEncoding()
+
+        if context_id.startswith('profile-'):
+
+            context_id = context_id[len('profile-'):]
+            info = _profile_registry.getProfileInfo(context_id)
+
+            if info.get('product'):
+                path = os.path.join(self._getProductPath(info['product'])
+                                   , info['path'])
+            else:
+                path = info['path']
+            if should_purge is None:
+                should_purge = (info.get('type') != EXTENSION)
+            return DirectoryImportContext(self, path, should_purge, encoding)
+
+        # else snapshot
+        context_id = context_id[len('snapshot-'):]
+        if should_purge is None:
+            should_purge = True
+        return SnapshotImportContext(self, context_id, should_purge, encoding)
+
+    security.declarePrivate('_updateImportStepsRegistry')
+    def _updateImportStepsRegistry(self, context, encoding):
+
+        """ Update our import steps registry from our profile.
+        """
+        if context is None:
+            context = self._getImportContext(self._import_context_id)
+        xml = context.readDataFile(IMPORT_STEPS_XML)
+        if xml is None:
+            return
+
+        info_list = self._import_registry.parseXML(xml, encoding)
+
+        for step_info in info_list:
+
+            id = step_info['id']
+            version = step_info['version']
+            handler = step_info['handler']
+            dependencies = tuple(step_info.get('dependencies', ()))
+            title = step_info.get('title', id)
+            description = ''.join(step_info.get('description', []))
+
+            self._import_registry.registerStep(id=id,
+                                               version=version,
+                                               handler=handler,
+                                               dependencies=dependencies,
+                                               title=title,
+                                               description=description,
+                                              )
+
+    security.declarePrivate('_updateExportStepsRegistry')
+    def _updateExportStepsRegistry(self, context, encoding):
+
+        """ Update our export steps registry from our profile.
+        """
+        if context is None:
+            context = self._getImportContext(self._import_context_id)
+        xml = context.readDataFile(EXPORT_STEPS_XML)
+        if xml is None:
+            return
+
+        info_list = self._export_registry.parseXML(xml, encoding)
+
+        for step_info in info_list:
+
+            id = step_info['id']
+            handler = step_info['handler']
+            title = step_info.get('title', id)
+            description = ''.join(step_info.get('description', []))
+
+            self._export_registry.registerStep(id=id,
+                                               handler=handler,
+                                               title=title,
+                                               description=description,
+                                              )
+
+    security.declarePrivate('_doRunImportStep')
+    def _doRunImportStep(self, step_id, context):
+
+        """ Run a single import step, using a pre-built context.
+        """
+        __traceback_info__ = step_id
+        marker = object()
+
+        handler = self.getImportStep(step_id)
+
+        if handler is marker:
+            raise ValueError('Invalid import step: %s' % step_id)
+
+        if handler is None:
+            msg = 'Step %s has an invalid import handler' % step_id
+            logger = logging.getLogger('GenericSetup')
+            logger.error(msg)
+            return 'ERROR: ' + msg
+
+        return handler(context)
+
+    security.declarePrivate('_doRunExportSteps')
+    def _doRunExportSteps(self, steps):
+
+        """ See ISetupTool.
+        """
+        context = TarballExportContext(self)
+        messages = {}
+        marker = object()
+
+        for step_id in steps:
+
+            handler = self._export_registry.getStep(step_id, marker)
+
+            if handler is marker:
+                raise ValueError('Invalid export step: %s' % step_id)
+
+            if handler is None:
+                msg = 'Step %s has an invalid import handler' % step_id
+                logger = logging.getLogger('GenericSetup')
+                logger.error(msg)
+                messages[step_id] = msg
+            else:
+                messages[step_id] = handler(context)
+
+        return { 'steps' : steps
+               , 'messages' : messages
+               , 'tarball' : context.getArchive()
+               , 'filename' : context.getArchiveFilename()
+               }
+
+    security.declarePrivate('_runImportStepsFromContext')
+    def _runImportStepsFromContext(self, context, steps=None, purge_old=None):
+        self.applyContext(context)
+
+        if steps is None:
+            steps = self.getSortedImportSteps()
+        messages = {}
+
+        for step in steps:
+            message = self._doRunImportStep(step, context)
+            message_list = filter(None, [message])
+            message_list.extend( ['%s: %s' % x[1:]
+                                  for x in context.listNotes()] )
+            messages[step] = '\n'.join(message_list)
+            context.clearNotes()
+
+        return { 'steps' : steps, 'messages' : messages }
+
+    security.declarePrivate('_mangleTimestampName')
+    def _mangleTimestampName(self, prefix, ext=None):
+
+        """ Create a mangled ID using a timestamp.
+        """
+        timestamp = time.gmtime()
+        items = (prefix,) + timestamp[:6]
+
+        if ext is None:
+            fmt = '%s-%4d%02d%02d%02d%02d%02d'
+        else:
+            fmt = '%s-%4d%02d%02d%02d%02d%02d.%s'
+            items += (ext,)
+
+        return fmt % items
+
+    security.declarePrivate('_createReport')
+    def _createReport(self, name, steps, messages):
+
+        """ Record the results of a run.
+        """
+        lines = []
+        # Create report
+        for step in steps:
+            lines.append('=' * 65)
+            lines.append('Step: %s' % step)
+            lines.append('=' * 65)
+            msg = messages[step]
+            lines.extend(msg.split('\n'))
+            lines.append('')
+
+        report = '\n'.join(lines)
+        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,
+                    content_type='text/plain'
+                   )
+        self._setObject(name, file)
+
+InitializeClass(SetupTool)
+
+_PLAINTEXT_DIFF_HEADER ="""\
+Comparing configurations: '%s' and '%s'
+
+%s"""
+
+_TOOL_ID = 'setup_tool'
+
+addSetupToolForm = PageTemplateFile('toolAdd.zpt', _wwwdir)
+
+def addSetupTool(dispatcher, RESPONSE):
+    """
+    """
+    dispatcher._setObject(_TOOL_ID, SetupTool(_TOOL_ID))
+
+    RESPONSE.redirect('%s/manage_main' % dispatcher.absolute_url())

Deleted: GenericSetup/branches/wichert-events/utils.py
===================================================================
--- GenericSetup/trunk/utils.py	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/utils.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,790 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 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 product utilities
-
-$Id$
-"""
-
-import os
-import sys
-from cgi import escape
-from inspect import getdoc
-from xml.dom.minidom import _nssplit
-from xml.dom.minidom import Document
-from xml.dom.minidom import Element
-from xml.dom.minidom import Node
-from xml.dom.minidom import parseString
-from xml.sax.handler import ContentHandler
-from xml.parsers.expat import ExpatError
-
-import Products
-from AccessControl import ClassSecurityInfo
-from Acquisition import Implicit
-from Globals import InitializeClass
-from Globals import package_home
-from OFS.interfaces import IOrderedContainer
-from zope.component import queryMultiAdapter
-from zope.deprecation import deprecated
-from zope.interface import implements
-from zope.interface import implementsOnly
-from zope.interface import providedBy
-from ZPublisher.HTTPRequest import default_encoding
-
-from exceptions import BadRequest
-from interfaces import IBody
-from interfaces import INode
-from interfaces import ISetupContext
-from interfaces import ISetupTool
-from permissions import ManagePortal
-
-
-_pkgdir = package_home( globals() )
-_wwwdir = os.path.join( _pkgdir, 'www' )
-_xmldir = os.path.join( _pkgdir, 'xml' )
-
-# Please note that these values may change. Always import 
-# the values from here instead of using the values directly.
-CONVERTER, DEFAULT, KEY = 1, 2, 3
-I18NURI = 'http://xml.zope.org/namespaces/i18n'
-
-
-def _getDottedName( named ):
-
-    if isinstance( named, basestring ):
-        return str( named )
-
-    try:
-        dotted = '%s.%s' % (named.__module__, named.__name__)
-    except AttributeError:
-        raise ValueError('Cannot compute dotted name: %s' % named)
-
-    # remove leading underscore names if possible
-
-    # Step 1: check if there is a short version
-    short_dotted = '.'.join([ n for n in dotted.split('.')
-                              if not n.startswith('_') ])
-    if short_dotted == dotted:
-        return dotted
-
-    # Step 2: check if short version can be resolved
-    try:
-        short_resolved = _resolveDottedName(short_dotted)
-    except (ValueError, ImportError):
-        return dotted
-
-    # Step 3: check if long version resolves to the same object
-    try:
-        resolved = _resolveDottedName(dotted)
-    except (ValueError, ImportError):
-        raise ValueError('Cannot compute dotted name: %s' % named)
-    if short_resolved is not resolved:
-        return dotted
-
-    return short_dotted
-
-def _resolveDottedName( dotted ):
-
-    __traceback_info__ = dotted
-
-    parts = dotted.split( '.' )
-
-    if not parts:
-        raise ValueError, "incomplete dotted name: %s" % dotted
-
-    parts_copy = parts[:]
-
-    while parts_copy:
-        try:
-            module = __import__( '.'.join( parts_copy ) )
-            break
-
-        except ImportError:
-            # Reraise if the import error was caused inside the imported file
-            if sys.exc_info()[2].tb_next is not None: raise
-
-            del parts_copy[ -1 ]
-
-            if not parts_copy:
-                return None
-
-    parts = parts[ 1: ] # Funky semantics of __import__'s return value
-
-    obj = module
-
-    for part in parts:
-        try:
-            obj = getattr( obj, part )
-        except AttributeError:
-            return None
-
-    return obj
-
-def _extractDocstring( func, default_title, default_description ):
-
-    try:
-        doc = getdoc( func )
-        lines = doc.split( '\n' )
-
-    except AttributeError:
-
-        title = default_title
-        description = default_description
-
-    else:
-        title = lines[ 0 ]
-
-        if len( lines ) > 1 and lines[ 1 ].strip() == '':
-            del lines[ 1 ]
-
-        description = '\n'.join( lines[ 1: ] )
-
-    return title, description
-
-
-deprecated('HandlerBase',
-           'SAX based XML parsing is no longer supported by GenericSetup. '
-           'HandlerBase will be removed in GenericSetup 1.5.')
-
-class HandlerBase( ContentHandler ):
-
-    _encoding = None
-    _MARKER = object()
-
-    def _extract( self, attrs, key, default=None ):
-
-        result = attrs.get( key, self._MARKER )
-
-        if result is self._MARKER:
-            return default
-
-        return self._encode( result )
-
-    def _extractBoolean( self, attrs, key, default ):
-
-        result = attrs.get( key, self._MARKER )
-
-        if result is self._MARKER:
-            return default
-
-        result = result.lower()
-        return result in ( '1', 'yes', 'true' )
-
-    def _encode( self, content ):
-
-        if self._encoding is None:
-            return content
-
-        return content.encode( self._encoding )
-
-
-##############################################################################
-# WARNING: PLEASE DON'T USE THE CONFIGURATOR PATTERN. THE RELATED BASE CLASSES
-# WILL BECOME DEPRECATED AS SOON AS GENERICSETUP ITSELF NO LONGER USES THEM.
-
-class ImportConfiguratorBase(Implicit):
-    # old code, will become deprecated
-    """ Synthesize data from XML description.
-    """
-    security = ClassSecurityInfo()
-    security.setDefaultAccess('allow')
-
-    def __init__(self, site, encoding=None):
-
-        self._site = site
-        self._encoding = encoding
-
-    security.declareProtected(ManagePortal, 'parseXML')
-    def parseXML(self, xml):
-        """ Pseudo API.
-        """
-        reader = getattr(xml, 'read', None)
-
-        if reader is not None:
-            xml = reader()
-
-        dom = parseString(xml)
-        root = dom.documentElement
-
-        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()
-            if node.nodeName not in nodes_map:
-                raise ValueError('Unknown node: %s' % node.nodeName)
-        node_map = nodes_map[node.nodeName]
-        info = {}
-
-        for name, val in node.attributes.items():
-            key = node_map[name].get( KEY, str(name) )
-            val = self._encoding and val.encode(self._encoding) or val
-            info[key] = val
-
-        for child in node.childNodes:
-            name = child.nodeName
-
-            if name == '#comment':
-                continue
-
-            if not name == '#text':
-                key = node_map[name].get(KEY, str(name) )
-                info[key] = info.setdefault( key, () ) + (
-                                                    self._extractNode(child),)
-
-            elif '#text' in node_map:
-                key = node_map['#text'].get(KEY, 'value')
-                val = child.nodeValue.lstrip()
-                val = self._encoding and val.encode(self._encoding) or val
-                info[key] = info.setdefault(key, '') + val
-
-        for k, v in node_map.items():
-            key = v.get(KEY, k)
-
-            if DEFAULT in v and not key in info:
-                if isinstance( v[DEFAULT], basestring ):
-                    info[key] = v[DEFAULT] % info
-                else:
-                    info[key] = v[DEFAULT]
-
-            elif CONVERTER in v and key in info:
-                info[key] = v[CONVERTER]( info[key] )
-
-            if key is None:
-                info = info[key]
-
-        return info
-
-    def _getSharedImportMapping(self):
-
-        return {
-          'object':
-            { 'i18n:domain':     {},
-              'name':            {KEY: 'id'},
-              'meta_type':       {},
-              'insert-before':   {},
-              'insert-after':    {},
-              'property':        {KEY: 'properties', DEFAULT: ()},
-              'object':          {KEY: 'objects', DEFAULT: ()},
-              'xmlns:i18n':      {} },
-          'property':
-            { 'name':            {KEY: 'id'},
-              '#text':           {KEY: 'value', DEFAULT: ''},
-              'element':         {KEY: 'elements', DEFAULT: ()},
-              'type':            {},
-              'select_variable': {},
-              'i18n:translate':  {} },
-          'element':
-            { 'value':           {KEY: None} },
-          'description':
-            { '#text':           {KEY: None, DEFAULT: ''} } }
-
-    def _convertToBoolean(self, val):
-
-        return val.lower() in ('true', 'yes', '1')
-
-    def _convertToUnique(self, val):
-
-        assert len(val) == 1
-        return val[0]
-
-InitializeClass(ImportConfiguratorBase)
-
-
-class ExportConfiguratorBase(Implicit):
-    # old code, will become deprecated
-    """ Synthesize XML description.
-    """
-    security = ClassSecurityInfo()
-    security.setDefaultAccess('allow')
-
-    def __init__(self, site, encoding=None):
-
-        self._site = site
-        self._encoding = encoding
-        self._template = self._getExportTemplate()
-
-    security.declareProtected(ManagePortal, 'generateXML')
-    def generateXML(self, **kw):
-        """ Pseudo API.
-        """
-        return self._template(**kw)
-
-InitializeClass(ExportConfiguratorBase)
-
-#
-##############################################################################
-
-class _LineWrapper:
-
-    def __init__(self, writer, indent, addindent, newl, max):
-        self._writer = writer
-        self._indent = indent
-        self._addindent = addindent
-        self._newl = newl
-        self._max = max
-        self._length = 0
-        self._queue = self._indent
-
-    def queue(self, text):
-        self._queue += text
-
-    def write(self, text='', enforce=False):
-        self._queue += text
-
-        if 0 < self._length > self._max - len(self._queue):
-            self._writer.write(self._newl)
-            self._length = 0
-            self._queue = '%s%s %s' % (self._indent, self._addindent,
-                                       self._queue)
-
-        if self._queue != self._indent:
-            self._writer.write(self._queue)
-            self._length += len(self._queue)
-            self._queue = ''
-
-        if 0 < self._length and enforce:
-            self._writer.write(self._newl)
-            self._length = 0
-            self._queue = self._indent
-
-
-class _Element(Element):
-
-    """minidom element with 'pretty' XML output.
-    """
-
-    def writexml(self, writer, indent="", addindent="", newl=""):
-        # indent = current indentation
-        # addindent = indentation to add to higher levels
-        # newl = newline string
-        wrapper = _LineWrapper(writer, indent, addindent, newl, 78)
-        wrapper.write('<%s' % self.tagName)
-
-        # move 'name', 'meta_type' and 'title' to the top, sort the rest 
-        attrs = self._get_attributes()
-        a_names = attrs.keys()
-        a_names.sort()
-        if 'title' in a_names:
-            a_names.remove('title')
-            a_names.insert(0, 'title')
-        if 'meta_type' in a_names:
-            a_names.remove('meta_type')
-            a_names.insert(0, 'meta_type')
-        if 'name' in a_names:
-            a_names.remove('name')
-            a_names.insert(0, 'name')
-
-        for a_name in a_names:
-            wrapper.write()
-            a_value = escape(attrs[a_name].value.encode('utf-8'), quote=True)
-            wrapper.queue(' %s="%s"' % (a_name, a_value))
-
-        if self.childNodes:
-            wrapper.queue('>')
-            for node in self.childNodes:
-                if node.nodeType == Node.TEXT_NODE:
-                    data = escape(node.data.encode('utf-8'))
-                    textlines = data.splitlines()
-                    if textlines:
-                        wrapper.queue(textlines.pop(0))
-                    if textlines:
-                        for textline in textlines:
-                            wrapper.write('', True)
-                            wrapper.queue('%s%s' % (addindent, textline))
-                else:
-                    wrapper.write('', True)
-                    node.writexml(writer, indent+addindent, addindent, newl)
-            wrapper.write('</%s>' % self.tagName, True)
-        else:
-            wrapper.write('/>', True)
-
-
-class PrettyDocument(Document):
-
-    """minidom document with 'pretty' XML output.
-    """
-
-    def createElement(self, tagName):
-        e = _Element(tagName)
-        e.ownerDocument = self
-        return e
-
-    def createElementNS(self, namespaceURI, qualifiedName):
-        prefix, localName = _nssplit(qualifiedName)
-        e = _Element(qualifiedName, namespaceURI, prefix)
-        e.ownerDocument = self
-        return e
-
-    def writexml(self, writer, indent="", addindent="", newl="",
-                 encoding = None):
-        if encoding is None:
-            writer.write('<?xml version="1.0"?>\n')
-        else:
-            writer.write('<?xml version="1.0" encoding="%s"?>\n' % encoding)
-        for node in self.childNodes:
-            node.writexml(writer, indent, addindent, newl)
-
-
-class NodeAdapterBase(object):
-
-    """Node im- and exporter base.
-    """
-
-    implements(INode)
-
-    _LOGGER_ID = ''
-
-    def __init__(self, context, environ):
-        self.context = context
-        self.environ = environ
-        self._logger = environ.getLogger(self._LOGGER_ID)
-        self._doc = PrettyDocument()
-
-    def _getObjectNode(self, name, i18n=True):
-        node = self._doc.createElement(name)
-        node.setAttribute('name', self.context.getId())
-        node.setAttribute('meta_type', self.context.meta_type)
-        i18n_domain = getattr(self.context, 'i18n_domain', None)
-        if i18n and i18n_domain:
-            node.setAttributeNS(I18NURI, 'i18n:domain', i18n_domain)
-            self._i18n_props = ('title', 'description')
-        return node
-
-    def _getNodeText(self, node):
-        text = ''
-        for child in node.childNodes:
-            if child.nodeName != '#text':
-                continue
-            lines = [ line.lstrip() for line in child.nodeValue.splitlines() ]
-            text += '\n'.join(lines)
-        return text
-
-    def _convertToBoolean(self, val):
-        return val.lower() in ('true', 'yes', '1')
-
-
-class BodyAdapterBase(NodeAdapterBase):
-
-    """Body im- and exporter base.
-    """
-
-    implementsOnly(IBody)
-
-    def _exportSimpleNode(self):
-        """Export the object as a DOM node.
-        """
-        if ISetupTool.providedBy(self.context):
-            return None
-        return self._getObjectNode('object', False)
-
-    def _importSimpleNode(self, node):
-        """Import the object from the DOM node.
-        """
-
-    node = property(_exportSimpleNode, _importSimpleNode)
-
-    def _exportBody(self):
-        """Export the object as a file body.
-        """
-        return ''
-
-    def _importBody(self, body):
-        """Import the object from the file body.
-        """
-
-    body = property(_exportBody, _importBody)
-
-    mime_type = 'text/plain'
-
-    name = ''
-
-    suffix = ''
-
-
-class XMLAdapterBase(BodyAdapterBase):
-
-    """XML im- and exporter base.
-    """
-
-    implementsOnly(IBody)
-
-    def _exportBody(self):
-        """Export the object as a file body.
-        """
-        self._doc.appendChild(self._exportNode())
-        xml = self._doc.toprettyxml(' ')
-        self._doc.unlink()
-        return xml
-
-    def _importBody(self, body):
-        """Import the object from the file body.
-        """
-        try:
-            dom = parseString(body)
-        except ExpatError, e:
-            filename = (self.filename or
-                        '/'.join(self.context.getPhysicalPath()))
-            raise ExpatError('%s: %s' % (filename, e))
-        self._importNode(dom.documentElement)
-
-    body = property(_exportBody, _importBody)
-
-    mime_type = 'text/xml'
-
-    name = ''
-
-    suffix = '.xml'
-
-    filename = '' # for error reporting during import
-
-
-class ObjectManagerHelpers(object):
-
-    """ObjectManager im- and export helpers.
-    """
-
-    def _extractObjects(self):
-        fragment = self._doc.createDocumentFragment()
-        objects = self.context.objectValues()
-        if not IOrderedContainer.providedBy(self.context):
-            objects = list(objects)
-            objects.sort(lambda x,y: cmp(x.getId(), y.getId()))
-        for obj in objects:
-            exporter = queryMultiAdapter((obj, self.environ), INode)
-            if exporter:
-                node = exporter.node
-                if node is not None:
-                    fragment.appendChild(exporter.node)
-        return fragment
-
-    def _purgeObjects(self):
-        for obj_id, obj in self.context.objectItems():
-            if ISetupTool.providedBy(obj):
-                continue
-            self.context._delObject(obj_id)
-
-    def _initObjects(self, node):
-        for child in node.childNodes:
-            if child.nodeName != 'object':
-                continue
-            if child.hasAttribute('deprecated'):
-                continue
-            parent = self.context
-
-            obj_id = str(child.getAttribute('name'))
-            if child.hasAttribute('remove'):
-                parent._delObject(obj_id)
-                continue
-
-            if obj_id not in parent.objectIds():
-                meta_type = str(child.getAttribute('meta_type'))
-                __traceback_info__ = obj_id, meta_type
-                for mt_info in Products.meta_types:
-                    if mt_info['name'] == meta_type:
-                        parent._setObject(obj_id, mt_info['instance'](obj_id))
-                        break
-                else:
-                    raise ValueError("unknown meta_type '%s'" % meta_type)
-
-            if child.hasAttribute('insert-before'):
-                insert_before = child.getAttribute('insert-before')
-                if insert_before == '*':
-                    parent.moveObjectsToTop(obj_id)
-                else:
-                    try:
-                        position = parent.getObjectPosition(insert_before)
-                        if parent.getObjectPosition(obj_id) < position:
-                            position -= 1
-                        parent.moveObjectToPosition(obj_id, position)
-                    except ValueError:
-                        pass
-            elif child.hasAttribute('insert-after'):
-                insert_after = child.getAttribute('insert-after')
-                if insert_after == '*':
-                    parent.moveObjectsToBottom(obj_id)
-                else:
-                    try:
-                        position = parent.getObjectPosition(insert_after)
-                        if parent.getObjectPosition(obj_id) < position:
-                            position -= 1
-                        parent.moveObjectToPosition(obj_id, position+1)
-                    except ValueError:
-                        pass
-
-            obj = getattr(self.context, obj_id)
-            importer = queryMultiAdapter((obj, self.environ), INode)
-            if importer:
-                importer.node = child
-
-
-class PropertyManagerHelpers(object):
-
-    """PropertyManager im- and export helpers.
-    """
-
-    _encoding = default_encoding
-
-    def _extractProperties(self):
-        fragment = self._doc.createDocumentFragment()
-
-        for prop_map in self.context._propertyMap():
-            prop_id = prop_map['id']
-            if prop_id == 'i18n_domain':
-                continue
-
-            # Don't export read-only nodes
-            if 'w' not in prop_map.get('mode', 'wd'):
-                continue
-
-            node = self._doc.createElement('property')
-            node.setAttribute('name', prop_id)
-
-            prop = self.context.getProperty(prop_id)
-            if isinstance(prop, (tuple, list)):
-                for value in prop:
-                    if isinstance(value, str):
-                        value.decode(self._encoding)
-                    child = self._doc.createElement('element')
-                    child.setAttribute('value', value)
-                    node.appendChild(child)
-            else:
-                if prop_map.get('type') == 'boolean':
-                    prop = unicode(bool(prop))
-                elif isinstance(prop, str):
-                    prop = prop.decode(self._encoding)
-                elif not isinstance(prop, basestring):
-                    prop = unicode(prop)
-                child = self._doc.createTextNode(prop)
-                node.appendChild(child)
-
-            if 'd' in prop_map.get('mode', 'wd') and not prop_id == 'title':
-                prop_type = prop_map.get('type', 'string')
-                node.setAttribute('type', unicode(prop_type))
-                select_variable = prop_map.get('select_variable', None)
-                if select_variable is not None:
-                    node.setAttribute('select_variable', select_variable)
-
-            if hasattr(self, '_i18n_props') and prop_id in self._i18n_props:
-                node.setAttribute('i18n:translate', '')
-
-            fragment.appendChild(node)
-
-        return fragment
-
-    def _purgeProperties(self):
-        for prop_map in self.context._propertyMap():
-            mode = prop_map.get('mode', 'wd')
-            if 'w' not in mode:
-                continue
-            prop_id = prop_map['id']
-            if 'd' in mode and not prop_id == 'title':
-                self.context._delProperty(prop_id)
-            else:
-                prop_type = prop_map.get('type')
-                if prop_type == 'multiple selection':
-                    prop_value = ()
-                elif prop_type in ('int', 'float'):
-                    prop_value = 0
-                else:
-                    prop_value = ''
-                self.context._updateProperty(prop_id, prop_value)
-
-    def _initProperties(self, node):
-        obj = self.context
-        if node.hasAttribute('i18n:domain'):
-            i18n_domain = str(node.getAttribute('i18n:domain'))
-            obj._updateProperty('i18n_domain', i18n_domain)
-        for child in node.childNodes:
-            if child.nodeName != 'property':
-                continue
-            prop_id = str(child.getAttribute('name'))
-            prop_map = obj.propdict().get(prop_id, None)
-
-            if prop_map is None:
-                if child.hasAttribute('type'):
-                    val = str(child.getAttribute('select_variable'))
-                    prop_type = str(child.getAttribute('type'))
-                    obj._setProperty(prop_id, val, prop_type)
-                    prop_map = obj.propdict().get(prop_id, None)
-                else:
-                    raise ValueError("undefined property '%s'" % prop_id)
-
-            if not 'w' in prop_map.get('mode', 'wd'):
-                raise BadRequest('%s cannot be changed' % prop_id)
-
-            elements = []
-            for sub in child.childNodes:
-                if sub.nodeName == 'element':
-                    value = sub.getAttribute('value')
-                    elements.append(value.encode(self._encoding))
-
-            if elements or prop_map.get('type') == 'multiple selection':
-                prop_value = tuple(elements) or ()
-            elif prop_map.get('type') == 'boolean':
-                prop_value = self._convertToBoolean(self._getNodeText(child))
-            else:
-                # if we pass a *string* to _updateProperty, all other values
-                # are converted to the right type
-                prop_value = self._getNodeText(child).encode(self._encoding)
-
-            if not self._convertToBoolean(child.getAttribute('purge')
-                                          or 'True'):
-                # If the purge attribute is False, merge sequences
-                prop = obj.getProperty(prop_id)
-                if isinstance(prop, (tuple, list)):
-                    prop_value = (tuple([p for p in prop
-                                         if p not in prop_value]) +
-                                  tuple(prop_value))
-
-            obj._updateProperty(prop_id, prop_value)
-
-
-def exportObjects(obj, parent_path, context):
-    """ Export subobjects recursively.
-    """
-    exporter = queryMultiAdapter((obj, context), IBody)
-    path = '%s%s' % (parent_path, obj.getId().replace(' ', '_'))
-    if exporter:
-        if exporter.name:
-            path = '%s%s' % (parent_path, exporter.name)
-        filename = '%s%s' % (path, exporter.suffix)
-        body = exporter.body
-        if body is not None:
-            context.writeDataFile(filename, body, exporter.mime_type)
-
-    if getattr(obj, 'objectValues', False):
-        for sub in obj.objectValues():
-            exportObjects(sub, path+'/', context)
-
-def importObjects(obj, parent_path, context):
-    """ Import subobjects recursively.
-    """
-    importer = queryMultiAdapter((obj, context), IBody)
-    path = '%s%s' % (parent_path, obj.getId().replace(' ', '_'))
-    __traceback_info__ = path
-    if importer:
-        if importer.name:
-            path = '%s%s' % (parent_path, importer.name)
-        filename = '%s%s' % (path, importer.suffix)
-        body = context.readDataFile(filename)
-        if body is not None:
-            importer.filename = filename # for error reporting
-            importer.body = body
-
-    if getattr(obj, 'objectValues', False):
-        for sub in obj.objectValues():
-            importObjects(sub, path+'/', context)

Copied: GenericSetup/branches/wichert-events/utils.py (from rev 81471, GenericSetup/trunk/utils.py)
===================================================================
--- GenericSetup/branches/wichert-events/utils.py	                        (rev 0)
+++ GenericSetup/branches/wichert-events/utils.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,828 @@
+##############################################################################
+#
+# Copyright (c) 2004 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 product utilities
+
+$Id$
+"""
+
+import os
+import sys
+from cgi import escape
+from inspect import getdoc
+from xml.dom.minidom import _nssplit
+from xml.dom.minidom import Document
+from xml.dom.minidom import Element
+from xml.dom.minidom import Node
+from xml.dom.minidom import parseString
+from xml.sax.handler import ContentHandler
+from xml.parsers.expat import ExpatError
+
+import Products
+from AccessControl import ClassSecurityInfo
+from Acquisition import Implicit
+from Globals import InitializeClass
+from Globals import package_home
+from OFS.interfaces import IOrderedContainer
+from zope.component import queryMultiAdapter
+from zope.deprecation import deprecated
+from zope.interface import implements
+from zope.interface import implementsOnly
+from zope.interface import providedBy
+from ZPublisher.HTTPRequest import default_encoding
+
+from exceptions import BadRequest
+from interfaces import IBody
+from interfaces import INode
+from interfaces import ISetupContext
+from interfaces import ISetupTool
+from permissions import ManagePortal
+
+
+_pkgdir = package_home( globals() )
+_wwwdir = os.path.join( _pkgdir, 'www' )
+_xmldir = os.path.join( _pkgdir, 'xml' )
+
+# Please note that these values may change. Always import 
+# the values from here instead of using the values directly.
+CONVERTER, DEFAULT, KEY = 1, 2, 3
+I18NURI = 'http://xml.zope.org/namespaces/i18n'
+
+
+def _getDottedName( named ):
+
+    if isinstance( named, basestring ):
+        return str( named )
+
+    try:
+        dotted = '%s.%s' % (named.__module__, named.__name__)
+    except AttributeError:
+        raise ValueError('Cannot compute dotted name: %s' % named)
+
+    # remove leading underscore names if possible
+
+    # Step 1: check if there is a short version
+    short_dotted = '.'.join([ n for n in dotted.split('.')
+                              if not n.startswith('_') ])
+    if short_dotted == dotted:
+        return dotted
+
+    # Step 2: check if short version can be resolved
+    try:
+        short_resolved = _resolveDottedName(short_dotted)
+    except (ValueError, ImportError):
+        return dotted
+
+    # Step 3: check if long version resolves to the same object
+    try:
+        resolved = _resolveDottedName(dotted)
+    except (ValueError, ImportError):
+        raise ValueError('Cannot compute dotted name: %s' % named)
+    if short_resolved is not resolved:
+        return dotted
+
+    return short_dotted
+
+def _resolveDottedName( dotted ):
+
+    __traceback_info__ = dotted
+
+    parts = dotted.split( '.' )
+
+    if not parts:
+        raise ValueError, "incomplete dotted name: %s" % dotted
+
+    parts_copy = parts[:]
+
+    while parts_copy:
+        try:
+            module = __import__( '.'.join( parts_copy ) )
+            break
+
+        except ImportError:
+            # Reraise if the import error was caused inside the imported file
+            if sys.exc_info()[2].tb_next is not None: raise
+
+            del parts_copy[ -1 ]
+
+            if not parts_copy:
+                return None
+
+    parts = parts[ 1: ] # Funky semantics of __import__'s return value
+
+    obj = module
+
+    for part in parts:
+        try:
+            obj = getattr( obj, part )
+        except AttributeError:
+            return None
+
+    return obj
+
+def _extractDocstring( func, default_title, default_description ):
+
+    try:
+        doc = getdoc( func )
+        lines = doc.split( '\n' )
+
+    except AttributeError:
+
+        title = default_title
+        description = default_description
+
+    else:
+        title = lines[ 0 ]
+
+        if len( lines ) > 1 and lines[ 1 ].strip() == '':
+            del lines[ 1 ]
+
+        description = '\n'.join( lines[ 1: ] )
+
+    return title, description
+
+
+deprecated('HandlerBase',
+           'SAX based XML parsing is no longer supported by GenericSetup. '
+           'HandlerBase will be removed in GenericSetup 1.5.')
+
+class HandlerBase( ContentHandler ):
+
+    _encoding = None
+    _MARKER = object()
+
+    def _extract( self, attrs, key, default=None ):
+
+        result = attrs.get( key, self._MARKER )
+
+        if result is self._MARKER:
+            return default
+
+        return self._encode( result )
+
+    def _extractBoolean( self, attrs, key, default ):
+
+        result = attrs.get( key, self._MARKER )
+
+        if result is self._MARKER:
+            return default
+
+        result = result.lower()
+        return result in ( '1', 'yes', 'true' )
+
+    def _encode( self, content ):
+
+        if self._encoding is None:
+            return content
+
+        return content.encode( self._encoding )
+
+
+##############################################################################
+# WARNING: PLEASE DON'T USE THE CONFIGURATOR PATTERN. THE RELATED BASE CLASSES
+# WILL BECOME DEPRECATED AS SOON AS GENERICSETUP ITSELF NO LONGER USES THEM.
+
+class ImportConfiguratorBase(Implicit):
+    # old code, will become deprecated
+    """ Synthesize data from XML description.
+    """
+    security = ClassSecurityInfo()
+    security.setDefaultAccess('allow')
+
+    def __init__(self, site, encoding=None):
+
+        self._site = site
+        self._encoding = encoding
+
+    security.declareProtected(ManagePortal, 'parseXML')
+    def parseXML(self, xml):
+        """ Pseudo API.
+        """
+        reader = getattr(xml, 'read', None)
+
+        if reader is not None:
+            xml = reader()
+
+        dom = parseString(xml)
+        root = dom.documentElement
+
+        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()
+            if node.nodeName not in nodes_map:
+                raise ValueError('Unknown node: %s' % node.nodeName)
+        node_map = nodes_map[node.nodeName]
+        info = {}
+
+        for name, val in node.attributes.items():
+            key = node_map[name].get( KEY, str(name) )
+            val = self._encoding and val.encode(self._encoding) or val
+            info[key] = val
+
+        for child in node.childNodes:
+            name = child.nodeName
+
+            if name == '#comment':
+                continue
+
+            if not name == '#text':
+                key = node_map[name].get(KEY, str(name) )
+                info[key] = info.setdefault( key, () ) + (
+                                                    self._extractNode(child),)
+
+            elif '#text' in node_map:
+                key = node_map['#text'].get(KEY, 'value')
+                val = child.nodeValue.lstrip()
+                val = self._encoding and val.encode(self._encoding) or val
+                info[key] = info.setdefault(key, '') + val
+
+        for k, v in node_map.items():
+            key = v.get(KEY, k)
+
+            if DEFAULT in v and not key in info:
+                if isinstance( v[DEFAULT], basestring ):
+                    info[key] = v[DEFAULT] % info
+                else:
+                    info[key] = v[DEFAULT]
+
+            elif CONVERTER in v and key in info:
+                info[key] = v[CONVERTER]( info[key] )
+
+            if key is None:
+                info = info[key]
+
+        return info
+
+    def _getSharedImportMapping(self):
+
+        return {
+          'object':
+            { 'i18n:domain':     {},
+              'name':            {KEY: 'id'},
+              'meta_type':       {},
+              'insert-before':   {},
+              'insert-after':    {},
+              'property':        {KEY: 'properties', DEFAULT: ()},
+              'object':          {KEY: 'objects', DEFAULT: ()},
+              'xmlns:i18n':      {} },
+          'property':
+            { 'name':            {KEY: 'id'},
+              '#text':           {KEY: 'value', DEFAULT: ''},
+              'element':         {KEY: 'elements', DEFAULT: ()},
+              'type':            {},
+              'select_variable': {},
+              'i18n:translate':  {} },
+          'element':
+            { 'value':           {KEY: None} },
+          'description':
+            { '#text':           {KEY: None, DEFAULT: ''} } }
+
+    def _convertToBoolean(self, val):
+
+        return val.lower() in ('true', 'yes', '1')
+
+    def _convertToUnique(self, val):
+
+        assert len(val) == 1
+        return val[0]
+
+InitializeClass(ImportConfiguratorBase)
+
+
+class ExportConfiguratorBase(Implicit):
+    # old code, will become deprecated
+    """ Synthesize XML description.
+    """
+    security = ClassSecurityInfo()
+    security.setDefaultAccess('allow')
+
+    def __init__(self, site, encoding=None):
+
+        self._site = site
+        self._encoding = encoding
+        self._template = self._getExportTemplate()
+
+    security.declareProtected(ManagePortal, 'generateXML')
+    def generateXML(self, **kw):
+        """ Pseudo API.
+        """
+        return self._template(**kw)
+
+InitializeClass(ExportConfiguratorBase)
+
+#
+##############################################################################
+
+class _LineWrapper:
+
+    def __init__(self, writer, indent, addindent, newl, max):
+        self._writer = writer
+        self._indent = indent
+        self._addindent = addindent
+        self._newl = newl
+        self._max = max
+        self._length = 0
+        self._queue = self._indent
+
+    def queue(self, text):
+        self._queue += text
+
+    def write(self, text='', enforce=False):
+        self._queue += text
+
+        if 0 < self._length > self._max - len(self._queue):
+            self._writer.write(self._newl)
+            self._length = 0
+            self._queue = '%s%s %s' % (self._indent, self._addindent,
+                                       self._queue)
+
+        if self._queue != self._indent:
+            self._writer.write(self._queue)
+            self._length += len(self._queue)
+            self._queue = ''
+
+        if 0 < self._length and enforce:
+            self._writer.write(self._newl)
+            self._length = 0
+            self._queue = self._indent
+
+
+class _Element(Element):
+
+    """minidom element with 'pretty' XML output.
+    """
+
+    def writexml(self, writer, indent="", addindent="", newl=""):
+        # indent = current indentation
+        # addindent = indentation to add to higher levels
+        # newl = newline string
+        wrapper = _LineWrapper(writer, indent, addindent, newl, 78)
+        wrapper.write('<%s' % self.tagName)
+
+        # move 'name', 'meta_type' and 'title' to the top, sort the rest 
+        attrs = self._get_attributes()
+        a_names = attrs.keys()
+        a_names.sort()
+        if 'title' in a_names:
+            a_names.remove('title')
+            a_names.insert(0, 'title')
+        if 'meta_type' in a_names:
+            a_names.remove('meta_type')
+            a_names.insert(0, 'meta_type')
+        if 'name' in a_names:
+            a_names.remove('name')
+            a_names.insert(0, 'name')
+
+        for a_name in a_names:
+            wrapper.write()
+            a_value = escape(attrs[a_name].value.encode('utf-8'), quote=True)
+            wrapper.queue(' %s="%s"' % (a_name, a_value))
+
+        if self.childNodes:
+            wrapper.queue('>')
+            for node in self.childNodes:
+                if node.nodeType == Node.TEXT_NODE:
+                    data = escape(node.data.encode('utf-8'))
+                    textlines = data.splitlines()
+                    if textlines:
+                        wrapper.queue(textlines.pop(0))
+                    if textlines:
+                        for textline in textlines:
+                            wrapper.write('', True)
+                            wrapper.queue('%s%s' % (addindent, textline))
+                else:
+                    wrapper.write('', True)
+                    node.writexml(writer, indent+addindent, addindent, newl)
+            wrapper.write('</%s>' % self.tagName, True)
+        else:
+            wrapper.write('/>', True)
+
+
+class PrettyDocument(Document):
+
+    """minidom document with 'pretty' XML output.
+    """
+
+    def createElement(self, tagName):
+        e = _Element(tagName)
+        e.ownerDocument = self
+        return e
+
+    def createElementNS(self, namespaceURI, qualifiedName):
+        prefix, localName = _nssplit(qualifiedName)
+        e = _Element(qualifiedName, namespaceURI, prefix)
+        e.ownerDocument = self
+        return e
+
+    def writexml(self, writer, indent="", addindent="", newl="",
+                 encoding = None):
+        if encoding is None:
+            writer.write('<?xml version="1.0"?>\n')
+        else:
+            writer.write('<?xml version="1.0" encoding="%s"?>\n' % encoding)
+        for node in self.childNodes:
+            node.writexml(writer, indent, addindent, newl)
+
+
+class NodeAdapterBase(object):
+
+    """Node im- and exporter base.
+    """
+
+    implements(INode)
+
+    _LOGGER_ID = ''
+
+    def __init__(self, context, environ):
+        self.context = context
+        self.environ = environ
+        self._logger = environ.getLogger(self._LOGGER_ID)
+        self._doc = PrettyDocument()
+
+    def _getObjectNode(self, name, i18n=True):
+        node = self._doc.createElement(name)
+        node.setAttribute('name', self.context.getId())
+        node.setAttribute('meta_type', self.context.meta_type)
+        i18n_domain = getattr(self.context, 'i18n_domain', None)
+        if i18n and i18n_domain:
+            node.setAttributeNS(I18NURI, 'i18n:domain', i18n_domain)
+            self._i18n_props = ('title', 'description')
+        return node
+
+    def _getNodeText(self, node):
+        text = ''
+        for child in node.childNodes:
+            if child.nodeName != '#text':
+                continue
+            lines = [ line.lstrip() for line in child.nodeValue.splitlines() ]
+            text += '\n'.join(lines)
+        return text
+
+    def _convertToBoolean(self, val):
+        return val.lower() in ('true', 'yes', '1')
+
+
+class BodyAdapterBase(NodeAdapterBase):
+
+    """Body im- and exporter base.
+    """
+
+    implementsOnly(IBody)
+
+    def _exportSimpleNode(self):
+        """Export the object as a DOM node.
+        """
+        if ISetupTool.providedBy(self.context):
+            return None
+        return self._getObjectNode('object', False)
+
+    def _importSimpleNode(self, node):
+        """Import the object from the DOM node.
+        """
+
+    node = property(_exportSimpleNode, _importSimpleNode)
+
+    def _exportBody(self):
+        """Export the object as a file body.
+        """
+        return ''
+
+    def _importBody(self, body):
+        """Import the object from the file body.
+        """
+
+    body = property(_exportBody, _importBody)
+
+    mime_type = 'text/plain'
+
+    name = ''
+
+    suffix = ''
+
+
+class XMLAdapterBase(BodyAdapterBase):
+
+    """XML im- and exporter base.
+    """
+
+    implementsOnly(IBody)
+
+    def _exportBody(self):
+        """Export the object as a file body.
+        """
+        self._doc.appendChild(self._exportNode())
+        xml = self._doc.toprettyxml(' ')
+        self._doc.unlink()
+        return xml
+
+    def _importBody(self, body):
+        """Import the object from the file body.
+        """
+        try:
+            dom = parseString(body)
+        except ExpatError, e:
+            filename = (self.filename or
+                        '/'.join(self.context.getPhysicalPath()))
+            raise ExpatError('%s: %s' % (filename, e))
+        self._importNode(dom.documentElement)
+
+    body = property(_exportBody, _importBody)
+
+    mime_type = 'text/xml'
+
+    name = ''
+
+    suffix = '.xml'
+
+    filename = '' # for error reporting during import
+
+
+class ObjectManagerHelpers(object):
+
+    """ObjectManager im- and export helpers.
+    """
+
+    def _extractObjects(self):
+        fragment = self._doc.createDocumentFragment()
+        objects = self.context.objectValues()
+        if not IOrderedContainer.providedBy(self.context):
+            objects = list(objects)
+            objects.sort(lambda x,y: cmp(x.getId(), y.getId()))
+        for obj in objects:
+            exporter = queryMultiAdapter((obj, self.environ), INode)
+            if exporter:
+                node = exporter.node
+                if node is not None:
+                    fragment.appendChild(exporter.node)
+        return fragment
+
+    def _purgeObjects(self):
+        for obj_id, obj in self.context.objectItems():
+            if ISetupTool.providedBy(obj):
+                continue
+            self.context._delObject(obj_id)
+
+    def _initObjects(self, node):
+        for child in node.childNodes:
+            if child.nodeName != 'object':
+                continue
+            if child.hasAttribute('deprecated'):
+                continue
+            parent = self.context
+
+            obj_id = str(child.getAttribute('name'))
+            if child.hasAttribute('remove'):
+                parent._delObject(obj_id)
+                continue
+
+            if obj_id not in parent.objectIds():
+                meta_type = str(child.getAttribute('meta_type'))
+                __traceback_info__ = obj_id, meta_type
+                for mt_info in Products.meta_types:
+                    if mt_info['name'] == meta_type:
+                        parent._setObject(obj_id, mt_info['instance'](obj_id))
+                        break
+                else:
+                    raise ValueError("unknown meta_type '%s'" % meta_type)
+
+            if child.hasAttribute('insert-before'):
+                insert_before = child.getAttribute('insert-before')
+                if insert_before == '*':
+                    parent.moveObjectsToTop(obj_id)
+                else:
+                    try:
+                        position = parent.getObjectPosition(insert_before)
+                        if parent.getObjectPosition(obj_id) < position:
+                            position -= 1
+                        parent.moveObjectToPosition(obj_id, position)
+                    except ValueError:
+                        pass
+            elif child.hasAttribute('insert-after'):
+                insert_after = child.getAttribute('insert-after')
+                if insert_after == '*':
+                    parent.moveObjectsToBottom(obj_id)
+                else:
+                    try:
+                        position = parent.getObjectPosition(insert_after)
+                        if parent.getObjectPosition(obj_id) < position:
+                            position -= 1
+                        parent.moveObjectToPosition(obj_id, position+1)
+                    except ValueError:
+                        pass
+
+            obj = getattr(self.context, obj_id)
+            importer = queryMultiAdapter((obj, self.environ), INode)
+            if importer:
+                importer.node = child
+
+
+class PropertyManagerHelpers(object):
+
+    """PropertyManager im- and export helpers.
+    """
+
+    _encoding = default_encoding
+
+    def _extractProperties(self):
+        fragment = self._doc.createDocumentFragment()
+
+        for prop_map in self.context._propertyMap():
+            prop_id = prop_map['id']
+            if prop_id == 'i18n_domain':
+                continue
+
+            # Don't export read-only nodes
+            if 'w' not in prop_map.get('mode', 'wd'):
+                continue
+
+            node = self._doc.createElement('property')
+            node.setAttribute('name', prop_id)
+
+            prop = self.context.getProperty(prop_id)
+            if isinstance(prop, (tuple, list)):
+                for value in prop:
+                    if isinstance(value, str):
+                        value.decode(self._encoding)
+                    child = self._doc.createElement('element')
+                    child.setAttribute('value', value)
+                    node.appendChild(child)
+            else:
+                if prop_map.get('type') == 'boolean':
+                    prop = unicode(bool(prop))
+                elif isinstance(prop, str):
+                    prop = prop.decode(self._encoding)
+                elif not isinstance(prop, basestring):
+                    prop = unicode(prop)
+                child = self._doc.createTextNode(prop)
+                node.appendChild(child)
+
+            if 'd' in prop_map.get('mode', 'wd') and not prop_id == 'title':
+                prop_type = prop_map.get('type', 'string')
+                node.setAttribute('type', unicode(prop_type))
+                select_variable = prop_map.get('select_variable', None)
+                if select_variable is not None:
+                    node.setAttribute('select_variable', select_variable)
+
+            if hasattr(self, '_i18n_props') and prop_id in self._i18n_props:
+                node.setAttribute('i18n:translate', '')
+
+            fragment.appendChild(node)
+
+        return fragment
+
+    def _purgeProperties(self):
+        for prop_map in self.context._propertyMap():
+            mode = prop_map.get('mode', 'wd')
+            if 'w' not in mode:
+                continue
+            prop_id = prop_map['id']
+            if 'd' in mode and not prop_id == 'title':
+                self.context._delProperty(prop_id)
+            else:
+                prop_type = prop_map.get('type')
+                if prop_type == 'multiple selection':
+                    prop_value = ()
+                elif prop_type in ('int', 'float'):
+                    prop_value = 0
+                else:
+                    prop_value = ''
+                self.context._updateProperty(prop_id, prop_value)
+
+    def _initProperties(self, node):
+        obj = self.context
+        if node.hasAttribute('i18n:domain'):
+            i18n_domain = str(node.getAttribute('i18n:domain'))
+            obj._updateProperty('i18n_domain', i18n_domain)
+        for child in node.childNodes:
+            if child.nodeName != 'property':
+                continue
+            prop_id = str(child.getAttribute('name'))
+            prop_map = obj.propdict().get(prop_id, None)
+
+            if prop_map is None:
+                if child.hasAttribute('type'):
+                    val = str(child.getAttribute('select_variable'))
+                    prop_type = str(child.getAttribute('type'))
+                    obj._setProperty(prop_id, val, prop_type)
+                    prop_map = obj.propdict().get(prop_id, None)
+                else:
+                    raise ValueError("undefined property '%s'" % prop_id)
+
+            if not 'w' in prop_map.get('mode', 'wd'):
+                raise BadRequest('%s cannot be changed' % prop_id)
+
+            elements = []
+            for sub in child.childNodes:
+                if sub.nodeName == 'element':
+                    value = sub.getAttribute('value')
+                    elements.append(value.encode(self._encoding))
+
+            if elements or prop_map.get('type') == 'multiple selection':
+                prop_value = tuple(elements) or ()
+            elif prop_map.get('type') == 'boolean':
+                prop_value = self._convertToBoolean(self._getNodeText(child))
+            else:
+                # if we pass a *string* to _updateProperty, all other values
+                # are converted to the right type
+                prop_value = self._getNodeText(child).encode(self._encoding)
+
+            if not self._convertToBoolean(child.getAttribute('purge')
+                                          or 'True'):
+                # If the purge attribute is False, merge sequences
+                prop = obj.getProperty(prop_id)
+                if isinstance(prop, (tuple, list)):
+                    prop_value = (tuple([p for p in prop
+                                         if p not in prop_value]) +
+                                  tuple(prop_value))
+
+            obj._updateProperty(prop_id, prop_value)
+
+
+def exportObjects(obj, parent_path, context):
+    """ Export subobjects recursively.
+    """
+    exporter = queryMultiAdapter((obj, context), IBody)
+    path = '%s%s' % (parent_path, obj.getId().replace(' ', '_'))
+    if exporter:
+        if exporter.name:
+            path = '%s%s' % (parent_path, exporter.name)
+        filename = '%s%s' % (path, exporter.suffix)
+        body = exporter.body
+        if body is not None:
+            context.writeDataFile(filename, body, exporter.mime_type)
+
+    if getattr(obj, 'objectValues', False):
+        for sub in obj.objectValues():
+            exportObjects(sub, path+'/', context)
+
+def importObjects(obj, parent_path, context):
+    """ Import subobjects recursively.
+    """
+    importer = queryMultiAdapter((obj, context), IBody)
+    path = '%s%s' % (parent_path, obj.getId().replace(' ', '_'))
+    __traceback_info__ = path
+    if importer:
+        if importer.name:
+            path = '%s%s' % (parent_path, importer.name)
+        filename = '%s%s' % (path, importer.suffix)
+        body = context.readDataFile(filename)
+        if body is not None:
+            importer.filename = filename # for error reporting
+            importer.body = body
+
+    if getattr(obj, 'objectValues', False):
+        for sub in obj.objectValues():
+            importObjects(sub, path+'/', context)
+
+
+def _computeTopologicalSort( steps ):
+    result = []
+    graph = [ ( x[ 'id' ], x[ 'dependencies' ] ) for x in steps ]
+
+    unresolved = []
+
+    while 1:
+        for node, edges in graph:
+
+            after = -1
+            resolved = 0
+
+            for edge in edges:
+
+                if edge in result:
+                    resolved += 1
+                    after = max( after, result.index( edge ) )
+            
+            if len(edges) > resolved:
+                unresolved.append((node, edges))
+            else:
+                result.insert( after + 1, node )
+
+        if not unresolved:
+            break
+        if len(unresolved) == len(graph):
+            # Nothing was resolved in this loop. There must be circular or
+            # missing dependencies. Just add them to the end. We can't
+            # raise an error, because checkComplete relies on this method.
+            for node, edges in unresolved:
+                result.append(node)
+            break
+        graph = unresolved
+        unresolved = []
+    
+    return result

Deleted: GenericSetup/branches/wichert-events/www/sutExportSteps.zpt
===================================================================
--- GenericSetup/trunk/www/sutExportSteps.zpt	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/www/sutExportSteps.zpt	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,75 +0,0 @@
-<h1 tal:replace="structure here/manage_page_header"> PAGE HEADER </h1>
-<h2 tal:replace="structure here/manage_tabs"> PAGE HEADER </h2>
-
-<h3> Site Configuration Export Steps </h3>
-
-
-<p class="form-help">
-Download selected export steps as tarball.
-</p>
-
-<h3>Available Export Steps</h3>
-
-<form action="." method="POST"
-      tal:attributes="action here/absolute_url" >
-<input type="hidden" name="ids:default:tokens" value="" />
-
-<table cellspacing="0" cellpadding="4">
-
- <thead>
-  <tr class="list-header">
-   <td class="list-item">Sel.</td>
-   <td class="list-item">#</td>
-   <td class="list-item">Title / Description</td>
-   <td class="list-item">Handler</td>
-  </tr>
- </thead>
-
- <tbody tal:define="registry here/getExportStepRegistry;
-                    step_ids registry/listSteps;
-                   ">
-  <tal:loop tal:repeat="step_id step_ids">
-  <tr valign="top"
-      tal:define="info python: registry.getStepMetadata( step_id );"
-      tal:attributes="class python:
-                     repeat[ 'step_id' ].odd and 'row-normal' or 'row-hilite'" >
-   <td class="list-item" width="16">
-    <input type="checkbox" name="ids:list" value="STEP_ID"
-           tal:attributes="value step_id" />
-   </td>
-   <td align="right" class="list-item"
-       tal:content="repeat/step_id/number">1</td>
-   <td class="list-item">
-    <span tal:content="info/title">STEP TITLE</span><br />
-    <em tal:content="info/description">STEP DESCRIPTION</em>
-   </td>
-   <td class="list-item"
-       tal:content="info/handler">DOTTED.NAME</td>
-  </tr>
-  </tal:loop>
-
-  <tr valign="top" class="list-header">
-   <td colspan="4">&nbsp;</td>
-  </tr>
-
-  <tr valign="top">
-   <td />
-   <td colspan="3">
-
-    <input class="form-element" type="submit"
-           name="manage_exportSelectedSteps:method"
-           value=" Export selected steps " />
-      
-    <input class="form-element" type="submit"
-           name="manage_exportAllSteps:method"
-           value=" Export all steps " />
-      
-   </td>
-  </tr>
- </tbody>
-
-</table>
-</form>
-
-
-<h1 tal:replace="structure here/manage_page_footer"> PAGE FOOTER </h1>

Copied: GenericSetup/branches/wichert-events/www/sutExportSteps.zpt (from rev 81471, GenericSetup/trunk/www/sutExportSteps.zpt)
===================================================================
--- GenericSetup/branches/wichert-events/www/sutExportSteps.zpt	                        (rev 0)
+++ GenericSetup/branches/wichert-events/www/sutExportSteps.zpt	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,74 @@
+<h1 tal:replace="structure here/manage_page_header"> PAGE HEADER </h1>
+<h2 tal:replace="structure here/manage_tabs"> PAGE HEADER </h2>
+
+<h3> Site Configuration Export Steps </h3>
+
+
+<p class="form-help">
+Download selected export steps as tarball.
+</p>
+
+<h3>Available Export Steps</h3>
+
+<form action="." method="POST"
+      tal:attributes="action here/absolute_url" >
+<input type="hidden" name="ids:default:tokens" value="" />
+
+<table cellspacing="0" cellpadding="4">
+
+ <thead>
+  <tr class="list-header">
+   <td class="list-item">Sel.</td>
+   <td class="list-item">#</td>
+   <td class="list-item">Title / Description</td>
+   <td class="list-item">Handler</td>
+  </tr>
+ </thead>
+
+ <tbody tal:define="step_ids here/listExportSteps;
+                   ">
+  <tal:loop tal:repeat="step_id step_ids">
+  <tr valign="top"
+      tal:define="info python: here.getExportStepMetadata( step_id );"
+      tal:attributes="class python:
+                     repeat[ 'step_id' ].odd and 'row-normal' or 'row-hilite'" >
+   <td class="list-item" width="16">
+    <input type="checkbox" name="ids:list" value="STEP_ID"
+           tal:attributes="value step_id" />
+   </td>
+   <td align="right" class="list-item"
+       tal:content="repeat/step_id/number">1</td>
+   <td class="list-item">
+    <span tal:content="info/title">STEP TITLE</span><br />
+    <em tal:content="info/description">STEP DESCRIPTION</em>
+   </td>
+   <td class="list-item"
+       tal:content="info/handler">DOTTED.NAME</td>
+  </tr>
+  </tal:loop>
+
+  <tr valign="top" class="list-header">
+   <td colspan="4">&nbsp;</td>
+  </tr>
+
+  <tr valign="top">
+   <td />
+   <td colspan="3">
+
+    <input class="form-element" type="submit"
+           name="manage_exportSelectedSteps:method"
+           value=" Export selected steps " />
+      
+    <input class="form-element" type="submit"
+           name="manage_exportAllSteps:method"
+           value=" Export all steps " />
+      
+   </td>
+  </tr>
+ </tbody>
+
+</table>
+</form>
+
+
+<h1 tal:replace="structure here/manage_page_footer"> PAGE FOOTER </h1>

Deleted: GenericSetup/branches/wichert-events/www/sutImportSteps.zpt
===================================================================
--- GenericSetup/trunk/www/sutImportSteps.zpt	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/www/sutImportSteps.zpt	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,135 +0,0 @@
-<tal:block define="base_context_id context/getBaselineContextID;
-                   context_id request/context_id|base_context_id;
-                   contexts context/listContextInfos;
-                   context_title python:[c['title'] for c in contexts if c['id']==context_id][0];
-                   extension_contexts python:[c for c in contexts if c['type'] in ['extension','snapshot']];
-                   ">
-<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
-<h2 tal:define="manage_tabs_message options/manage_tabs_message | nothing"
-    tal:replace="structure context/manage_tabs">TABS</h2>
-
-<h2> Site Configuration Import Steps </h2>
-
-<p class="form-help">
-This tool allows one to re-run individual steps of the site setup
-procedure, in order to pick up changes since the site was created.
-</p>
-
-<h3>Select Profile or Snapshot</h3>
-
-<form action="." method="POST" id="profileform" tal:attributes="action string:${context/absolute_url}/manage_importSteps">
-<select name="context_id"
-        onchange="document.getElementById('profileform').submit();">
- <option value=""
-         tal:attributes="value base_context_id;
-                         selected python:context_id==base_context_id">
-   Current base profile</option>
- <option value="context-CONTEXT_ID"
-    tal:repeat="context extension_contexts"
-    tal:attributes="value context/id; selected python:context_id==context['id']"
-    tal:content="context/title">CONTEXT_TITLE</option>
-</select>
-  <noscript>
-    <input class="form-element" type="submit"
-           name="manage_importSteps:method"
-           value="Switch profile" />
-  </noscript>
-</form>
-
-
-<h3>Available Import Steps for "<span tal:replace="context_title">Base profile</span>"</h3>
-
-<form action="." method="POST" enctype="multipart/form-data"
-      tal:attributes="action context/absolute_url" >
-<tal:dummy define="dummy python:context.applyContextById(context_id)"/>
-<input type="hidden" name="ids:default:tokens" value="" />
-
-<table cellspacing="0" cellpadding="4">
-
- <thead>
-  <tr class="list-header">
-   <td class="list-item">Sel.</td>
-   <td class="list-item">#</td>
-   <td class="list-item">Title / Description</td>
-   <td class="list-item">Handler</td>
-  </tr>
- </thead>
-
- <tbody tal:define="registry context/getImportStepRegistry;
-                    step_ids registry/sortSteps;
-                   ">
-  <tal:loop tal:repeat="step_id step_ids">
-  <tr valign="top"
-      tal:define="info python: registry.getStepMetadata( step_id );"
-      tal:attributes="class python:
-                     repeat[ 'step_id' ].odd and 'row-normal' or 'row-hilite';
-                     style python:info['invalid'] and 'background: red' or None" >
-   <td class="list-item" width="16">
-    <input type="checkbox" name="ids:list" value="STEP_ID"
-           tal:attributes="value step_id" />
-   </td>
-   <td align="right" class="list-item"
-       tal:content="repeat/step_id/number">1</td>
-   <td class="list-item">
-    <span tal:content="info/title">STEP TITLE</span><br />
-    <em tal:content="info/description">STEP DESCRIPTION</em>
-   </td>
-   <td class="list-item"
-       tal:content="info/handler">DOTTED.NAME</td>
-  </tr>
-  </tal:loop>
-
-  <tr valign="top" class="list-header">
-   <td colspan="4">&nbsp;</td>
-  </tr>
-
-  <tr valign="top">
-   <td />
-   <td colspan="3">
-
-    <input type="hidden" name="context_id" value="" tal:attributes="value context_id"/>
-    <input type="hidden" name="run_dependencies:int:default" value="0" />
-    <input class="form-element" type="checkbox" id="run_dependencies"
-           name="run_dependencies:boolean" value="1" checked="checked" />
-    <label for="run_dependencies">Include dependencies?</label>
-    &nbsp; &nbsp;
-
-    <input class="form-element" type="submit"
-           name="manage_importSelectedSteps:method"
-           value=" Import selected steps " />
-      
-    <input class="form-element" type="submit"
-           name="manage_importAllSteps:method"
-           value=" Import all steps " />
-      
-    <input class="form-element" type="file"
-           name="tarball" />
-    <input class="form-element" type="submit"
-           name="manage_importTarball:method"
-           value=" Import uploaded tarball " />
-   </td>
-  </tr>
- </tbody>
-</table>
-
-<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>
-   <td colspan="3"
-       tal:content="structure python: item[1].replace('\n', '<br />')"
-       >MESSAGE</td>
-  </tr>
-
-</table>
-</form>
-
-<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
-</tal:block>
-

Copied: GenericSetup/branches/wichert-events/www/sutImportSteps.zpt (from rev 81471, GenericSetup/trunk/www/sutImportSteps.zpt)
===================================================================
--- GenericSetup/branches/wichert-events/www/sutImportSteps.zpt	                        (rev 0)
+++ GenericSetup/branches/wichert-events/www/sutImportSteps.zpt	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,134 @@
+<tal:block define="base_context_id context/getBaselineContextID;
+                   context_id request/context_id|base_context_id;
+                   contexts context/listContextInfos;
+                   context_title python:[c['title'] for c in contexts if c['id']==context_id][0];
+                   extension_contexts python:[c for c in contexts if c['type'] in ['extension','snapshot']];
+                   ">
+<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
+<h2 tal:define="manage_tabs_message options/manage_tabs_message | nothing"
+    tal:replace="structure context/manage_tabs">TABS</h2>
+
+<h2> Site Configuration Import Steps </h2>
+
+<p class="form-help">
+This tool allows one to re-run individual steps of the site setup
+procedure, in order to pick up changes since the site was created.
+</p>
+
+<h3>Select Profile or Snapshot</h3>
+
+<form action="." method="POST" id="profileform" tal:attributes="action string:${context/absolute_url}/manage_importSteps">
+<select name="context_id"
+        onchange="document.getElementById('profileform').submit();">
+ <option value=""
+         tal:attributes="value base_context_id;
+                         selected python:context_id==base_context_id">
+   Current base profile</option>
+ <option value="context-CONTEXT_ID"
+    tal:repeat="context extension_contexts"
+    tal:attributes="value context/id; selected python:context_id==context['id']"
+    tal:content="context/title">CONTEXT_TITLE</option>
+</select>
+  <noscript>
+    <input class="form-element" type="submit"
+           name="manage_importSteps:method"
+           value="Switch profile" />
+  </noscript>
+</form>
+
+
+<h3>Available Import Steps for "<span tal:replace="context_title">Base profile</span>"</h3>
+
+<form action="." method="POST" enctype="multipart/form-data"
+      tal:attributes="action context/absolute_url" >
+<tal:dummy define="dummy python:context.applyContextById(context_id)"/>
+<input type="hidden" name="ids:default:tokens" value="" />
+
+<table cellspacing="0" cellpadding="4">
+
+ <thead>
+  <tr class="list-header">
+   <td class="list-item">Sel.</td>
+   <td class="list-item">#</td>
+   <td class="list-item">Title / Description</td>
+   <td class="list-item">Handler</td>
+  </tr>
+ </thead>
+
+ <tbody tal:define="step_ids context/getSortedImportSteps;
+                   ">
+  <tal:loop tal:repeat="step_id step_ids">
+  <tr valign="top"
+      tal:define="info python: context.getImportStepMetadata( step_id );"
+      tal:attributes="class python:
+                     repeat[ 'step_id' ].odd and 'row-normal' or 'row-hilite';
+                     style python:info['invalid'] and 'background: red' or None" >
+   <td class="list-item" width="16">
+    <input type="checkbox" name="ids:list" value="STEP_ID"
+           tal:attributes="value step_id" />
+   </td>
+   <td align="right" class="list-item"
+       tal:content="repeat/step_id/number">1</td>
+   <td class="list-item">
+    <span tal:content="info/title">STEP TITLE</span><br />
+    <em tal:content="info/description">STEP DESCRIPTION</em>
+   </td>
+   <td class="list-item"
+       tal:content="info/handler">DOTTED.NAME</td>
+  </tr>
+  </tal:loop>
+
+  <tr valign="top" class="list-header">
+   <td colspan="4">&nbsp;</td>
+  </tr>
+
+  <tr valign="top">
+   <td />
+   <td colspan="3">
+
+    <input type="hidden" name="context_id" value="" tal:attributes="value context_id"/>
+    <input type="hidden" name="run_dependencies:int:default" value="0" />
+    <input class="form-element" type="checkbox" id="run_dependencies"
+           name="run_dependencies:boolean" value="1" checked="checked" />
+    <label for="run_dependencies">Include dependencies?</label>
+    &nbsp; &nbsp;
+
+    <input class="form-element" type="submit"
+           name="manage_importSelectedSteps:method"
+           value=" Import selected steps " />
+      
+    <input class="form-element" type="submit"
+           name="manage_importAllSteps:method"
+           value=" Import all steps " />
+      
+    <input class="form-element" type="file"
+           name="tarball" />
+    <input class="form-element" type="submit"
+           name="manage_importTarball:method"
+           value=" Import uploaded tarball " />
+   </td>
+  </tr>
+ </tbody>
+</table>
+
+<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>
+   <td colspan="3"
+       tal:content="structure python: item[1].replace('\n', '<br />')"
+       >MESSAGE</td>
+  </tr>
+
+</table>
+</form>
+
+<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
+</tal:block>
+

Deleted: GenericSetup/branches/wichert-events/zcml.py
===================================================================
--- GenericSetup/trunk/zcml.py	2007-11-04 07:07:19 UTC (rev 81470)
+++ GenericSetup/branches/wichert-events/zcml.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -1,191 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2006 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 ZCML directives.
-
-$Id$
-"""
-
-from zope.configuration.fields import GlobalObject
-from zope.configuration.fields import MessageID
-from zope.configuration.fields import Path
-from zope.configuration.fields import PythonIdentifier
-from zope.interface import Interface
-
-from interfaces import BASE
-from registry import _profile_registry
-from upgrade import _upgrade_registry
-
-#### genericsetup:registerProfile
-
-class IRegisterProfileDirective(Interface):
-
-    """Register profiles with the global registry.
-    """
-
-    name = PythonIdentifier(
-        title=u'Name',
-        description=u'',
-        required=True)
-
-    title = MessageID(
-        title=u'Title',
-        description=u'',
-        required=True)
-
-    description = MessageID(
-        title=u'Description',
-        description=u'',
-        required=True)
-
-    directory = Path(
-        title=u'Path',
-        description=u"If not specified 'profiles/<name>' is used.",
-        required=False)
-
-    provides = GlobalObject(
-        title=u'Type',
-        description=u"If not specified 'BASE' is used.",
-        default=BASE,
-        required=False)
-
-    for_ = GlobalObject(
-        title=u'Site Interface',
-        description=u'If not specified the profile is always available.',
-        default=None,
-        required=False)
-
-
-_profile_regs = []
-def registerProfile(_context, name, title, description, directory=None,
-                    provides=BASE, for_=None):
-    """ Add a new profile to the registry.
-    """
-    product = _context.package.__name__
-    if directory is None:
-        directory = 'profiles/%s' % name
-
-    _profile_regs.append('%s:%s' % (product, name))
-
-    _context.action(
-        discriminator = ('registerProfile', product, name),
-        callable = _profile_registry.registerProfile,
-        args = (name, title, description, directory, product, provides, for_)
-        )
-
-
-#### 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:
-        del _profile_registry._profile_info[profile_id]
-        _profile_registry._profile_ids.remove(profile_id)
-    _profile_regs = []
-
-    _upgrade_registry.clear()
-
-
-from zope.testing.cleanup import addCleanUp
-addCleanUp(cleanUp)
-del addCleanUp

Copied: GenericSetup/branches/wichert-events/zcml.py (from rev 81471, GenericSetup/trunk/zcml.py)
===================================================================
--- GenericSetup/branches/wichert-events/zcml.py	                        (rev 0)
+++ GenericSetup/branches/wichert-events/zcml.py	2007-11-04 14:51:55 UTC (rev 81473)
@@ -0,0 +1,319 @@
+
+#
+# Copyright (c) 2006-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 ZCML directives.
+
+$Id$
+"""
+
+from zope.configuration.fields import GlobalObject
+from zope.configuration.fields import MessageID
+from zope.configuration.fields import Path
+from zope.configuration.fields import PythonIdentifier
+from zope.interface import Interface
+
+from interfaces import BASE
+from registry import _import_step_registry
+from registry import _export_step_registry
+from registry import _profile_registry
+from upgrade import _upgrade_registry
+
+#### genericsetup:registerProfile
+
+class IRegisterProfileDirective(Interface):
+
+    """Register profiles with the global registry.
+    """
+
+    name = PythonIdentifier(
+        title=u'Name',
+        description=u'',
+        required=True)
+
+    title = MessageID(
+        title=u'Title',
+        description=u'',
+        required=True)
+
+    description = MessageID(
+        title=u'Description',
+        description=u'',
+        required=True)
+
+    directory = Path(
+        title=u'Path',
+        description=u"If not specified 'profiles/<name>' is used.",
+        required=False)
+
+    provides = GlobalObject(
+        title=u'Type',
+        description=u"If not specified 'BASE' is used.",
+        default=BASE,
+        required=False)
+
+    for_ = GlobalObject(
+        title=u'Site Interface',
+        description=u'If not specified the profile is always available.',
+        default=None,
+        required=False)
+
+
+_profile_regs = []
+def registerProfile(_context, name, title, description, directory=None,
+                    provides=BASE, for_=None):
+    """ Add a new profile to the registry.
+    """
+    product = _context.package.__name__
+    if directory is None:
+        directory = 'profiles/%s' % name
+
+    _profile_regs.append('%s:%s' % (product, name))
+
+    _context.action(
+        discriminator = ('registerProfile', product, name),
+        callable = _profile_registry.registerProfile,
+        args = (name, title, description, directory, product, provides, for_)
+        )
+
+
+#### genericsetup:exportStep
+
+class IExportStepDirective(Interface):
+    name = PythonIdentifier(
+        title=u'Name',
+        description=u'',
+        required=True)
+
+    title = MessageID(
+        title=u'Title',
+        description=u'',
+        required=True)
+
+    description = MessageID(
+        title=u'Description',
+        description=u'',
+        required=True)
+
+    handler = GlobalObject(
+        title=u'Handler',
+        description=u'',
+        required=True)
+
+
+_export_step_regs = []
+
+def exportStep(context, name, handler, title=None, description=None):
+    global _export_step_regs
+    _export_step_regs.append(name)
+
+    context.action(
+        discriminator = ('exportStep', name),
+        callable = _export_step_registry.registerStep,
+        args = (name, handler, title, description),
+        )
+#### genericsetup:importStep
+
+class IImportStepDirective(Interface):
+
+    """Register import steps with the global registry.
+    """
+
+    name = PythonIdentifier(
+        title=u'Name',
+        description=u'',
+        required=True)
+
+    title = MessageID(
+        title=u'Title',
+        description=u'',
+        required=True)
+
+    description = MessageID(
+        title=u'Description',
+        description=u'',
+        required=True)
+
+    handler = GlobalObject(
+        title=u'Handler',
+        description=u'',
+        required=True)
+
+    version = MessageID(
+        title=u'Version',
+        description=u'',
+        required=True)
+
+class IImportStepDependsDirective(Interface):
+    name = PythonIdentifier(
+        title=u'Name',
+        description=u'Name of another import step that has to be run first',
+        required=True)
+
+_import_step_regs = []
+
+class importStep:
+    def __init__(self, context, name, version, title, description, handler):
+        """ Add a new import step to the registry.
+        """
+        self.context=context
+        self.discriminator = ('importStep', name),
+        self.name=name
+        self.version=version
+        self.handler=handler
+        self.title=title
+        self.description=description
+        self.dependencies=()
+
+
+    def depends(self, context, name):
+        self.dependencies+=(name,)
+
+
+    def __call__(self):
+        global _import_step_regs
+        _import_step_regs.append(self.name)
+
+        self.context.action(
+            discriminator = self.discriminator,
+            callable = _import_step_registry.registerStep,
+            args = (self.name, self.version, self.handler, self.dependencies,
+                        self.title, self.description),
+            )
+
+#### 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 cleanUpProfiles():
+    global _profile_regs
+    for profile_id in _profile_regs:
+        del _profile_registry._profile_info[profile_id]
+        _profile_registry._profile_ids.remove(profile_id)
+    _profile_regs = []
+
+    _upgrade_registry.clear()
+
+
+def cleanUpImportSteps():
+    global _import_step_regs
+    for name in  _import_step_regs:
+        try:
+             _import_step_registry.unregisterStep( name )
+        except KeyError:
+            pass
+
+    _import_step_regs=[]
+
+def cleanUpExportSteps():
+    global _export_step_regs
+    for name in  _export_step_regs:
+        try:
+             _export_step_registry.unregisterStep( name )
+        except KeyError:
+            pass
+
+    _export_step_regs=[]
+
+from zope.testing.cleanup import addCleanUp
+addCleanUp(cleanUpProfiles)
+addCleanUp(cleanUpImportSteps)
+addCleanUp(cleanUpExportSteps)
+del addCleanUp



More information about the Checkins mailing list