vendor/CMF/1.6-r41367/GenericSetup

changeset 0:d62b03aaa782 GenericSetup tip

CMF 1.6 r41367 snapshot.
author fguillaume
date Thu, 19 Jan 2006 16:49:24 +0000
parents
children
files CHANGES.txt CREDITS.txt DEPENDENCIES.txt MailHost/__init__.py MailHost/configure.zcml MailHost/exportimport.py MailHost/tests/__init__.py MailHost/tests/test_exportimport.py OFSP/__init__.py OFSP/configure.zcml OFSP/exportimport.py OFSP/tests/__init__.py OFSP/tests/test_exportimport.py PROFILES.txt PluginIndexes/__init__.py PluginIndexes/configure.zcml PluginIndexes/exportimport.py PluginIndexes/tests/__init__.py PluginIndexes/tests/test_exportimport.py PythonScripts/__init__.py PythonScripts/configure.zcml PythonScripts/exportimport.py PythonScripts/interfaces.py PythonScripts/tests/__init__.py PythonScripts/tests/test_exportimport.py README.txt ZCTextIndex/__init__.py ZCTextIndex/configure.zcml ZCTextIndex/exportimport.py ZCTextIndex/tests/__init__.py ZCTextIndex/tests/test_exportimport.py ZCatalog/__init__.py ZCatalog/configure.zcml ZCatalog/exportimport.py ZCatalog/tests/__init__.py ZCatalog/tests/test_exportimport.py __init__.py bbb/__init__.py bbb/registry.py bbb/tool.py browser/__init__.py browser/addWithPresettings.pt browser/utils.py configure.zcml content.py context.py differ.py exceptions.py interfaces.py permissions.py registry.py rolemap.py testing.py tests/__init__.py tests/common.py tests/conformance.py tests/default_profile/export_steps.xml tests/default_profile/import_steps.xml tests/default_profile/profile.ini tests/default_profile/toolset.xml tests/faux_objects.py tests/simple.png tests/test_content.py tests/test_context.py tests/test_differ.py tests/test_registry.py tests/test_rolemap.py tests/test_tool.py tests/test_utils.py tool.py utils.py version.txt www/sutCompare.zpt www/sutExportSteps.zpt www/sutImportSteps.zpt www/sutProperties.zpt www/sutSnapshots.zpt www/tool.png www/toolAdd.zpt xml/esrExport.xml xml/isrExport.xml xml/rmeExport.xml xml/tscExport.xml
diffstat 83 files changed, 13875 insertions(+), 0 deletions(-) [+]
line diff
     1.1 new file mode 100644
     1.2 --- /dev/null
     1.3 +++ b/CHANGES.txt
     1.4 @@ -0,0 +1,65 @@
     1.5 +GenericSetup Product Changelog
     1.6 +
     1.7 +  After GenericSetup 1.0
     1.8 +
     1.9 +    - Made GenericSetup a standalone package independent of the CMF
    1.10 +
    1.11 +    - Added 'for_' argument to profile registry operations.
    1.12 +      A profile may be registered and queried as appropriate to a specific
    1.13 +      site interface;  the default value, 'None', indicates that the profile
    1.14 +      is relevant to any site.  Note that this is essentially an adapter
    1.15 +      lookup;  perhaps we should reimplement it so.
    1.16 +
    1.17 +    - Forward ported changes from GenericSetup 0.11 and 0.12 (which were
    1.18 +      created in a separate repository).
    1.19 +
    1.20 +    - A sequence property with the purge="False" attribute will not be
    1.21 +      purged, but merged (the sequences are treated as sets, which means
    1.22 +      that duplicates are removed). This is useful in extension profiles.
    1.23 +
    1.24 +    - Don't export or purge read-only properties. Correctly purge
    1.25 +      non-deletable int/float properties.
    1.26 +
    1.27 +    - Correctly quote XML on export.
    1.28 +
    1.29 +  GenericSetup 1.0 (2005/09/23)
    1.30 +
    1.31 +    - CVS tag:  GenericSetup-1_0
    1.32 +
    1.33 +    - Forward-ported i18n support from CMF 1.5 branch.
    1.34 +
    1.35 +    - Forward ported BBB for old instances that stored properties as
    1.36 +      lists from CMFSetup.
    1.37 +
    1.38 +    - Forward ported fix for tools with non unique IDs from CMFSetup.
    1.39 +
    1.40 +  GenericSetup-0.12 (2005/08/29)
    1.41 +
    1.42 +    - CVS tag:  GenericSetup-0_12
    1.43 +
    1.44 +    - Import requests now create reports (by default) which record any
    1.45 +      status messages generated by the profile's steps.
    1.46 +
    1.47 +  GenericSetup-0.11 (2005/08/23)
    1.48 +
    1.49 +    - CVS tag:  GenericSetup-0_11
    1.50 +
    1.51 +    - Added report of messages generated by import to the "Import" tab.
    1.52 +
    1.53 +    - Consolidated ISetupContext implementation into base class,
    1.54 +      'SetupContextBase'.
    1.55 +
    1.56 +    - Added 'note', 'listNotes', and 'clearNotes'  methods to ISetupContext,
    1.57 +      to allow plugins to record information about the state of the operation.
    1.58 +
    1.59 +  GenericSetup 0.10 (2005/08/11)
    1.60 +
    1.61 +    - CVS tag:  GenericSetup-0_10
    1.62 +
    1.63 +    - Added TarballImportContext, including full test suite.
    1.64 +
    1.65 +  GenericSetup 0.9 (2005/08/08)
    1.66 +
    1.67 +    - CVS tag:  GenericSetup-0_9
    1.68 +
    1.69 +    - Initial version, cut down from CMFSetup-1.5.3
     2.1 new file mode 100644
     2.2 --- /dev/null
     2.3 +++ b/CREDITS.txt
     2.4 @@ -0,0 +1,7 @@
     2.5 +GenericSetup Credits
     2.6 +
     2.7 +  - Martijn Pieters <mj@zopatista.com> wrote the original version of this
     2.8 +    software while working at Zope Corporation.
     2.9 +
    2.10 +  - He developed his version as part of the "Bonzai" CMS which Zope corp.
    2.11 +    built Boston.com using its Zope4Media Print product.
     3.1 new file mode 100644
     3.2 --- /dev/null
     3.3 +++ b/DEPENDENCIES.txt
     3.4 @@ -0,0 +1,2 @@
     3.5 +Zope >= 2.8.5
     3.6 +Five >= 1.2
     4.1 new file mode 100644
     4.2 --- /dev/null
     4.3 +++ b/MailHost/__init__.py
     4.4 @@ -0,0 +1,16 @@
     4.5 +##############################################################################
     4.6 +#
     4.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
     4.8 +#
     4.9 +# This software is subject to the provisions of the Zope Public License,
    4.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    4.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    4.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    4.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    4.14 +# FOR A PARTICULAR PURPOSE.
    4.15 +#
    4.16 +##############################################################################
    4.17 +"""MailHost support.
    4.18 +
    4.19 +$Id: __init__.py 38762 2005-10-05 10:44:00Z yuppie $
    4.20 +"""
     5.1 new file mode 100644
     5.2 --- /dev/null
     5.3 +++ b/MailHost/configure.zcml
     5.4 @@ -0,0 +1,12 @@
     5.5 +<configure
     5.6 +    xmlns="http://namespaces.zope.org/zope"
     5.7 +    >
     5.8 +
     5.9 +  <adapter
    5.10 +      factory=".exportimport.MailHostXMLAdapter"
    5.11 +      provides="Products.GenericSetup.interfaces.IBody"
    5.12 +      for="Products.MailHost.interfaces.IMailHost
    5.13 +           Products.GenericSetup.interfaces.ISetupEnviron"
    5.14 +      />
    5.15 +
    5.16 +</configure>
     6.1 new file mode 100644
     6.2 --- /dev/null
     6.3 +++ b/MailHost/exportimport.py
     6.4 @@ -0,0 +1,54 @@
     6.5 +##############################################################################
     6.6 +#
     6.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
     6.8 +#
     6.9 +# This software is subject to the provisions of the Zope Public License,
    6.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    6.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    6.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    6.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    6.14 +# FOR A PARTICULAR PURPOSE.
    6.15 +#
    6.16 +##############################################################################
    6.17 +"""MailHost export / import support.
    6.18 +
    6.19 +$Id: exportimport.py 40878 2005-12-18 21:57:56Z yuppie $
    6.20 +"""
    6.21 +
    6.22 +from Products.GenericSetup.utils import XMLAdapterBase
    6.23 +
    6.24 +from Products.MailHost.interfaces import IMailHost
    6.25 +
    6.26 +
    6.27 +class MailHostXMLAdapter(XMLAdapterBase):
    6.28 +
    6.29 +    """XML im- and exporter for MailHost.
    6.30 +    """
    6.31 +
    6.32 +    __used_for__ = IMailHost
    6.33 +
    6.34 +    _LOGGER_ID = 'mailhost'
    6.35 +
    6.36 +    name = 'mailhost'
    6.37 +
    6.38 +    def _exportNode(self):
    6.39 +        """Export the object as a DOM node.
    6.40 +        """
    6.41 +        node = self._getObjectNode('object')
    6.42 +        node.setAttribute('smtp_host', str(self.context.smtp_host))
    6.43 +        node.setAttribute('smtp_port', str(self.context.smtp_port))
    6.44 +        node.setAttribute('smtp_uid', self.context.smtp_uid)
    6.45 +        node.setAttribute('smtp_pwd', self.context.smtp_pwd)
    6.46 +
    6.47 +        self._logger.info('Mailhost exported.')
    6.48 +        return node
    6.49 +
    6.50 +    def _importNode(self, node):
    6.51 +        """Import the object from the DOM node.
    6.52 +        """
    6.53 +        self.context.smtp_host = str(node.getAttribute('smtp_host'))
    6.54 +        self.context.smtp_port = int(node.getAttribute('smtp_port'))
    6.55 +        self.context.smtp_uid = node.getAttribute('smtp_uid').encode('utf-8')
    6.56 +        self.context.smtp_pwd = node.getAttribute('smtp_pwd').encode('utf-8')
    6.57 +
    6.58 +        self._logger.info('Mailhost imported.')
     7.1 new file mode 100644
     7.2 --- /dev/null
     7.3 +++ b/MailHost/tests/__init__.py
     7.4 @@ -0,0 +1,16 @@
     7.5 +##############################################################################
     7.6 +#
     7.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
     7.8 +#
     7.9 +# This software is subject to the provisions of the Zope Public License,
    7.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    7.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    7.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    7.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    7.14 +# FOR A PARTICULAR PURPOSE.
    7.15 +#
    7.16 +##############################################################################
    7.17 +"""MailHost support tests.
    7.18 +
    7.19 +$Id: __init__.py 38762 2005-10-05 10:44:00Z yuppie $
    7.20 +"""
     8.1 new file mode 100644
     8.2 --- /dev/null
     8.3 +++ b/MailHost/tests/test_exportimport.py
     8.4 @@ -0,0 +1,67 @@
     8.5 +##############################################################################
     8.6 +#
     8.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
     8.8 +#
     8.9 +# This software is subject to the provisions of the Zope Public License,
    8.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    8.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    8.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    8.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    8.14 +# FOR A PARTICULAR PURPOSE.
    8.15 +#
    8.16 +##############################################################################
    8.17 +"""MailHost export / import support unit tests.
    8.18 +
    8.19 +$Id: test_exportimport.py 40413 2005-11-29 19:44:07Z yuppie $
    8.20 +"""
    8.21 +
    8.22 +import unittest
    8.23 +import Testing
    8.24 +
    8.25 +from Products.Five import zcml
    8.26 +
    8.27 +from Products.GenericSetup.testing import BodyAdapterTestCase
    8.28 +
    8.29 +_MAILHOST_BODY = """\
    8.30 +<?xml version="1.0"?>
    8.31 +<object name="foo_mailhost" meta_type="Mail Host" smtp_host="localhost"
    8.32 +   smtp_port="25" smtp_pwd="" smtp_uid=""/>
    8.33 +"""
    8.34 +
    8.35 +
    8.36 +class MailHostXMLAdapterTests(BodyAdapterTestCase):
    8.37 +
    8.38 +    def _getTargetClass(self):
    8.39 +        from Products.GenericSetup.MailHost.exportimport \
    8.40 +                import MailHostXMLAdapter
    8.41 +
    8.42 +        return MailHostXMLAdapter
    8.43 +
    8.44 +    def _verifyImport(self, obj):
    8.45 +        self.assertEqual(type(obj.smtp_host), str)
    8.46 +        self.assertEqual(obj.smtp_host, 'localhost')
    8.47 +        self.assertEqual(type(obj.smtp_port), int)
    8.48 +        self.assertEqual(obj.smtp_port, 25)
    8.49 +        self.assertEqual(type(obj.smtp_pwd), str)
    8.50 +        self.assertEqual(obj.smtp_pwd, '')
    8.51 +        self.assertEqual(type(obj.smtp_uid), str)
    8.52 +        self.assertEqual(obj.smtp_uid, '')
    8.53 +
    8.54 +    def setUp(self):
    8.55 +        import Products.GenericSetup.MailHost
    8.56 +        from Products.MailHost.MailHost import MailHost
    8.57 +
    8.58 +        BodyAdapterTestCase.setUp(self)
    8.59 +        zcml.load_config('configure.zcml', Products.GenericSetup.MailHost)
    8.60 +
    8.61 +        self._obj = MailHost('foo_mailhost')
    8.62 +        self._BODY = _MAILHOST_BODY
    8.63 +
    8.64 +
    8.65 +def test_suite():
    8.66 +    return unittest.TestSuite((
    8.67 +        unittest.makeSuite(MailHostXMLAdapterTests),
    8.68 +        ))
    8.69 +
    8.70 +if __name__ == '__main__':
    8.71 +    unittest.main(defaultTest='test_suite')
     9.1 new file mode 100644
     9.2 --- /dev/null
     9.3 +++ b/OFSP/__init__.py
     9.4 @@ -0,0 +1,16 @@
     9.5 +##############################################################################
     9.6 +#
     9.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
     9.8 +#
     9.9 +# This software is subject to the provisions of the Zope Public License,
    9.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    9.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    9.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    9.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    9.14 +# FOR A PARTICULAR PURPOSE.
    9.15 +#
    9.16 +##############################################################################
    9.17 +"""OFS support.
    9.18 +
    9.19 +$Id: __init__.py 40386 2005-11-28 11:38:16Z yuppie $
    9.20 +"""
    10.1 new file mode 100644
    10.2 --- /dev/null
    10.3 +++ b/OFSP/configure.zcml
    10.4 @@ -0,0 +1,12 @@
    10.5 +<configure
    10.6 +    xmlns="http://namespaces.zope.org/zope"
    10.7 +    >
    10.8 +
    10.9 +  <adapter
   10.10 +      factory=".exportimport.FolderXMLAdapter"
   10.11 +      provides="Products.GenericSetup.interfaces.IBody"
   10.12 +      for="OFS.interfaces.IFolder
   10.13 +           Products.GenericSetup.interfaces.ISetupEnviron"
   10.14 +      />
   10.15 +
   10.16 +</configure>
    11.1 new file mode 100644
    11.2 --- /dev/null
    11.3 +++ b/OFSP/exportimport.py
    11.4 @@ -0,0 +1,64 @@
    11.5 +##############################################################################
    11.6 +#
    11.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    11.8 +#
    11.9 +# This software is subject to the provisions of the Zope Public License,
   11.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   11.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   11.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   11.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   11.14 +# FOR A PARTICULAR PURPOSE.
   11.15 +#
   11.16 +##############################################################################
   11.17 +"""OFSP export / import support.
   11.18 +
   11.19 +$Id: exportimport.py 40715 2005-12-12 10:33:40Z yuppie $
   11.20 +"""
   11.21 +
   11.22 +from Products.GenericSetup.utils import XMLAdapterBase
   11.23 +from Products.GenericSetup.utils import ObjectManagerHelpers
   11.24 +from Products.GenericSetup.utils import PropertyManagerHelpers
   11.25 +
   11.26 +from OFS.interfaces import IFolder
   11.27 +
   11.28 +
   11.29 +class FolderXMLAdapter(XMLAdapterBase, ObjectManagerHelpers,
   11.30 +                       PropertyManagerHelpers):
   11.31 +
   11.32 +    """XML im- and exporter for Folder.
   11.33 +    """
   11.34 +
   11.35 +    __used_for__ = IFolder
   11.36 +
   11.37 +    _LOGGER_ID = 'ofs'
   11.38 +
   11.39 +    def _exportNode(self):
   11.40 +        """Export the object as a DOM node.
   11.41 +        """
   11.42 +        node = self._getObjectNode('object')
   11.43 +        node.appendChild(self._extractProperties())
   11.44 +        node.appendChild(self._extractObjects())
   11.45 +
   11.46 +        self._logger.info('Folder exported.')
   11.47 +        return node
   11.48 +
   11.49 +    def _importNode(self, node):
   11.50 +        """Import the object from the DOM node.
   11.51 +        """
   11.52 +        if self.environ.shouldPurge():
   11.53 +            self._purgeProperties()
   11.54 +            self._purgeObjects()
   11.55 +
   11.56 +        self._initProperties(node)
   11.57 +        self._initObjects(node)
   11.58 +
   11.59 +        self._logger.info('Folder imported.')
   11.60 +
   11.61 +    def _exportBody(self):
   11.62 +        """Export the object as a file body.
   11.63 +        """
   11.64 +        if self.context.meta_type == 'Folder':
   11.65 +            return XMLAdapterBase._exportBody(self)
   11.66 +        return None
   11.67 +
   11.68 +    body = property(_exportBody, XMLAdapterBase._importBody)
    12.1 new file mode 100644
    12.2 --- /dev/null
    12.3 +++ b/OFSP/tests/__init__.py
    12.4 @@ -0,0 +1,16 @@
    12.5 +##############################################################################
    12.6 +#
    12.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    12.8 +#
    12.9 +# This software is subject to the provisions of the Zope Public License,
   12.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   12.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   12.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   12.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   12.14 +# FOR A PARTICULAR PURPOSE.
   12.15 +#
   12.16 +##############################################################################
   12.17 +"""OFS support tests.
   12.18 +
   12.19 +$Id: __init__.py 40386 2005-11-28 11:38:16Z yuppie $
   12.20 +"""
    13.1 new file mode 100644
    13.2 --- /dev/null
    13.3 +++ b/OFSP/tests/test_exportimport.py
    13.4 @@ -0,0 +1,70 @@
    13.5 +##############################################################################
    13.6 +#
    13.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    13.8 +#
    13.9 +# This software is subject to the provisions of the Zope Public License,
   13.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   13.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   13.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   13.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   13.14 +# FOR A PARTICULAR PURPOSE.
   13.15 +#
   13.16 +##############################################################################
   13.17 +"""OFSP export / import support unit tests.
   13.18 +
   13.19 +$Id: test_exportimport.py 40386 2005-11-28 11:38:16Z yuppie $
   13.20 +"""
   13.21 +
   13.22 +import unittest
   13.23 +import Testing
   13.24 +
   13.25 +from Products.Five import zcml
   13.26 +
   13.27 +from Products.GenericSetup.testing import BodyAdapterTestCase
   13.28 +
   13.29 +_FOLDER_BODY = """\
   13.30 +<?xml version="1.0"?>
   13.31 +<object name="foo_folder" meta_type="Folder">
   13.32 + <property name="title">Foo</property>
   13.33 +</object>
   13.34 +"""
   13.35 +
   13.36 +
   13.37 +class FolderXMLAdapterTests(BodyAdapterTestCase):
   13.38 +
   13.39 +    def _getTargetClass(self):
   13.40 +        from Products.GenericSetup.OFSP.exportimport import FolderXMLAdapter
   13.41 +
   13.42 +        return FolderXMLAdapter
   13.43 +
   13.44 +    def _populate(self, obj):
   13.45 +        obj._setPropValue('title', 'Foo')
   13.46 +
   13.47 +    def _verifyImport(self, obj):
   13.48 +        self.assertEqual(type(obj.title), str)
   13.49 +        self.assertEqual(obj.title, 'Foo')
   13.50 +
   13.51 +    def setUp(self):
   13.52 +        import Products.GenericSetup.OFSP
   13.53 +        from OFS.Folder import Folder
   13.54 +
   13.55 +        BodyAdapterTestCase.setUp(self)
   13.56 +        try:
   13.57 +            #BBB: for Zope 2.8
   13.58 +            import Products.Five
   13.59 +            zcml.load_config('interfaces.zcml', Products.Five)
   13.60 +        except IOError:
   13.61 +            pass
   13.62 +        zcml.load_config('configure.zcml', Products.GenericSetup.OFSP)
   13.63 +
   13.64 +        self._obj = Folder('foo_folder')
   13.65 +        self._BODY = _FOLDER_BODY
   13.66 +
   13.67 +
   13.68 +def test_suite():
   13.69 +    return unittest.TestSuite((
   13.70 +        unittest.makeSuite(FolderXMLAdapterTests),
   13.71 +        ))
   13.72 +
   13.73 +if __name__ == '__main__':
   13.74 +    unittest.main(defaultTest='test_suite')
    14.1 new file mode 100644
    14.2 --- /dev/null
    14.3 +++ b/PROFILES.txt
    14.4 @@ -0,0 +1,28 @@
    14.5 +Profiles
    14.6 +
    14.7 +  Overview
    14.8 +
    14.9 +    There are two different kinds of profiles: Base profiles and extension
   14.10 +    profiles. Base profiles have no dependencies. Extension profiles are
   14.11 +    profile fragments used to modify base profiles. They can be shipped with
   14.12 +    add-on products or used for customization steps. Importing an extension
   14.13 +    profile adds or overwrites existing settings in a fine-grained way. You
   14.14 +    can't export extension profiles. Snapshots and exports always represent
   14.15 +    the merged settings.
   14.16 +
   14.17 +  Update Directives
   14.18 +
   14.19 +    For some XML elements there are additional attributes and values to
   14.20 +    specify update directives. They are only useful for extension profiles and
   14.21 +    you will never see them in snapshots and exports.
   14.22 +
   14.23 +    'insert-before' and 'insert-after'
   14.24 +
   14.25 +      applies to: object (generic)
   14.26 +
   14.27 +      'insert-before' and 'insert-after' specify the position of a new item
   14.28 +      relative to an existing item. If they are omitted or not valid, items
   14.29 +      are appended. You can also use '*' as wildcard. This will insert the new
   14.30 +      item at the top (before all existing items) or the bottom (after all
   14.31 +      existing items). If an item with the given ID exists already, it is
   14.32 +      moved to the specified position.
    15.1 new file mode 100644
    15.2 --- /dev/null
    15.3 +++ b/PluginIndexes/__init__.py
    15.4 @@ -0,0 +1,16 @@
    15.5 +##############################################################################
    15.6 +#
    15.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    15.8 +#
    15.9 +# This software is subject to the provisions of the Zope Public License,
   15.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   15.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   15.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   15.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   15.14 +# FOR A PARTICULAR PURPOSE.
   15.15 +#
   15.16 +##############################################################################
   15.17 +"""PluginIndexes support.
   15.18 +
   15.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   15.20 +"""
    16.1 new file mode 100644
    16.2 --- /dev/null
    16.3 +++ b/PluginIndexes/configure.zcml
    16.4 @@ -0,0 +1,61 @@
    16.5 +<configure
    16.6 +    xmlns="http://namespaces.zope.org/zope"
    16.7 +    >
    16.8 +
    16.9 +  <adapter
   16.10 +      factory=".exportimport.PluggableIndexNodeAdapter"
   16.11 +      provides="Products.GenericSetup.interfaces.INode"
   16.12 +      for="Products.PluginIndexes.interfaces.IPluggableIndex
   16.13 +           Products.GenericSetup.interfaces.ISetupEnviron"
   16.14 +      />
   16.15 +
   16.16 +  <adapter
   16.17 +      factory=".exportimport.DateIndexNodeAdapter"
   16.18 +      provides="Products.GenericSetup.interfaces.INode"
   16.19 +      for="Products.PluginIndexes.interfaces.IDateIndex
   16.20 +           Products.GenericSetup.interfaces.ISetupEnviron"
   16.21 +      />
   16.22 +
   16.23 +  <adapter
   16.24 +      factory=".exportimport.DateRangeIndexNodeAdapter"
   16.25 +      provides="Products.GenericSetup.interfaces.INode"
   16.26 +      for="Products.PluginIndexes.interfaces.IDateRangeIndex
   16.27 +           Products.GenericSetup.interfaces.ISetupEnviron"
   16.28 +      />
   16.29 +
   16.30 +  <adapter
   16.31 +      factory=".exportimport.PathIndexNodeAdapter"
   16.32 +      provides="Products.GenericSetup.interfaces.INode"
   16.33 +      for="Products.PluginIndexes.interfaces.IPathIndex
   16.34 +           Products.GenericSetup.interfaces.ISetupEnviron"
   16.35 +      />
   16.36 +
   16.37 +  <adapter
   16.38 +      factory=".exportimport.VocabularyNodeAdapter"
   16.39 +      provides="Products.GenericSetup.interfaces.INode"
   16.40 +      for="Products.PluginIndexes.interfaces.IVocabulary
   16.41 +           Products.GenericSetup.interfaces.ISetupEnviron"
   16.42 +      />
   16.43 +
   16.44 +  <adapter
   16.45 +      factory=".exportimport.TextIndexNodeAdapter"
   16.46 +      provides="Products.GenericSetup.interfaces.INode"
   16.47 +      for="Products.PluginIndexes.interfaces.ITextIndex
   16.48 +           Products.GenericSetup.interfaces.ISetupEnviron"
   16.49 +      />
   16.50 +
   16.51 +  <adapter
   16.52 +      factory=".exportimport.FilteredSetNodeAdapter"
   16.53 +      provides="Products.GenericSetup.interfaces.INode"
   16.54 +      for="Products.PluginIndexes.interfaces.IFilteredSet
   16.55 +           Products.GenericSetup.interfaces.ISetupEnviron"
   16.56 +      />
   16.57 +
   16.58 +  <adapter
   16.59 +      factory=".exportimport.TopicIndexNodeAdapter"
   16.60 +      provides="Products.GenericSetup.interfaces.INode"
   16.61 +      for="Products.PluginIndexes.interfaces.ITopicIndex
   16.62 +           Products.GenericSetup.interfaces.ISetupEnviron"
   16.63 +      />
   16.64 +
   16.65 +</configure>
    17.1 new file mode 100644
    17.2 --- /dev/null
    17.3 +++ b/PluginIndexes/exportimport.py
    17.4 @@ -0,0 +1,218 @@
    17.5 +##############################################################################
    17.6 +#
    17.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    17.8 +#
    17.9 +# This software is subject to the provisions of the Zope Public License,
   17.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   17.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   17.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   17.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   17.14 +# FOR A PARTICULAR PURPOSE.
   17.15 +#
   17.16 +##############################################################################
   17.17 +"""PluginIndexes export / import support.
   17.18 +
   17.19 +$Id: exportimport.py 40715 2005-12-12 10:33:40Z yuppie $
   17.20 +"""
   17.21 +
   17.22 +from zope.app import zapi
   17.23 +
   17.24 +from Products.GenericSetup.interfaces import INode
   17.25 +from Products.GenericSetup.utils import NodeAdapterBase
   17.26 +from Products.GenericSetup.utils import PropertyManagerHelpers
   17.27 +
   17.28 +from Products.PluginIndexes.interfaces import IDateIndex
   17.29 +from Products.PluginIndexes.interfaces import IDateRangeIndex
   17.30 +from Products.PluginIndexes.interfaces import IFilteredSet
   17.31 +from Products.PluginIndexes.interfaces import IPathIndex
   17.32 +from Products.PluginIndexes.interfaces import IPluggableIndex
   17.33 +from Products.PluginIndexes.interfaces import ITextIndex
   17.34 +from Products.PluginIndexes.interfaces import ITopicIndex
   17.35 +from Products.PluginIndexes.interfaces import IVocabulary
   17.36 +
   17.37 +
   17.38 +class PluggableIndexNodeAdapter(NodeAdapterBase):
   17.39 +
   17.40 +    """Node im- and exporter for FieldIndex, KeywordIndex.
   17.41 +    """
   17.42 +
   17.43 +    __used_for__ = IPluggableIndex
   17.44 +
   17.45 +    def _exportNode(self):
   17.46 +        """Export the object as a DOM node.
   17.47 +        """
   17.48 +        node = self._getObjectNode('index')
   17.49 +        for value in self.context.getIndexSourceNames():
   17.50 +            child = self._doc.createElement('indexed_attr')
   17.51 +            child.setAttribute('value', value)
   17.52 +            node.appendChild(child)
   17.53 +        return node
   17.54 +
   17.55 +    def _importNode(self, node):
   17.56 +        """Import the object from the DOM node.
   17.57 +        """
   17.58 +        indexed_attrs = []
   17.59 +        for child in node.childNodes:
   17.60 +            if child.nodeName == 'indexed_attr':
   17.61 +                indexed_attrs.append(
   17.62 +                                  child.getAttribute('value').encode('utf-8'))
   17.63 +        self.context.indexed_attrs = indexed_attrs
   17.64 +        self.context.clear()
   17.65 +
   17.66 +    node = property(_exportNode, _importNode)
   17.67 +
   17.68 +
   17.69 +class DateIndexNodeAdapter(NodeAdapterBase, PropertyManagerHelpers):
   17.70 +
   17.71 +    """Node im- and exporter for DateIndex.
   17.72 +    """
   17.73 +
   17.74 +    __used_for__ = IDateIndex
   17.75 +
   17.76 +    def _exportNode(self):
   17.77 +        """Export the object as a DOM node.
   17.78 +        """
   17.79 +        node = self._getObjectNode('index')
   17.80 +        node.appendChild(self._extractProperties())
   17.81 +        return node
   17.82 +
   17.83 +    def _importNode(self, node):
   17.84 +        """Import the object from the DOM node.
   17.85 +        """
   17.86 +        if self.environ.shouldPurge():
   17.87 +            self._purgeProperties()
   17.88 +
   17.89 +        self._initProperties(node)
   17.90 +        self.context.clear()
   17.91 +
   17.92 +    node = property(_exportNode, _importNode)
   17.93 +
   17.94 +
   17.95 +class DateRangeIndexNodeAdapter(NodeAdapterBase):
   17.96 +
   17.97 +    """Node im- and exporter for DateRangeIndex.
   17.98 +    """
   17.99 +
  17.100 +    __used_for__ = IDateRangeIndex
  17.101 +
  17.102 +    def _exportNode(self):
  17.103 +        """Export the object as a DOM node.
  17.104 +        """
  17.105 +        node = self._getObjectNode('index')
  17.106 +        node.setAttribute('since_field', self.context.getSinceField())
  17.107 +        node.setAttribute('until_field', self.context.getUntilField())
  17.108 +        return node
  17.109 +
  17.110 +    def _importNode(self, node):
  17.111 +        """Import the object from the DOM node.
  17.112 +        """
  17.113 +        self.context._edit(node.getAttribute('since_field').encode('utf-8'),
  17.114 +                           node.getAttribute('until_field').encode('utf-8'))
  17.115 +        self.context.clear()
  17.116 +
  17.117 +    node = property(_exportNode, _importNode)
  17.118 +
  17.119 +
  17.120 +class PathIndexNodeAdapter(NodeAdapterBase):
  17.121 +
  17.122 +    """Node im- and exporter for PathIndex.
  17.123 +    """
  17.124 +
  17.125 +    __used_for__ = IPathIndex
  17.126 +
  17.127 +    def _exportNode(self):
  17.128 +        """Export the object as a DOM node.
  17.129 +        """
  17.130 +        return self._getObjectNode('index')
  17.131 +
  17.132 +    node = property(_exportNode, lambda self, val: None)
  17.133 +
  17.134 +
  17.135 +class VocabularyNodeAdapter(NodeAdapterBase):
  17.136 +
  17.137 +    """Node im- and exporter for Vocabulary.
  17.138 +    """
  17.139 +
  17.140 +    __used_for__ = IVocabulary
  17.141 +
  17.142 +    def _exportNode(self):
  17.143 +        """Export the object as a DOM node.
  17.144 +        """
  17.145 +        node = self._getObjectNode('object')
  17.146 +        node.setAttribute('deprecated', 'True')
  17.147 +        return node
  17.148 +
  17.149 +    node = property(_exportNode, lambda self, val: None)
  17.150 +
  17.151 +
  17.152 +class TextIndexNodeAdapter(NodeAdapterBase):
  17.153 +
  17.154 +    """Node im- and exporter for TextIndex.
  17.155 +    """
  17.156 +
  17.157 +    __used_for__ = ITextIndex
  17.158 +
  17.159 +    def _exportNode(self):
  17.160 +        """Export the object as a DOM node.
  17.161 +        """
  17.162 +        node = self._getObjectNode('index')
  17.163 +        node.setAttribute('deprecated', 'True')
  17.164 +        return node
  17.165 +
  17.166 +    node = property(_exportNode, lambda self, val: None)
  17.167 +
  17.168 +
  17.169 +class FilteredSetNodeAdapter(NodeAdapterBase):
  17.170 +
  17.171 +    """Node im- and exporter for FilteredSet.
  17.172 +    """
  17.173 +
  17.174 +    __used_for__ = IFilteredSet
  17.175 +
  17.176 +    def _exportNode(self):
  17.177 +        """Export the object as a DOM node.
  17.178 +        """
  17.179 +        node = self._getObjectNode('filtered_set')
  17.180 +        node.setAttribute('expression', self.context.getExpression())
  17.181 +        return node
  17.182 +
  17.183 +    def _importNode(self, node):
  17.184 +        """Import the object from the DOM node.
  17.185 +        """
  17.186 +        self.context.setExpression(
  17.187 +                              node.getAttribute('expression').encode('utf-8'))
  17.188 +        self.context.clear()
  17.189 +
  17.190 +    node = property(_exportNode, _importNode)
  17.191 +
  17.192 +
  17.193 +class TopicIndexNodeAdapter(NodeAdapterBase):
  17.194 +
  17.195 +    """Node im- and exporter for TopicIndex.
  17.196 +    """
  17.197 +
  17.198 +    __used_for__ = ITopicIndex
  17.199 +
  17.200 +    def _exportNode(self):
  17.201 +        """Export the object as a DOM node.
  17.202 +        """
  17.203 +        node = self._getObjectNode('index')
  17.204 +        for set in self.context.filteredSets.values():
  17.205 +            exporter = zapi.queryMultiAdapter((set, self.environ), INode)
  17.206 +            node.appendChild(exporter.node)
  17.207 +        return node
  17.208 +
  17.209 +    def _importNode(self, node):
  17.210 +        """Import the object from the DOM node.
  17.211 +        """
  17.212 +        for child in node.childNodes:
  17.213 +            if child.nodeName == 'filtered_set':
  17.214 +                set_id = str(child.getAttribute('name'))
  17.215 +                set_meta_type = str(child.getAttribute('meta_type'))
  17.216 +                self.context.addFilteredSet(set_id, set_meta_type, '')
  17.217 +                set = self.context.filteredSets[set_id]
  17.218 +                importer = zapi.queryMultiAdapter((set, self.environ), INode)
  17.219 +                importer.node = child
  17.220 +        self.context.clear()
  17.221 +
  17.222 +    node = property(_exportNode, _importNode)
    18.1 new file mode 100644
    18.2 --- /dev/null
    18.3 +++ b/PluginIndexes/tests/__init__.py
    18.4 @@ -0,0 +1,16 @@
    18.5 +##############################################################################
    18.6 +#
    18.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    18.8 +#
    18.9 +# This software is subject to the provisions of the Zope Public License,
   18.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   18.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   18.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   18.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   18.14 +# FOR A PARTICULAR PURPOSE.
   18.15 +#
   18.16 +##############################################################################
   18.17 +"""PluginIndexes support tests.
   18.18 +
   18.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   18.20 +"""
    19.1 new file mode 100644
    19.2 --- /dev/null
    19.3 +++ b/PluginIndexes/tests/test_exportimport.py
    19.4 @@ -0,0 +1,290 @@
    19.5 +##############################################################################
    19.6 +#
    19.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    19.8 +#
    19.9 +# This software is subject to the provisions of the Zope Public License,
   19.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   19.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   19.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   19.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   19.14 +# FOR A PARTICULAR PURPOSE.
   19.15 +#
   19.16 +##############################################################################
   19.17 +"""PluginIndexes export / import support unit tests.
   19.18 +
   19.19 +$Id: test_exportimport.py 40452 2005-12-01 18:20:45Z yuppie $
   19.20 +"""
   19.21 +
   19.22 +import unittest
   19.23 +import Testing
   19.24 +
   19.25 +from Products.Five import zcml
   19.26 +from Products.GenericSetup.testing import NodeAdapterTestCase
   19.27 +
   19.28 +_DATE_XML = """\
   19.29 +<index name="foo_date" meta_type="DateIndex">
   19.30 + <property name="index_naive_time_as_local">True</property>
   19.31 +</index>
   19.32 +"""
   19.33 +
   19.34 +_DATERANGE_XML = """\
   19.35 +<index name="foo_daterange" meta_type="DateRangeIndex" since_field="bar"
   19.36 +   until_field="baz"/>
   19.37 +"""
   19.38 +
   19.39 +_FIELD_XML = """\
   19.40 +<index name="foo_field" meta_type="FieldIndex">
   19.41 + <indexed_attr value="bar"/>
   19.42 +</index>
   19.43 +"""
   19.44 +
   19.45 +_KEYWORD_XML = """\
   19.46 +<index name="foo_keyword" meta_type="KeywordIndex">
   19.47 + <indexed_attr value="bar"/>
   19.48 +</index>
   19.49 +"""
   19.50 +
   19.51 +_PATH_XML = """\
   19.52 +<index name="foo_path" meta_type="PathIndex"/>
   19.53 +"""
   19.54 +
   19.55 +_VOCABULARY_XML = """\
   19.56 +<object name="foo_vocabulary" meta_type="Vocabulary" deprecated="True"/>
   19.57 +"""
   19.58 +
   19.59 +_TEXT_XML = """\
   19.60 +<index name="foo_text" meta_type="TextIndex" deprecated="True"/>
   19.61 +"""
   19.62 +
   19.63 +_SET_XML = """\
   19.64 +<filtered_set name="bar" meta_type="PythonFilteredSet" expression="True"/>
   19.65 +"""
   19.66 +
   19.67 +_TOPIC_XML = """\
   19.68 +<index name="foo_topic" meta_type="TopicIndex">
   19.69 + <filtered_set name="bar" meta_type="PythonFilteredSet" expression="True"/>
   19.70 + <filtered_set name="baz" meta_type="PythonFilteredSet" expression="False"/>
   19.71 +</index>
   19.72 +"""
   19.73 +
   19.74 +
   19.75 +class DateIndexNodeAdapterTests(NodeAdapterTestCase):
   19.76 +
   19.77 +    def _getTargetClass(self):
   19.78 +        from Products.GenericSetup.PluginIndexes.exportimport \
   19.79 +                import DateIndexNodeAdapter
   19.80 +
   19.81 +        return DateIndexNodeAdapter
   19.82 +
   19.83 +    def setUp(self):
   19.84 +        import Products.GenericSetup.PluginIndexes
   19.85 +        from Products.PluginIndexes.DateIndex.DateIndex import DateIndex
   19.86 +
   19.87 +        NodeAdapterTestCase.setUp(self)
   19.88 +        zcml.load_config('configure.zcml',
   19.89 +                         Products.GenericSetup.PluginIndexes)
   19.90 +
   19.91 +        self._obj = DateIndex('foo_date')
   19.92 +        self._XML = _DATE_XML
   19.93 +
   19.94 +
   19.95 +class DateRangeIndexNodeAdapterTests(NodeAdapterTestCase):
   19.96 +
   19.97 +    def _getTargetClass(self):
   19.98 +        from Products.GenericSetup.PluginIndexes.exportimport \
   19.99 +                import DateRangeIndexNodeAdapter
  19.100 +
  19.101 +        return DateRangeIndexNodeAdapter
  19.102 +
  19.103 +    def _populate(self, obj):
  19.104 +        obj._edit('bar', 'baz')
  19.105 +
  19.106 +    def setUp(self):
  19.107 +        import Products.GenericSetup.PluginIndexes
  19.108 +        from Products.PluginIndexes.DateRangeIndex.DateRangeIndex \
  19.109 +                import DateRangeIndex
  19.110 +
  19.111 +        NodeAdapterTestCase.setUp(self)
  19.112 +        zcml.load_config('configure.zcml',
  19.113 +                         Products.GenericSetup.PluginIndexes)
  19.114 +
  19.115 +        self._obj = DateRangeIndex('foo_daterange')
  19.116 +        self._XML = _DATERANGE_XML
  19.117 +
  19.118 +
  19.119 +class FieldIndexNodeAdapterTests(NodeAdapterTestCase):
  19.120 +
  19.121 +    def _getTargetClass(self):
  19.122 +        from Products.GenericSetup.PluginIndexes.exportimport \
  19.123 +                import PluggableIndexNodeAdapter
  19.124 +
  19.125 +        return PluggableIndexNodeAdapter
  19.126 +
  19.127 +    def _populate(self, obj):
  19.128 +        obj.indexed_attrs = ('bar',)
  19.129 +
  19.130 +    def setUp(self):
  19.131 +        import Products.GenericSetup.PluginIndexes
  19.132 +        from Products.PluginIndexes.FieldIndex.FieldIndex import FieldIndex
  19.133 +
  19.134 +        NodeAdapterTestCase.setUp(self)
  19.135 +        zcml.load_config('configure.zcml',
  19.136 +                         Products.GenericSetup.PluginIndexes)
  19.137 +
  19.138 +        self._obj = FieldIndex('foo_field')
  19.139 +        self._XML = _FIELD_XML
  19.140 +
  19.141 +
  19.142 +class KeywordIndexNodeAdapterTests(NodeAdapterTestCase):
  19.143 +
  19.144 +    def _getTargetClass(self):
  19.145 +        from Products.GenericSetup.PluginIndexes.exportimport \
  19.146 +                import PluggableIndexNodeAdapter
  19.147 +
  19.148 +        return PluggableIndexNodeAdapter
  19.149 +
  19.150 +    def _populate(self, obj):
  19.151 +        obj.indexed_attrs = ('bar',)
  19.152 +
  19.153 +    def setUp(self):
  19.154 +        import Products.GenericSetup.PluginIndexes
  19.155 +        from Products.PluginIndexes.KeywordIndex.KeywordIndex \
  19.156 +                import KeywordIndex
  19.157 +
  19.158 +        NodeAdapterTestCase.setUp(self)
  19.159 +        zcml.load_config('configure.zcml',
  19.160 +                         Products.GenericSetup.PluginIndexes)
  19.161 +
  19.162 +        self._obj = KeywordIndex('foo_keyword')
  19.163 +        self._XML = _KEYWORD_XML
  19.164 +
  19.165 +
  19.166 +class PathIndexNodeAdapterTests(NodeAdapterTestCase):
  19.167 +
  19.168 +    def _getTargetClass(self):
  19.169 +        from Products.GenericSetup.PluginIndexes.exportimport \
  19.170 +                import PathIndexNodeAdapter
  19.171 +
  19.172 +        return PathIndexNodeAdapter
  19.173 +
  19.174 +    def setUp(self):
  19.175 +        import Products.GenericSetup.PluginIndexes
  19.176 +        from Products.PluginIndexes.PathIndex.PathIndex import PathIndex
  19.177 +
  19.178 +        NodeAdapterTestCase.setUp(self)
  19.179 +        zcml.load_config('configure.zcml',
  19.180 +                         Products.GenericSetup.PluginIndexes)
  19.181 +
  19.182 +        self._obj = PathIndex('foo_path')
  19.183 +        self._XML = _PATH_XML
  19.184 +
  19.185 +
  19.186 +class VocabularyNodeAdapterTests(NodeAdapterTestCase):
  19.187 +
  19.188 +    def _getTargetClass(self):
  19.189 +        from Products.GenericSetup.PluginIndexes.exportimport \
  19.190 +                import VocabularyNodeAdapter
  19.191 +
  19.192 +        return VocabularyNodeAdapter
  19.193 +
  19.194 +    def setUp(self):
  19.195 +        import Products.GenericSetup.PluginIndexes
  19.196 +        from Products.PluginIndexes.TextIndex.Vocabulary import Vocabulary
  19.197 +
  19.198 +        NodeAdapterTestCase.setUp(self)
  19.199 +        zcml.load_config('configure.zcml',
  19.200 +                         Products.GenericSetup.PluginIndexes)
  19.201 +
  19.202 +        self._obj = Vocabulary('foo_vocabulary')
  19.203 +        self._XML = _VOCABULARY_XML
  19.204 +
  19.205 +    def test_importNode(self):
  19.206 +        pass
  19.207 +
  19.208 +
  19.209 +class TextIndexNodeAdapterTests(NodeAdapterTestCase):
  19.210 +
  19.211 +    def _getTargetClass(self):
  19.212 +        from Products.GenericSetup.PluginIndexes.exportimport \
  19.213 +                import TextIndexNodeAdapter
  19.214 +
  19.215 +        return TextIndexNodeAdapter
  19.216 +
  19.217 +    def setUp(self):
  19.218 +        import Products.GenericSetup.PluginIndexes
  19.219 +        from Products.PluginIndexes.TextIndex.TextIndex import TextIndex
  19.220 +
  19.221 +        NodeAdapterTestCase.setUp(self)
  19.222 +        zcml.load_config('configure.zcml',
  19.223 +                         Products.GenericSetup.PluginIndexes)
  19.224 +
  19.225 +        self._obj = TextIndex('foo_text')
  19.226 +        self._XML = _TEXT_XML
  19.227 +
  19.228 +    def test_importNode(self):
  19.229 +        pass
  19.230 +
  19.231 +
  19.232 +class FilteredSetNodeAdapterTests(NodeAdapterTestCase):
  19.233 +
  19.234 +    def _getTargetClass(self):
  19.235 +        from Products.GenericSetup.PluginIndexes.exportimport \
  19.236 +                import FilteredSetNodeAdapter
  19.237 +
  19.238 +        return FilteredSetNodeAdapter
  19.239 +
  19.240 +    def _populate(self, obj):
  19.241 +        obj.setExpression('True')
  19.242 +
  19.243 +    def setUp(self):
  19.244 +        import Products.GenericSetup.PluginIndexes
  19.245 +        from Products.PluginIndexes.TopicIndex.FilteredSet \
  19.246 +                import PythonFilteredSet
  19.247 +
  19.248 +        NodeAdapterTestCase.setUp(self)
  19.249 +        zcml.load_config('configure.zcml',
  19.250 +                         Products.GenericSetup.PluginIndexes)
  19.251 +
  19.252 +        self._obj = PythonFilteredSet('bar', '')
  19.253 +        self._XML = _SET_XML
  19.254 +
  19.255 +
  19.256 +class TopicIndexNodeAdapterTests(NodeAdapterTestCase):
  19.257 +
  19.258 +    def _getTargetClass(self):
  19.259 +        from Products.GenericSetup.PluginIndexes.exportimport \
  19.260 +                import TopicIndexNodeAdapter
  19.261 +
  19.262 +        return TopicIndexNodeAdapter
  19.263 +
  19.264 +    def _populate(self, obj):
  19.265 +        obj.addFilteredSet('bar', 'PythonFilteredSet', 'True')
  19.266 +        obj.addFilteredSet('baz', 'PythonFilteredSet', 'False')
  19.267 +
  19.268 +    def setUp(self):
  19.269 +        import Products.GenericSetup.PluginIndexes
  19.270 +        from Products.PluginIndexes.TopicIndex.TopicIndex import TopicIndex
  19.271 +
  19.272 +        NodeAdapterTestCase.setUp(self)
  19.273 +        zcml.load_config('configure.zcml',
  19.274 +                         Products.GenericSetup.PluginIndexes)
  19.275 +
  19.276 +        self._obj = TopicIndex('foo_topic')
  19.277 +        self._XML = _TOPIC_XML
  19.278 +
  19.279 +
  19.280 +def test_suite():
  19.281 +    return unittest.TestSuite((
  19.282 +        unittest.makeSuite(DateIndexNodeAdapterTests),
  19.283 +        unittest.makeSuite(DateRangeIndexNodeAdapterTests),
  19.284 +        unittest.makeSuite(FieldIndexNodeAdapterTests),
  19.285 +        unittest.makeSuite(KeywordIndexNodeAdapterTests),
  19.286 +        unittest.makeSuite(PathIndexNodeAdapterTests),
  19.287 +        unittest.makeSuite(VocabularyNodeAdapterTests),
  19.288 +        unittest.makeSuite(TextIndexNodeAdapterTests),
  19.289 +        unittest.makeSuite(FilteredSetNodeAdapterTests),
  19.290 +        unittest.makeSuite(TopicIndexNodeAdapterTests),
  19.291 +        ))
  19.292 +
  19.293 +if __name__ == '__main__':
  19.294 +    unittest.main(defaultTest='test_suite')
    20.1 new file mode 100644
    20.2 --- /dev/null
    20.3 +++ b/PythonScripts/__init__.py
    20.4 @@ -0,0 +1,16 @@
    20.5 +##############################################################################
    20.6 +#
    20.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    20.8 +#
    20.9 +# This software is subject to the provisions of the Zope Public License,
   20.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   20.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   20.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   20.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   20.14 +# FOR A PARTICULAR PURPOSE.
   20.15 +#
   20.16 +##############################################################################
   20.17 +"""PythonScript support.
   20.18 +
   20.19 +$Id: __init__.py 40314 2005-11-22 13:21:47Z yuppie $
   20.20 +"""
    21.1 new file mode 100644
    21.2 --- /dev/null
    21.3 +++ b/PythonScripts/configure.zcml
    21.4 @@ -0,0 +1,18 @@
    21.5 +<configure
    21.6 +    xmlns="http://namespaces.zope.org/zope"
    21.7 +    xmlns:five="http://namespaces.zope.org/five"
    21.8 +    >
    21.9 +
   21.10 +  <adapter
   21.11 +      factory=".exportimport.PythonScriptBodyAdapter"
   21.12 +      provides="Products.GenericSetup.interfaces.IBody"
   21.13 +      for=".interfaces.IPythonScript
   21.14 +           Products.GenericSetup.interfaces.ISetupEnviron"
   21.15 +      />
   21.16 +
   21.17 +  <five:implements
   21.18 +      class="Products.PythonScripts.PythonScript.PythonScript"
   21.19 +      interface=".interfaces.IPythonScript"
   21.20 +      />
   21.21 +
   21.22 +</configure>
    22.1 new file mode 100644
    22.2 --- /dev/null
    22.3 +++ b/PythonScripts/exportimport.py
    22.4 @@ -0,0 +1,44 @@
    22.5 +##############################################################################
    22.6 +#
    22.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    22.8 +#
    22.9 +# This software is subject to the provisions of the Zope Public License,
   22.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   22.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   22.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   22.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   22.14 +# FOR A PARTICULAR PURPOSE.
   22.15 +#
   22.16 +##############################################################################
   22.17 +"""PythonScript export / import support.
   22.18 +
   22.19 +$Id: exportimport.py 40314 2005-11-22 13:21:47Z yuppie $
   22.20 +"""
   22.21 +
   22.22 +from Products.GenericSetup.utils import BodyAdapterBase
   22.23 +
   22.24 +from interfaces import IPythonScript
   22.25 +
   22.26 +
   22.27 +class PythonScriptBodyAdapter(BodyAdapterBase):
   22.28 +
   22.29 +    """Body im- and exporter for PythonScript.
   22.30 +    """
   22.31 +
   22.32 +    __used_for__ = IPythonScript
   22.33 +
   22.34 +    def _exportBody(self):
   22.35 +        """Export the object as a file body.
   22.36 +        """
   22.37 +        return self.context.read()
   22.38 +
   22.39 +    def _importBody(self, body):
   22.40 +        """Import the object from the file body.
   22.41 +        """
   22.42 +        self.context.write(body)
   22.43 +
   22.44 +    body = property(_exportBody, _importBody)
   22.45 +
   22.46 +    mime_type = 'text/plain'
   22.47 +
   22.48 +    suffix = '.py'
    23.1 new file mode 100644
    23.2 --- /dev/null
    23.3 +++ b/PythonScripts/interfaces.py
    23.4 @@ -0,0 +1,38 @@
    23.5 +##############################################################################
    23.6 +#
    23.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    23.8 +#
    23.9 +# This software is subject to the provisions of the Zope Public License,
   23.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   23.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   23.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   23.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   23.14 +# FOR A PARTICULAR PURPOSE.
   23.15 +#
   23.16 +##############################################################################
   23.17 +"""PythonScripts interfaces.
   23.18 +
   23.19 +$Id: interfaces.py 40314 2005-11-22 13:21:47Z yuppie $
   23.20 +"""
   23.21 +
   23.22 +from zope.interface import Interface
   23.23 +
   23.24 +
   23.25 +class IPythonScript(Interface):
   23.26 +
   23.27 +    """Web-callable scripts written in a safe subset of Python.
   23.28 +
   23.29 +    The function may include standard python code, so long as it does not
   23.30 +    attempt to use the "exec" statement or certain restricted builtins.
   23.31 +    """
   23.32 +
   23.33 +    def read():
   23.34 +        """Generate a text representation of the Script source.
   23.35 +
   23.36 +        Includes specially formatted comment lines for parameters, bindings
   23.37 +        and the title.
   23.38 +        """
   23.39 +
   23.40 +    def write(text):
   23.41 +        """Change the Script by parsing a read()-style source text.
   23.42 +        """
    24.1 new file mode 100644
    24.2 --- /dev/null
    24.3 +++ b/PythonScripts/tests/__init__.py
    24.4 @@ -0,0 +1,16 @@
    24.5 +##############################################################################
    24.6 +#
    24.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    24.8 +#
    24.9 +# This software is subject to the provisions of the Zope Public License,
   24.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   24.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   24.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   24.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   24.14 +# FOR A PARTICULAR PURPOSE.
   24.15 +#
   24.16 +##############################################################################
   24.17 +"""PythonScript support tests.
   24.18 +
   24.19 +$Id: __init__.py 40314 2005-11-22 13:21:47Z yuppie $
   24.20 +"""
    25.1 new file mode 100644
    25.2 --- /dev/null
    25.3 +++ b/PythonScripts/tests/test_exportimport.py
    25.4 @@ -0,0 +1,65 @@
    25.5 +##############################################################################
    25.6 +#
    25.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    25.8 +#
    25.9 +# This software is subject to the provisions of the Zope Public License,
   25.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   25.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   25.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   25.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   25.14 +# FOR A PARTICULAR PURPOSE.
   25.15 +#
   25.16 +##############################################################################
   25.17 +"""PythonScript export / import support unit tests.
   25.18 +
   25.19 +$Id: test_exportimport.py 40314 2005-11-22 13:21:47Z yuppie $
   25.20 +"""
   25.21 +
   25.22 +import unittest
   25.23 +import Testing
   25.24 +
   25.25 +from Products.Five import zcml
   25.26 +
   25.27 +from Products.GenericSetup.testing import BodyAdapterTestCase
   25.28 +
   25.29 +
   25.30 +_PYTHONSCRIPT_BODY = """\
   25.31 +## Script (Python) "foo_script"
   25.32 +##bind container=container
   25.33 +##bind context=context
   25.34 +##bind namespace=
   25.35 +##bind script=script
   25.36 +##bind subpath=traverse_subpath
   25.37 +##parameters=
   25.38 +##title=
   25.39 +##
   25.40 +"""
   25.41 +
   25.42 +
   25.43 +class PythonScriptBodyAdapterTests(BodyAdapterTestCase):
   25.44 +
   25.45 +    def _getTargetClass(self):
   25.46 +        from Products.GenericSetup.PythonScripts.exportimport \
   25.47 +                import PythonScriptBodyAdapter
   25.48 +
   25.49 +        return PythonScriptBodyAdapter
   25.50 +
   25.51 +    def setUp(self):
   25.52 +        import Products.GenericSetup.PythonScripts
   25.53 +        from Products.PythonScripts.PythonScript import PythonScript
   25.54 +
   25.55 +        BodyAdapterTestCase.setUp(self)
   25.56 +        zcml.load_config('configure.zcml',
   25.57 +                         Products.GenericSetup.PythonScripts)
   25.58 +
   25.59 +        self._obj = PythonScript('foo_script')
   25.60 +        self._BODY = _PYTHONSCRIPT_BODY
   25.61 +
   25.62 +
   25.63 +def test_suite():
   25.64 +    return unittest.TestSuite((
   25.65 +        unittest.makeSuite(PythonScriptBodyAdapterTests),
   25.66 +        ))
   25.67 +
   25.68 +if __name__ == '__main__':
   25.69 +    unittest.main(defaultTest='test_suite')
    26.1 new file mode 100644
    26.2 --- /dev/null
    26.3 +++ b/README.txt
    26.4 @@ -0,0 +1,43 @@
    26.5 +GenericSetup Product README
    26.6 +
    26.7 +  Overview
    26.8 +
    26.9 +    This product provides a mini-framework for expressing the configured
   26.10 +    state of a Zope Site as a set of filesystem artifacts.  These artifacts
   26.11 +    consist of declarative XML files, which spell out the configuration
   26.12 +    settings for each "tool" in the site , and supporting scripts / templates,
   26.13 +    in their "canonical" filesystem representations.
   26.14 +
   26.15 +  Configurations Included
   26.16 +
   26.17 +    The 'setup_tool' knows how to export / import configurations and scripts
   26.18 +    for the following tools:
   26.19 +
   26.20 +      - (x) removal / creation of specified tools
   26.21 +
   26.22 +      - (x) itself :)
   26.23 +
   26.24 +      - (x) the role / permission map on the "site" object (its parent)
   26.25 +
   26.26 +      - (x) properties of the site object
   26.27 +
   26.28 +  Extending The Tool
   26.29 +
   26.30 +    Third-party products extend the tool by registering handlers for
   26.31 +    import / export of their unique tools.
   26.32 +
   26.33 +  Glossary
   26.34 +
   26.35 +    Site --
   26.36 +      The instance in the Zope URL space which defines a "zone of service"
   26.37 +      for a set of tools.
   26.38 +
   26.39 +    Profile --
   26.40 +      A "preset" configuration of a site, defined on the filesystem
   26.41 +
   26.42 +    Snapshot --
   26.43 +      "Frozen" site configuration, captured within the setup tool
   26.44 +
   26.45 +    "dotted name" --
   26.46 +      The Pythonic representation of the "path" to a given function /
   26.47 +      module, e.g. 'Products.GenericSetup.tool.exportToolset'.
    27.1 new file mode 100644
    27.2 --- /dev/null
    27.3 +++ b/ZCTextIndex/__init__.py
    27.4 @@ -0,0 +1,16 @@
    27.5 +##############################################################################
    27.6 +#
    27.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    27.8 +#
    27.9 +# This software is subject to the provisions of the Zope Public License,
   27.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   27.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   27.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   27.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   27.14 +# FOR A PARTICULAR PURPOSE.
   27.15 +#
   27.16 +##############################################################################
   27.17 +"""ZCTextIndex support.
   27.18 +
   27.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   27.20 +"""
    28.1 new file mode 100644
    28.2 --- /dev/null
    28.3 +++ b/ZCTextIndex/configure.zcml
    28.4 @@ -0,0 +1,19 @@
    28.5 +<configure
    28.6 +    xmlns="http://namespaces.zope.org/zope"
    28.7 +    >
    28.8 +
    28.9 +  <adapter
   28.10 +      factory=".exportimport.ZCLexiconNodeAdapter"
   28.11 +      provides="Products.GenericSetup.interfaces.INode"
   28.12 +      for="Products.ZCTextIndex.interfaces.IZCLexicon
   28.13 +           Products.GenericSetup.interfaces.ISetupEnviron"
   28.14 +      />
   28.15 +
   28.16 +  <adapter
   28.17 +      factory=".exportimport.ZCTextIndexNodeAdapter"
   28.18 +      provides="Products.GenericSetup.interfaces.INode"
   28.19 +      for="Products.ZCTextIndex.interfaces.IZCTextIndex
   28.20 +           Products.GenericSetup.interfaces.ISetupEnviron"
   28.21 +      />
   28.22 +
   28.23 +</configure>
    29.1 new file mode 100644
    29.2 --- /dev/null
    29.3 +++ b/ZCTextIndex/exportimport.py
    29.4 @@ -0,0 +1,113 @@
    29.5 +##############################################################################
    29.6 +#
    29.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    29.8 +#
    29.9 +# This software is subject to the provisions of the Zope Public License,
   29.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   29.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   29.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   29.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   29.14 +# FOR A PARTICULAR PURPOSE.
   29.15 +#
   29.16 +##############################################################################
   29.17 +"""ZCTextIndex export / import support.
   29.18 +
   29.19 +$Id: exportimport.py 40715 2005-12-12 10:33:40Z yuppie $
   29.20 +"""
   29.21 +
   29.22 +from BTrees.IOBTree import IOBTree
   29.23 +from BTrees.Length import Length
   29.24 +from BTrees.OIBTree import OIBTree
   29.25 +
   29.26 +from Products.GenericSetup.utils import NodeAdapterBase
   29.27 +
   29.28 +from Products.ZCTextIndex.interfaces import IZCLexicon
   29.29 +from Products.ZCTextIndex.interfaces import IZCTextIndex
   29.30 +from Products.ZCTextIndex.PipelineFactory import element_factory
   29.31 +
   29.32 +
   29.33 +class ZCLexiconNodeAdapter(NodeAdapterBase):
   29.34 +
   29.35 +    """Node im- and exporter for ZCTextIndex Lexicon.
   29.36 +    """
   29.37 +
   29.38 +    __used_for__ = IZCLexicon
   29.39 +
   29.40 +    def _exportNode(self):
   29.41 +        """Export the object as a DOM node.
   29.42 +        """
   29.43 +        node = self._getObjectNode('object')
   29.44 +        for element in self.context._pipeline:
   29.45 +            group, name = self._getKeys(element)
   29.46 +            child = self._doc.createElement('element')
   29.47 +            child.setAttribute('group', group)
   29.48 +            child.setAttribute('name', name)
   29.49 +            node.appendChild(child)
   29.50 +        return node
   29.51 +
   29.52 +    def _importNode(self, node):
   29.53 +        """Import the object from the DOM node.
   29.54 +        """
   29.55 +        pipeline = []
   29.56 +        for child in node.childNodes:
   29.57 +            if child.nodeName == 'element':
   29.58 +                element = element_factory.instantiate(
   29.59 +                      child.getAttribute('group').encode('utf-8'),
   29.60 +                      child.getAttribute('name').encode('utf-8'))
   29.61 +                pipeline.append(element)
   29.62 +        self.context._pipeline = tuple(pipeline)
   29.63 +        #clear lexicon
   29.64 +        self.context._wids = OIBTree()
   29.65 +        self.context._words = IOBTree()
   29.66 +        self.context.length = Length()
   29.67 +
   29.68 +    node = property(_exportNode, _importNode)
   29.69 +
   29.70 +    def _getKeys(self, element):
   29.71 +        for group in element_factory.getFactoryGroups():
   29.72 +            for name, factory in element_factory._groups[group].items():
   29.73 +                if factory == element.__class__:
   29.74 +                    return group, name
   29.75 +
   29.76 +
   29.77 +class ZCTextIndexNodeAdapter(NodeAdapterBase):
   29.78 +
   29.79 +    """Node im- and exporter for ZCTextIndex.
   29.80 +    """
   29.81 +
   29.82 +    __used_for__ = IZCTextIndex
   29.83 +
   29.84 +    def _exportNode(self):
   29.85 +        """Export the object as a DOM node.
   29.86 +        """
   29.87 +        node = self._getObjectNode('index')
   29.88 +
   29.89 +        for value in self.context.getIndexSourceNames():
   29.90 +            child = self._doc.createElement('indexed_attr')
   29.91 +            child.setAttribute('value', value)
   29.92 +            node.appendChild(child)
   29.93 +
   29.94 +        child = self._doc.createElement('extra')
   29.95 +        child.setAttribute('name', 'index_type')
   29.96 +        child.setAttribute('value', self.context.getIndexType())
   29.97 +        node.appendChild(child)
   29.98 +
   29.99 +        child = self._doc.createElement('extra')
  29.100 +        child.setAttribute('name', 'lexicon_id')
  29.101 +        child.setAttribute('value', self.context.lexicon_id)
  29.102 +        node.appendChild(child)
  29.103 +
  29.104 +        return node
  29.105 +
  29.106 +    def _importNode(self, node):
  29.107 +        """Import the object from the DOM node.
  29.108 +        """
  29.109 +        indexed_attrs = []
  29.110 +        for child in node.childNodes:
  29.111 +            if child.nodeName == 'indexed_attr':
  29.112 +                indexed_attrs.append(
  29.113 +                                  child.getAttribute('value').encode('utf-8'))
  29.114 +        self.context.indexed_attrs = indexed_attrs
  29.115 +        self.context.clear()
  29.116 +
  29.117 +    node = property(_exportNode, _importNode)
    30.1 new file mode 100644
    30.2 --- /dev/null
    30.3 +++ b/ZCTextIndex/tests/__init__.py
    30.4 @@ -0,0 +1,16 @@
    30.5 +##############################################################################
    30.6 +#
    30.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    30.8 +#
    30.9 +# This software is subject to the provisions of the Zope Public License,
   30.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   30.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   30.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   30.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   30.14 +# FOR A PARTICULAR PURPOSE.
   30.15 +#
   30.16 +##############################################################################
   30.17 +"""ZCTextIndex support tests.
   30.18 +
   30.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   30.20 +"""
    31.1 new file mode 100644
    31.2 --- /dev/null
    31.3 +++ b/ZCTextIndex/tests/test_exportimport.py
    31.4 @@ -0,0 +1,111 @@
    31.5 +##############################################################################
    31.6 +#
    31.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    31.8 +#
    31.9 +# This software is subject to the provisions of the Zope Public License,
   31.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   31.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   31.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   31.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   31.14 +# FOR A PARTICULAR PURPOSE.
   31.15 +#
   31.16 +##############################################################################
   31.17 +"""ZCTextIndex export / import support unit tests.
   31.18 +
   31.19 +$Id: test_exportimport.py 40452 2005-12-01 18:20:45Z yuppie $
   31.20 +"""
   31.21 +
   31.22 +import unittest
   31.23 +import Testing
   31.24 +
   31.25 +from Acquisition import Implicit
   31.26 +
   31.27 +from Products.Five import zcml
   31.28 +from Products.GenericSetup.testing import NodeAdapterTestCase
   31.29 +
   31.30 +_PLEXICON_XML = """\
   31.31 +<object name="foo_plexicon" meta_type="ZCTextIndex Lexicon">
   31.32 + <element name="Whitespace splitter" group="Word Splitter"/>
   31.33 + <element name="Case Normalizer" group="Case Normalizer"/>
   31.34 + <element name="Remove listed stop words only" group="Stop Words"/>
   31.35 +</object>
   31.36 +"""
   31.37 +
   31.38 +_ZCTEXT_XML = """\
   31.39 +<index name="foo_zctext" meta_type="ZCTextIndex">
   31.40 + <indexed_attr value="foo_zctext"/>
   31.41 + <extra name="index_type" value="Okapi BM25 Rank"/>
   31.42 + <extra name="lexicon_id" value="foo_plexicon"/>
   31.43 +</index>
   31.44 +"""
   31.45 +
   31.46 +
   31.47 +class _extra:
   31.48 +
   31.49 +    pass
   31.50 +
   31.51 +
   31.52 +class DummyCatalog(Implicit):
   31.53 +
   31.54 +    pass
   31.55 +
   31.56 +
   31.57 +class ZCLexiconNodeAdapterTests(NodeAdapterTestCase):
   31.58 +
   31.59 +    def _getTargetClass(self):
   31.60 +        from Products.GenericSetup.ZCTextIndex.exportimport \
   31.61 +                import ZCLexiconNodeAdapter
   31.62 +
   31.63 +        return ZCLexiconNodeAdapter
   31.64 +
   31.65 +    def _populate(self, obj):
   31.66 +        from Products.ZCTextIndex.Lexicon import CaseNormalizer
   31.67 +        from Products.ZCTextIndex.Lexicon import Splitter
   31.68 +        from Products.ZCTextIndex.Lexicon import StopWordRemover
   31.69 +        obj._pipeline = (Splitter(), CaseNormalizer(), StopWordRemover())
   31.70 +
   31.71 +    def setUp(self):
   31.72 +        import Products.GenericSetup.ZCTextIndex
   31.73 +        from Products.ZCTextIndex.ZCTextIndex import PLexicon
   31.74 +
   31.75 +        NodeAdapterTestCase.setUp(self)
   31.76 +        zcml.load_config('configure.zcml', Products.GenericSetup.ZCTextIndex)
   31.77 +
   31.78 +        self._obj = PLexicon('foo_plexicon')
   31.79 +        self._XML = _PLEXICON_XML
   31.80 +
   31.81 +
   31.82 +class ZCTextIndexNodeAdapterTests(NodeAdapterTestCase):
   31.83 +
   31.84 +    def _getTargetClass(self):
   31.85 +        from Products.GenericSetup.ZCTextIndex.exportimport \
   31.86 +                import ZCTextIndexNodeAdapter
   31.87 +
   31.88 +        return ZCTextIndexNodeAdapter
   31.89 +
   31.90 +    def setUp(self):
   31.91 +        import Products.GenericSetup.ZCTextIndex
   31.92 +        from Products.ZCTextIndex.ZCTextIndex import PLexicon
   31.93 +        from Products.ZCTextIndex.ZCTextIndex import ZCTextIndex
   31.94 +
   31.95 +        NodeAdapterTestCase.setUp(self)
   31.96 +        zcml.load_config('configure.zcml', Products.GenericSetup.ZCTextIndex)
   31.97 +
   31.98 +        catalog = DummyCatalog()
   31.99 +        catalog.foo_plexicon = PLexicon('foo_plexicon')
  31.100 +        extra = _extra()
  31.101 +        extra.lexicon_id = 'foo_plexicon'
  31.102 +        extra.index_type='Okapi BM25 Rank'
  31.103 +        self._obj = ZCTextIndex('foo_zctext', extra=extra,
  31.104 +                                caller=catalog).__of__(catalog)
  31.105 +        self._XML = _ZCTEXT_XML
  31.106 +
  31.107 +
  31.108 +def test_suite():
  31.109 +    return unittest.TestSuite((
  31.110 +        unittest.makeSuite(ZCLexiconNodeAdapterTests),
  31.111 +        unittest.makeSuite(ZCTextIndexNodeAdapterTests),
  31.112 +        ))
  31.113 +
  31.114 +if __name__ == '__main__':
  31.115 +    unittest.main(defaultTest='test_suite')
    32.1 new file mode 100644
    32.2 --- /dev/null
    32.3 +++ b/ZCatalog/__init__.py
    32.4 @@ -0,0 +1,16 @@
    32.5 +##############################################################################
    32.6 +#
    32.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    32.8 +#
    32.9 +# This software is subject to the provisions of the Zope Public License,
   32.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   32.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   32.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   32.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   32.14 +# FOR A PARTICULAR PURPOSE.
   32.15 +#
   32.16 +##############################################################################
   32.17 +"""ZCatalog support.
   32.18 +
   32.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   32.20 +"""
    33.1 new file mode 100644
    33.2 --- /dev/null
    33.3 +++ b/ZCatalog/configure.zcml
    33.4 @@ -0,0 +1,12 @@
    33.5 +<configure
    33.6 +    xmlns="http://namespaces.zope.org/zope"
    33.7 +    >
    33.8 +
    33.9 +  <adapter
   33.10 +      factory=".exportimport.ZCatalogXMLAdapter"
   33.11 +      provides="Products.GenericSetup.interfaces.IBody"
   33.12 +      for="Products.ZCatalog.interfaces.IZCatalog
   33.13 +           Products.GenericSetup.interfaces.ISetupEnviron"
   33.14 +      />
   33.15 +
   33.16 +</configure>
    34.1 new file mode 100644
    34.2 --- /dev/null
    34.3 +++ b/ZCatalog/exportimport.py
    34.4 @@ -0,0 +1,133 @@
    34.5 +##############################################################################
    34.6 +#
    34.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    34.8 +#
    34.9 +# This software is subject to the provisions of the Zope Public License,
   34.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   34.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   34.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   34.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   34.14 +# FOR A PARTICULAR PURPOSE.
   34.15 +#
   34.16 +##############################################################################
   34.17 +"""ZCatalog export / import support.
   34.18 +
   34.19 +$Id: exportimport.py 40878 2005-12-18 21:57:56Z yuppie $
   34.20 +"""
   34.21 +
   34.22 +from zope.app import zapi
   34.23 +
   34.24 +from Products.GenericSetup.interfaces import INode
   34.25 +from Products.GenericSetup.utils import ObjectManagerHelpers
   34.26 +from Products.GenericSetup.utils import PropertyManagerHelpers
   34.27 +from Products.GenericSetup.utils import XMLAdapterBase
   34.28 +
   34.29 +from Products.ZCatalog.interfaces import IZCatalog
   34.30 +
   34.31 +
   34.32 +class _extra:
   34.33 +
   34.34 +    pass
   34.35 +
   34.36 +
   34.37 +class ZCatalogXMLAdapter(XMLAdapterBase, ObjectManagerHelpers,
   34.38 +                         PropertyManagerHelpers):
   34.39 +
   34.40 +    """XML im- and exporter for ZCatalog.
   34.41 +    """
   34.42 +
   34.43 +    __used_for__ = IZCatalog
   34.44 +
   34.45 +    _LOGGER_ID = 'catalog'
   34.46 +
   34.47 +    name = 'catalog'
   34.48 +
   34.49 +    def _exportNode(self):
   34.50 +        """Export the object as a DOM node.
   34.51 +        """
   34.52 +        node = self._getObjectNode('object')
   34.53 +        node.appendChild(self._extractProperties())
   34.54 +        node.appendChild(self._extractObjects())
   34.55 +        node.appendChild(self._extractIndexes())
   34.56 +        node.appendChild(self._extractColumns())
   34.57 +
   34.58 +        self._logger.info('Catalog exported.')
   34.59 +        return node
   34.60 +
   34.61 +    def _importNode(self, node):
   34.62 +        """Import the object from the DOM node.
   34.63 +        """
   34.64 +        if self.environ.shouldPurge():
   34.65 +            self._purgeProperties()
   34.66 +            self._purgeObjects()
   34.67 +            self._purgeIndexes()
   34.68 +            self._purgeColumns()
   34.69 +
   34.70 +        self._initProperties(node)
   34.71 +        self._initObjects(node)
   34.72 +        self._initIndexes(node)
   34.73 +        self._initColumns(node)
   34.74 +
   34.75 +        self._logger.info('Catalog imported.')
   34.76 +
   34.77 +    def _extractIndexes(self):
   34.78 +        fragment = self._doc.createDocumentFragment()
   34.79 +        indexes = self.context.getIndexObjects()[:]
   34.80 +        indexes.sort(lambda x,y: cmp(x.getId(), y.getId()))
   34.81 +        for idx in indexes:
   34.82 +            exporter = zapi.queryMultiAdapter((idx, self.environ), INode)
   34.83 +            if exporter:
   34.84 +                fragment.appendChild(exporter.node)
   34.85 +        return fragment
   34.86 +
   34.87 +    def _purgeIndexes(self):
   34.88 +        for idx_id in self.context.indexes():
   34.89 +            self.context.delIndex(idx_id)
   34.90 +
   34.91 +    def _initIndexes(self, node):
   34.92 +        for child in node.childNodes:
   34.93 +            if child.nodeName != 'index':
   34.94 +                continue
   34.95 +            if child.hasAttribute('deprecated'):
   34.96 +                continue
   34.97 +            zcatalog = self.context
   34.98 +
   34.99 +            idx_id = str(child.getAttribute('name'))
  34.100 +            if idx_id not in zcatalog.indexes():
  34.101 +                extra = _extra()
  34.102 +                for sub in child.childNodes:
  34.103 +                    if sub.nodeName == 'extra':
  34.104 +                        name = str(sub.getAttribute('name'))
  34.105 +                        value = str(sub.getAttribute('value'))
  34.106 +                        setattr(extra, name, value)
  34.107 +                extra = extra.__dict__ and extra or None
  34.108 +
  34.109 +                meta_type = str(child.getAttribute('meta_type'))
  34.110 +                zcatalog.addIndex(idx_id, meta_type, extra)
  34.111 +
  34.112 +            idx = zcatalog._catalog.getIndex(idx_id)
  34.113 +            importer = zapi.queryMultiAdapter((idx, self.environ), INode)
  34.114 +            if importer:
  34.115 +                importer.node = child
  34.116 +
  34.117 +    def _extractColumns(self):
  34.118 +        fragment = self._doc.createDocumentFragment()
  34.119 +        schema = self.context.schema()[:]
  34.120 +        schema.sort()
  34.121 +        for col in schema:
  34.122 +            child = self._doc.createElement('column')
  34.123 +            child.setAttribute('value', col)
  34.124 +            fragment.appendChild(child)
  34.125 +        return fragment
  34.126 +
  34.127 +    def _purgeColumns(self):
  34.128 +        for col in self.context.schema()[:]:
  34.129 +            self.context.delColumn(col)
  34.130 +
  34.131 +    def _initColumns(self, node):
  34.132 +        for child in node.childNodes:
  34.133 +            if child.nodeName != 'column':
  34.134 +                continue
  34.135 +            col = str(child.getAttribute('value'))
  34.136 +            if col not in self.context.schema()[:]:
  34.137 +                self.context.addColumn(col)
    35.1 new file mode 100644
    35.2 --- /dev/null
    35.3 +++ b/ZCatalog/tests/__init__.py
    35.4 @@ -0,0 +1,16 @@
    35.5 +##############################################################################
    35.6 +#
    35.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    35.8 +#
    35.9 +# This software is subject to the provisions of the Zope Public License,
   35.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   35.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   35.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   35.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   35.14 +# FOR A PARTICULAR PURPOSE.
   35.15 +#
   35.16 +##############################################################################
   35.17 +"""ZCatalog support tests.
   35.18 +
   35.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   35.20 +"""
    36.1 new file mode 100644
    36.2 --- /dev/null
    36.3 +++ b/ZCatalog/tests/test_exportimport.py
    36.4 @@ -0,0 +1,163 @@
    36.5 +##############################################################################
    36.6 +#
    36.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    36.8 +#
    36.9 +# This software is subject to the provisions of the Zope Public License,
   36.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   36.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   36.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   36.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   36.14 +# FOR A PARTICULAR PURPOSE.
   36.15 +#
   36.16 +##############################################################################
   36.17 +"""ZCatalog export / import support unit tests.
   36.18 +
   36.19 +$Id: test_exportimport.py 40715 2005-12-12 10:33:40Z yuppie $
   36.20 +"""
   36.21 +
   36.22 +import unittest
   36.23 +import Testing
   36.24 +import Zope2
   36.25 +Zope2.startup()
   36.26 +
   36.27 +from Products.Five import zcml
   36.28 +from zope.app import zapi
   36.29 +
   36.30 +from Products.GenericSetup.interfaces import IBody
   36.31 +from Products.GenericSetup.testing import BodyAdapterTestCase
   36.32 +from Products.GenericSetup.testing import DummySetupEnviron
   36.33 +
   36.34 +
   36.35 +class _extra:
   36.36 +
   36.37 +    pass
   36.38 +
   36.39 +
   36.40 +_CATALOG_BODY = """\
   36.41 +<?xml version="1.0"?>
   36.42 +<object name="foo_catalog" meta_type="ZCatalog">
   36.43 + <property name="title"></property>
   36.44 + <object name="foo_plexicon" meta_type="ZCTextIndex Lexicon">
   36.45 +  <element name="Whitespace splitter" group="Word Splitter"/>
   36.46 +  <element name="Case Normalizer" group="Case Normalizer"/>
   36.47 +  <element name="Remove listed stop words only" group="Stop Words"/>
   36.48 + </object>
   36.49 +%s <index name="foo_date" meta_type="DateIndex">
   36.50 +  <property name="index_naive_time_as_local">True</property>
   36.51 + </index>
   36.52 + <index name="foo_daterange" meta_type="DateRangeIndex" since_field="bar"
   36.53 +    until_field="baz"/>
   36.54 + <index name="foo_field" meta_type="FieldIndex">
   36.55 +  <indexed_attr value="bar"/>
   36.56 + </index>
   36.57 + <index name="foo_keyword" meta_type="KeywordIndex">
   36.58 +  <indexed_attr value="bar"/>
   36.59 + </index>
   36.60 + <index name="foo_path" meta_type="PathIndex"/>
   36.61 +%s <index name="foo_topic" meta_type="TopicIndex">
   36.62 +  <filtered_set name="bar" meta_type="PythonFilteredSet" expression="True"/>
   36.63 +  <filtered_set name="baz" meta_type="PythonFilteredSet" expression="False"/>
   36.64 + </index>
   36.65 + <index name="foo_zctext" meta_type="ZCTextIndex">
   36.66 +  <indexed_attr value="foo_zctext"/>
   36.67 +  <extra name="index_type" value="Okapi BM25 Rank"/>
   36.68 +  <extra name="lexicon_id" value="foo_plexicon"/>
   36.69 + </index>
   36.70 + <column value="eggs"/>
   36.71 + <column value="spam"/>
   36.72 +</object>
   36.73 +"""
   36.74 +
   36.75 +_TEXT_XML = """\
   36.76 + <index name="foo_text" meta_type="TextIndex" deprecated="True"/>
   36.77 +"""
   36.78 +
   36.79 +_VOCABULARY_XML = """\
   36.80 + <object name="foo_vocabulary" meta_type="Vocabulary" deprecated="True"/>
   36.81 +"""
   36.82 +
   36.83 +
   36.84 +class ZCatalogXMLAdapterTests(BodyAdapterTestCase):
   36.85 +
   36.86 +    def _getTargetClass(self):
   36.87 +        from Products.GenericSetup.ZCatalog.exportimport \
   36.88 +                import ZCatalogXMLAdapter
   36.89 +
   36.90 +        return ZCatalogXMLAdapter
   36.91 +
   36.92 +    def _populate(self, obj):
   36.93 +        from Products.ZCTextIndex.Lexicon import CaseNormalizer
   36.94 +        from Products.ZCTextIndex.Lexicon import Splitter
   36.95 +        from Products.ZCTextIndex.Lexicon import StopWordRemover
   36.96 +        from Products.ZCTextIndex.ZCTextIndex import PLexicon
   36.97 +
   36.98 +        obj._setObject('foo_plexicon', PLexicon('foo_plexicon'))
   36.99 +        lex = obj.foo_plexicon
  36.100 +        lex._pipeline = (Splitter(), CaseNormalizer(), StopWordRemover())
  36.101 +
  36.102 +        obj.addIndex('foo_date', 'DateIndex')
  36.103 +
  36.104 +        obj.addIndex('foo_daterange', 'DateRangeIndex')
  36.105 +        idx = obj._catalog.getIndex('foo_daterange')
  36.106 +        idx._edit('bar', 'baz')
  36.107 +
  36.108 +        obj.addIndex('foo_field', 'FieldIndex')
  36.109 +        idx = obj._catalog.getIndex('foo_field')
  36.110 +        idx.indexed_attrs = ('bar',)
  36.111 +
  36.112 +        obj.addIndex('foo_keyword', 'KeywordIndex')
  36.113 +        idx = obj._catalog.getIndex('foo_keyword')
  36.114 +        idx.indexed_attrs = ('bar',)
  36.115 +
  36.116 +        obj.addIndex('foo_path', 'PathIndex')
  36.117 +
  36.118 +        obj.addIndex('foo_topic', 'TopicIndex')
  36.119 +        idx = obj._catalog.getIndex('foo_topic')
  36.120 +        idx.addFilteredSet('bar', 'PythonFilteredSet', 'True')
  36.121 +        idx.addFilteredSet('baz', 'PythonFilteredSet', 'False')
  36.122 +
  36.123 +        extra = _extra()
  36.124 +        extra.lexicon_id = 'foo_plexicon'
  36.125 +        extra.index_type = 'Okapi BM25 Rank'
  36.126 +        obj.addIndex('foo_zctext', 'ZCTextIndex', extra)
  36.127 +
  36.128 +        obj.addColumn('spam')
  36.129 +        obj.addColumn('eggs')
  36.130 +
  36.131 +    def _populate_special(self, obj):
  36.132 +        from Products.PluginIndexes.TextIndex.Vocabulary import Vocabulary
  36.133 +
  36.134 +        self._populate(self._obj)
  36.135 +        obj._setObject('foo_vocabulary', Vocabulary('foo_vocabulary'))
  36.136 +        obj.addIndex('foo_text', 'TextIndex')
  36.137 +
  36.138 +    def setUp(self):
  36.139 +        import Products.GenericSetup.PluginIndexes
  36.140 +        import Products.GenericSetup.ZCatalog
  36.141 +        import Products.GenericSetup.ZCTextIndex
  36.142 +        from Products.ZCatalog.ZCatalog import ZCatalog
  36.143 +
  36.144 +        BodyAdapterTestCase.setUp(self)
  36.145 +        zcml.load_config('configure.zcml',
  36.146 +                         Products.GenericSetup.PluginIndexes)
  36.147 +        zcml.load_config('configure.zcml', Products.GenericSetup.ZCatalog)
  36.148 +        zcml.load_config('configure.zcml', Products.GenericSetup.ZCTextIndex)
  36.149 +
  36.150 +        self._obj = ZCatalog('foo_catalog')
  36.151 +        self._BODY = _CATALOG_BODY % ('', '')
  36.152 +
  36.153 +    def test_body_get_special(self):
  36.154 +        self._populate_special(self._obj)
  36.155 +        context = DummySetupEnviron()
  36.156 +        adapted = zapi.getMultiAdapter((self._obj, context), IBody)
  36.157 +        self.assertEqual(adapted.body,
  36.158 +                         _CATALOG_BODY % (_VOCABULARY_XML, _TEXT_XML))
  36.159 +
  36.160 +
  36.161 +def test_suite():
  36.162 +    return unittest.TestSuite((
  36.163 +        unittest.makeSuite(ZCatalogXMLAdapterTests),
  36.164 +        ))
  36.165 +
  36.166 +if __name__ == '__main__':
  36.167 +    unittest.main(defaultTest='test_suite')
    37.1 new file mode 100644
    37.2 --- /dev/null
    37.3 +++ b/__init__.py
    37.4 @@ -0,0 +1,38 @@
    37.5 +""" GenericSetup product initialization.
    37.6 +
    37.7 +$Id: __init__.py,v 1.1.1.1 2005/08/08 19:38:37 tseaver Exp $
    37.8 +"""
    37.9 +
   37.10 +from AccessControl import ModuleSecurityInfo
   37.11 +
   37.12 +from interfaces import BASE, EXTENSION
   37.13 +from permissions import ManagePortal
   37.14 +from registry import _profile_registry as profile_registry
   37.15 +
   37.16 +security = ModuleSecurityInfo('Products.GenericSetup')
   37.17 +security.declareProtected(ManagePortal, 'profile_registry')
   37.18 +
   37.19 +def initialize(context):
   37.20 +
   37.21 +    import tool
   37.22 +
   37.23 +    context.registerClass(tool.SetupTool,
   37.24 +                          constructors=(#tool.addSetupToolForm,
   37.25 +                                        tool.addSetupTool,
   37.26 +                                        ),
   37.27 +                          permissions=(ManagePortal,),
   37.28 +                          interfaces=None,
   37.29 +                          icon='www/tool.png',
   37.30 +                         )
   37.31 +
   37.32 +# BBB: for setup tools created with CMF 1.5 if CMFSetup isn't installed
   37.33 +try:
   37.34 +    import Products.CMFSetup
   37.35 +except ImportError:
   37.36 +    import bbb
   37.37 +    import bbb.registry
   37.38 +    import bbb.tool
   37.39 +
   37.40 +    __module_aliases__ = (('Products.CMFSetup', bbb),
   37.41 +                          ('Products.CMFSetup.registry', bbb.registry),
   37.42 +                          ('Products.CMFSetup.tool', bbb.tool))
    38.1 new file mode 100644
    38.2 --- /dev/null
    38.3 +++ b/bbb/__init__.py
    38.4 @@ -0,0 +1,16 @@
    38.5 +##############################################################################
    38.6 +#
    38.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    38.8 +#
    38.9 +# This software is subject to the provisions of the Zope Public License,
   38.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   38.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   38.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   38.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   38.14 +# FOR A PARTICULAR PURPOSE.
   38.15 +#
   38.16 +##############################################################################
   38.17 +""" CMFSetup product initialization.
   38.18 +
   38.19 +$Id: __init__.py 40429 2005-11-30 22:12:58Z yuppie $
   38.20 +"""
    39.1 new file mode 100644
    39.2 --- /dev/null
    39.3 +++ b/bbb/registry.py
    39.4 @@ -0,0 +1,21 @@
    39.5 +##############################################################################
    39.6 +#
    39.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    39.8 +#
    39.9 +# This software is subject to the provisions of the Zope Public License,
   39.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   39.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   39.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   39.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   39.14 +# FOR A PARTICULAR PURPOSE.
   39.15 +#
   39.16 +##############################################################################
   39.17 +""" Classes:  ImportStepRegistry, ExportStepRegistry
   39.18 +
   39.19 +$Id: registry.py 40429 2005-11-30 22:12:58Z yuppie $
   39.20 +"""
   39.21 +
   39.22 +# BBB: for setup tools created with CMF 1.5
   39.23 +from Products.GenericSetup.registry import ImportStepRegistry
   39.24 +from Products.GenericSetup.registry import ExportStepRegistry
   39.25 +from Products.GenericSetup.registry import ToolsetRegistry
    40.1 new file mode 100644
    40.2 --- /dev/null
    40.3 +++ b/bbb/tool.py
    40.4 @@ -0,0 +1,30 @@
    40.5 +##############################################################################
    40.6 +#
    40.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    40.8 +#
    40.9 +# This software is subject to the provisions of the Zope Public License,
   40.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   40.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   40.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   40.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   40.14 +# FOR A PARTICULAR PURPOSE.
   40.15 +#
   40.16 +##############################################################################
   40.17 +""" Classes:  SetupTool
   40.18 +
   40.19 +$Id: tool.py 40429 2005-11-30 22:12:58Z yuppie $
   40.20 +"""
   40.21 +
   40.22 +from Products.GenericSetup.tool import exportStepRegistries
   40.23 +from Products.GenericSetup.tool import importToolset
   40.24 +from Products.GenericSetup.tool import exportToolset
   40.25 +from Products.GenericSetup.tool import SetupTool as BaseTool
   40.26 +
   40.27 +
   40.28 +class SetupTool(BaseTool):
   40.29 +
   40.30 +    #BBB: for setup tools created with CMF 1.5
   40.31 +    id = 'portal_setup'
   40.32 +
   40.33 +    def __init__(self, id='portal_setup'):
   40.34 +        BaseTool.__init__(self, id)
    41.1 new file mode 100644
    41.2 --- /dev/null
    41.3 +++ b/browser/__init__.py
    41.4 @@ -0,0 +1,16 @@
    41.5 +##############################################################################
    41.6 +#
    41.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    41.8 +#
    41.9 +# This software is subject to the provisions of the Zope Public License,
   41.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   41.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   41.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   41.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   41.14 +# FOR A PARTICULAR PURPOSE.
   41.15 +#
   41.16 +##############################################################################
   41.17 +"""GenericSetup browser views.
   41.18 +
   41.19 +$Id: __init__.py 40355 2005-11-24 11:39:06Z yuppie $
   41.20 +"""
    42.1 new file mode 100644
    42.2 --- /dev/null
    42.3 +++ b/browser/addWithPresettings.pt
    42.4 @@ -0,0 +1,46 @@
    42.5 +<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
    42.6 +<h2 tal:define="form_title view/title"
    42.7 +    tal:replace="structure context/manage_form_title">FORM TITLE</h2>
    42.8 +
    42.9 +<p class="form-help" tal:content="view/description">DESCRIPTION TEXT.</p>
   42.10 +
   42.11 +<form action="." method="post"
   42.12 +   tal:attributes="action request/ACTUAL_URL">
   42.13 +<table cellspacing="0" cellpadding="2" border="0">
   42.14 + <tr>
   42.15 +  <td>
   42.16 +   <div class="form-label">ID</div>
   42.17 +  </td>
   42.18 +  <td>
   42.19 +   <input type="text" name="add_input_name" size="40" />
   42.20 +  </td>
   42.21 + </tr>
   42.22 + <tr tal:condition="view/getProfileInfos">
   42.23 +  <td>
   42.24 +   <div class="form-label">Presettings</div>
   42.25 +  </td>
   42.26 +  <td>
   42.27 +   <select name="settings_id">
   42.28 +    <option value="" selected="selected">(None)</option>
   42.29 +    <optgroup label="PROFILE_TITLE"
   42.30 +       tal:repeat="profile view/getProfileInfos"
   42.31 +       tal:attributes="label profile/title">
   42.32 +     <option value="SETTINGS_ID"
   42.33 +             tal:repeat="obj_id profile/obj_ids"
   42.34 +             tal:attributes="value string:${profile/id}/${obj_id}"
   42.35 +             tal:content="obj_id">OBJ ID</option></optgroup>
   42.36 +   </select>
   42.37 +  </td>
   42.38 + </tr>
   42.39 + <tr>
   42.40 +  <td>
   42.41 +   &nbsp;
   42.42 +  </td>
   42.43 +  <td>
   42.44 +   <input class="form-element" type="submit" name="submit_add" value="Add" /> 
   42.45 +  </td>
   42.46 + </tr>
   42.47 +</table>
   42.48 +</form>
   42.49 +
   42.50 +<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
    43.1 new file mode 100644
    43.2 --- /dev/null
    43.3 +++ b/browser/utils.py
    43.4 @@ -0,0 +1,39 @@
    43.5 +##############################################################################
    43.6 +#
    43.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    43.8 +#
    43.9 +# This software is subject to the provisions of the Zope Public License,
   43.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   43.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   43.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   43.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   43.14 +# FOR A PARTICULAR PURPOSE.
   43.15 +#
   43.16 +##############################################################################
   43.17 +"""GenericSetup browser view utils.
   43.18 +
   43.19 +$Id: utils.py 40715 2005-12-12 10:33:40Z yuppie $
   43.20 +"""
   43.21 +
   43.22 +class AddWithPresettingsViewBase:
   43.23 +
   43.24 +    """Base class for add views with selectable presettings.
   43.25 +    """
   43.26 +
   43.27 +    def title(self):
   43.28 +        return u'Add %s' % self.klass.meta_type
   43.29 +
   43.30 +    def __call__(self, add_input_name='', settings_id='', submit_add=''):
   43.31 +        if submit_add:
   43.32 +            obj = self.klass('temp')
   43.33 +            if settings_id:
   43.34 +                ids = settings_id.split('/')
   43.35 +                profile_id = ids[0]
   43.36 +                obj_path = ids[1:]
   43.37 +                if not add_input_name:
   43.38 +                    self.request.set('add_input_name', obj_path[-1])
   43.39 +                self._initSettings(obj, profile_id, obj_path)
   43.40 +            self.context.add(obj)
   43.41 +            self.request.response.redirect(self.context.nextURL())
   43.42 +            return ''
   43.43 +        return self.index()
    44.1 new file mode 100644
    44.2 --- /dev/null
    44.3 +++ b/configure.zcml
    44.4 @@ -0,0 +1,53 @@
    44.5 +<configure
    44.6 +    xmlns="http://namespaces.zope.org/zope"
    44.7 +    >
    44.8 +
    44.9 +  <include package=".MailHost"/>
   44.10 +
   44.11 +  <include package=".OFSP"/>
   44.12 +
   44.13 +  <include package=".PluginIndexes"/>
   44.14 +
   44.15 +  <include package=".PythonScripts"/>
   44.16 +
   44.17 +  <include package=".ZCatalog"/>
   44.18 +
   44.19 +  <include package=".ZCTextIndex"/>
   44.20 +
   44.21 +  <adapter
   44.22 +      factory=".content.CSVAwareFileAdapter"
   44.23 +      provides="Products.GenericSetup.interfaces.IFilesystemExporter"
   44.24 +      for="Products.GenericSetup.interfaces.ICSVAware"
   44.25 +      />
   44.26 +
   44.27 +  <adapter
   44.28 +      factory=".content.CSVAwareFileAdapter"
   44.29 +      provides="Products.GenericSetup.interfaces.IFilesystemImporter"
   44.30 +      for="Products.GenericSetup.interfaces.ICSVAware"
   44.31 +      />
   44.32 +
   44.33 +  <adapter
   44.34 +      factory=".content.INIAwareFileAdapter"
   44.35 +      provides="Products.GenericSetup.interfaces.IFilesystemExporter"
   44.36 +      for="Products.GenericSetup.interfaces.IINIAware"
   44.37 +      />
   44.38 +
   44.39 +  <adapter
   44.40 +      factory=".content.INIAwareFileAdapter"
   44.41 +      provides="Products.GenericSetup.interfaces.IFilesystemImporter"
   44.42 +      for="Products.GenericSetup.interfaces.IINIAware"
   44.43 +      />
   44.44 +
   44.45 +  <adapter
   44.46 +      factory=".content.DAVAwareFileAdapter"
   44.47 +      provides="Products.GenericSetup.interfaces.IFilesystemExporter"
   44.48 +      for="Products.GenericSetup.interfaces.IDAVAware"
   44.49 +      />
   44.50 +
   44.51 +  <adapter
   44.52 +      factory=".content.DAVAwareFileAdapter"
   44.53 +      provides="Products.GenericSetup.interfaces.IFilesystemImporter"
   44.54 +      for="Products.GenericSetup.interfaces.IDAVAware"
   44.55 +      />
   44.56 +
   44.57 +</configure>
    45.1 new file mode 100644
    45.2 --- /dev/null
    45.3 +++ b/content.py
    45.4 @@ -0,0 +1,404 @@
    45.5 +##############################################################################
    45.6 +#
    45.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    45.8 +#
    45.9 +# This software is subject to the provisions of the Zope Public License,
   45.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   45.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   45.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   45.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   45.14 +# FOR A PARTICULAR PURPOSE.
   45.15 +#
   45.16 +##############################################################################
   45.17 +"""Filesystem exporter / importer adapters.
   45.18 +
   45.19 +$Id: content.py 40285 2005-11-21 02:46:17Z tseaver $
   45.20 +"""
   45.21 +
   45.22 +from csv import reader
   45.23 +from csv import register_dialect
   45.24 +from csv import writer
   45.25 +from ConfigParser import ConfigParser
   45.26 +import re
   45.27 +from StringIO import StringIO
   45.28 +
   45.29 +from zope.interface import implements
   45.30 +from zope.interface import directlyProvides
   45.31 +from zope.app import zapi
   45.32 +
   45.33 +from interfaces import IContentFactory
   45.34 +from interfaces import IContentFactoryName
   45.35 +from interfaces import IFilesystemExporter
   45.36 +from interfaces import IFilesystemImporter
   45.37 +from interfaces import IINIAware
   45.38 +from interfaces import ISetupTool
   45.39 +from utils import _getDottedName
   45.40 +from utils import _resolveDottedName
   45.41 +
   45.42 +#
   45.43 +#   setup_tool handlers
   45.44 +#
   45.45 +def exportSiteStructure(context):
   45.46 +    IFilesystemExporter(context.getSite()).export(context, 'structure', True)
   45.47 +
   45.48 +def importSiteStructure(context):
   45.49 +    IFilesystemImporter(context.getSite()).import_(context, 'structure', True)
   45.50 +
   45.51 +
   45.52 +#
   45.53 +#   Filesystem export/import adapters
   45.54 +#
   45.55 +class FolderishExporterImporter(object):
   45.56 +    """ Tree-walking exporter / importer for "folderish" types.
   45.57 +
   45.58 +    Folderish instances are mapped to directories within the 'structure'
   45.59 +    portion of the profile, where the folder's relative path within the site
   45.60 +    corresponds to the path of its directory under 'structure'.
   45.61 +
   45.62 +    The subobjects of a folderish instance are enumerated in the '.objects'
   45.63 +    file in the corresponding directory.  This file is a CSV file, with one
   45.64 +    row per subobject, with the following wtructure::
   45.65 +
   45.66 +     "<subobject id>","<subobject portal_type>"
   45.67 +
   45.68 +    Subobjects themselves are represented as individual files or
   45.69 +    subdirectories within the parent's directory.
   45.70 +    """
   45.71 +
   45.72 +    implements(IFilesystemExporter, IFilesystemImporter)
   45.73 +
   45.74 +    def __init__(self, context):
   45.75 +        self.context = context
   45.76 +
   45.77 +    def listExportableItems(self):
   45.78 +        """ See IFilesystemExporter.
   45.79 +        """
   45.80 +        exportable = self.context.objectItems()
   45.81 +        exportable = [x for x in exportable
   45.82 +                        if not ISetupTool.providedBy(x[1])]
   45.83 +        exportable = [x + (IFilesystemExporter(x[1], None),)
   45.84 +                        for x in exportable]
   45.85 +        return exportable
   45.86 +
   45.87 +    def export(self, export_context, subdir, root=False):
   45.88 +        """ See IFilesystemExporter.
   45.89 +        """
   45.90 +        context = self.context
   45.91 +
   45.92 +        if not root:
   45.93 +            subdir = '%s/%s' % (subdir, context.getId())
   45.94 +
   45.95 +        exportable = self.listExportableItems()
   45.96 +
   45.97 +        stream = StringIO()
   45.98 +        csv_writer = writer(stream)
   45.99 +
  45.100 +        for object_id, object, adapter in exportable:
  45.101 +
  45.102 +            factory_namer = IContentFactoryName(object, None)
  45.103 +            if factory_namer is None:
  45.104 +                factory_name = _getDottedName(object.__class__)
  45.105 +            else:
  45.106 +                factory_name = factory_namer()
  45.107 +
  45.108 +            csv_writer.writerow((object_id, factory_name))
  45.109 +
  45.110 +        export_context.writeDataFile('.objects',
  45.111 +                                     text=stream.getvalue(),
  45.112 +                                     content_type='text/comma-separated-values',
  45.113 +                                     subdir=subdir,
  45.114 +                                    )
  45.115 +
  45.116 +        prop_adapter = IINIAware(context, None)
  45.117 +
  45.118 +        if prop_adapter is not None:
  45.119 +            export_context.writeDataFile('.properties',
  45.120 +                                         text=prop_adapter.as_ini(),
  45.121 +                                         content_type='text/plain',
  45.122 +                                         subdir=subdir,
  45.123 +                                        )
  45.124 +
  45.125 +        for object_id, object, adapter in exportable:
  45.126 +            if adapter is not None:
  45.127 +                adapter.export(export_context, subdir)
  45.128 +
  45.129 +    def import_(self, import_context, subdir, root=False):
  45.130 +        """ See IFilesystemImporter.
  45.131 +        """
  45.132 +        context = self.context
  45.133 +        if not root:
  45.134 +            subdir = '%s/%s' % (subdir, context.getId())
  45.135 +
  45.136 +        prop_adapter = IINIAware(context, None)
  45.137 +
  45.138 +        if prop_adapter is not None:
  45.139 +            prop_text = import_context.readDataFile('.properties',
  45.140 +                                                    subdir=subdir,
  45.141 +                                                   )
  45.142 +            if prop_text is not None:
  45.143 +                prop_adapter.put_ini(prop_text)
  45.144 +
  45.145 +        preserve = import_context.readDataFile('.preserve', subdir)
  45.146 +        must_preserve = self._mustPreserve()
  45.147 +
  45.148 +        prior = context.objectIds()
  45.149 +
  45.150 +        if not preserve:
  45.151 +            preserve = []
  45.152 +        else:
  45.153 +            preserve = _globtest(preserve, prior)
  45.154 +
  45.155 +        preserve.extend([x[0] for x in must_preserve])
  45.156 +
  45.157 +        for id in prior:
  45.158 +            if id not in preserve:
  45.159 +                context._delObject(id)
  45.160 +
  45.161 +        objects = import_context.readDataFile('.objects', subdir)
  45.162 +        if objects is None:
  45.163 +            return
  45.164 +
  45.165 +        dialect = 'excel'
  45.166 +        stream = StringIO(objects)
  45.167 +
  45.168 +        rowiter = reader(stream, dialect)
  45.169 +
  45.170 +        existing = context.objectIds()
  45.171 +
  45.172 +        for object_id, type_name in rowiter:
  45.173 +
  45.174 +            if object_id not in existing:
  45.175 +                object = self._makeInstance(object_id, type_name,
  45.176 +                                            subdir, import_context)
  45.177 +                if object is None:
  45.178 +                    logger = import_context.getLogger('SFWA')
  45.179 +                    logger.warning("Couldn't make instance: %s/%s" %
  45.180 +                                   (subdir, object_id))
  45.181 +                    continue
  45.182 +
  45.183 +            wrapped = context._getOb(object_id)
  45.184 +
  45.185 +            IFilesystemImporter(wrapped).import_(import_context, subdir)
  45.186 +
  45.187 +    def _makeInstance(self, instance_id, type_name, subdir, import_context):
  45.188 +
  45.189 +        context = self.context
  45.190 +        class _OldStyleClass:
  45.191 +            pass
  45.192 +
  45.193 +        if '.' in type_name:
  45.194 +
  45.195 +            factory = _resolveDottedName(type_name)
  45.196 +
  45.197 +            if getattr(factory, '__bases__', None) is not None:
  45.198 +
  45.199 +                def _factory(instance_id,
  45.200 +                             container=self.context,
  45.201 +                             klass=factory):
  45.202 +                    try:
  45.203 +                        instance = klass(instance_id)
  45.204 +                    except (TypeError, ValueError):
  45.205 +                        instance = klass()
  45.206 +                    instance._setId(instance_id)
  45.207 +                    container._setObject(instance_id, instance)
  45.208 +
  45.209 +                    return instance
  45.210 +
  45.211 +                factory = _factory
  45.212 +
  45.213 +        else:
  45.214 +            factory = zapi.queryAdapter(self.context,
  45.215 +                                        IContentFactory,
  45.216 +                                        name=type_name,
  45.217 +                                       )
  45.218 +        if factory is None:
  45.219 +            return None
  45.220 +
  45.221 +        try:
  45.222 +            instance = factory(instance_id)
  45.223 +        except ValueError: # invalid type
  45.224 +            return None
  45.225 +
  45.226 +        if context._getOb(instance_id, None) is None:
  45.227 +            context._setObject(instance_id, instance) 
  45.228 +
  45.229 +        return context._getOb(instance_id)
  45.230 +
  45.231 +    def _mustPreserve(self):
  45.232 +        return [x for x in self.context.objectItems()
  45.233 +                        if ISetupTool.providedBy(x[1])]
  45.234 + 
  45.235 +
  45.236 +def _globtest(globpattern, namelist):
  45.237 +    """ Filter names in 'namelist', returning those which match 'globpattern'.
  45.238 +    """
  45.239 +    import re
  45.240 +    pattern = globpattern.replace(".", r"\.")       # mask dots
  45.241 +    pattern = pattern.replace("*", r".*")           # change glob sequence
  45.242 +    pattern = pattern.replace("?", r".")            # change glob char
  45.243 +    pattern = '|'.join(pattern.split())             # 'or' each line
  45.244 +
  45.245 +    compiled = re.compile(pattern)
  45.246 +
  45.247 +    return filter(compiled.match, namelist)
  45.248 +
  45.249 +
  45.250 +class CSVAwareFileAdapter(object):
  45.251 +    """ Adapter for content whose "natural" representation is CSV.
  45.252 +    """
  45.253 +    implements(IFilesystemExporter, IFilesystemImporter)
  45.254 +
  45.255 +    def __init__(self, context):
  45.256 +        self.context = context
  45.257 +
  45.258 +    def export(self, export_context, subdir, root=False):
  45.259 +        """ See IFilesystemExporter.
  45.260 +        """
  45.261 +        export_context.writeDataFile('%s.csv' % self.context.getId(),
  45.262 +                                     self.context.as_csv(),
  45.263 +                                     'text/comma-separated-values',
  45.264 +                                     subdir,
  45.265 +                                    )
  45.266 +
  45.267 +    def listExportableItems(self):
  45.268 +        """ See IFilesystemExporter.
  45.269 +        """
  45.270 +        return ()
  45.271 +
  45.272 +    def import_(self, import_context, subdir, root=False):
  45.273 +        """ See IFilesystemImporter.
  45.274 +        """
  45.275 +        cid = self.context.getId()
  45.276 +        data = import_context.readDataFile('%s.csv' % cid, subdir)
  45.277 +        if data is None:
  45.278 +            logger = import_context.getLogger('CSAFA')
  45.279 +            logger.info('no .csv file for %s/%s' % (subdir, cid))
  45.280 +        else:
  45.281 +            stream = StringIO(data)
  45.282 +            self.context.put_csv(stream)
  45.283 +
  45.284 +class INIAwareFileAdapter(object):
  45.285 +    """ Exporter/importer for content whose "natural" representation is an
  45.286 +        '.ini' file.
  45.287 +    """
  45.288 +    implements(IFilesystemExporter, IFilesystemImporter)
  45.289 +
  45.290 +    def __init__(self, context):
  45.291 +        self.context = context
  45.292 +
  45.293 +    def export(self, export_context, subdir, root=False):
  45.294 +        """ See IFilesystemExporter.
  45.295 +        """
  45.296 +        export_context.writeDataFile('%s.ini' % self.context.getId(),
  45.297 +                                     self.context.as_ini(),
  45.298 +                                     'text/plain',
  45.299 +                                     subdir,
  45.300 +                                    )
  45.301 +
  45.302 +    def listExportableItems(self):
  45.303 +        """ See IFilesystemExporter.
  45.304 +        """
  45.305 +        return ()
  45.306 +
  45.307 +    def import_(self, import_context, subdir, root=False):
  45.308 +        """ See IFilesystemImporter.
  45.309 +        """
  45.310 +        cid = self.context.getId()
  45.311 +        data = import_context.readDataFile('%s.ini' % cid, subdir)
  45.312 +        if data is None:
  45.313 +            logger = import_context.getLogger('SGAIFA')
  45.314 +            logger.info('no .ini file for %s/%s' % (subdir, cid))
  45.315 +        else:
  45.316 +            self.context.put_ini(data)
  45.317 +
  45.318 +class SimpleINIAware(object):
  45.319 +    """ Exporter/importer for content which doesn't know from INI.
  45.320 +    """
  45.321 +    implements(IINIAware,)
  45.322 +
  45.323 +    def __init__(self, context):
  45.324 +        self.context = context
  45.325 +
  45.326 +    def getId(self):
  45.327 +        return self.context.getId()
  45.328 +
  45.329 +    def as_ini(self):
  45.330 +        """
  45.331 +        """
  45.332 +        context = self.context
  45.333 +        parser = ConfigParser()
  45.334 +        stream = StringIO()
  45.335 +        for k, v in context.propertyItems():
  45.336 +            parser.set('DEFAULT', k, str(v))
  45.337 +        parser.write(stream)
  45.338 +        return stream.getvalue()
  45.339 +
  45.340 +    def put_ini(self, text):
  45.341 +        """
  45.342 +        """
  45.343 +        context = self.context
  45.344 +        parser = ConfigParser()
  45.345 +        parser.readfp(StringIO(text))
  45.346 +        for option, value in parser.defaults().items():
  45.347 +            prop_type = context.getPropertyType(option)
  45.348 +            if prop_type is None:
  45.349 +                context._setProperty(option, value, 'string')
  45.350 +            else:
  45.351 +                context._updateProperty(option, value)
  45.352 +
  45.353 +class FauxDAVRequest:
  45.354 +
  45.355 +    def __init__(self, **kw):
  45.356 +        self._data = {}
  45.357 +        self._headers = {}
  45.358 +        self._data.update(kw)
  45.359 +
  45.360 +    def __getitem__(self, key):
  45.361 +        return self._data[key]
  45.362 +
  45.363 +    def get(self, key, default=None):
  45.364 +        return self._data.get(key, default)
  45.365 +
  45.366 +    def get_header(self, key, default=None):
  45.367 +        return self._headers.get(key, default)
  45.368 +
  45.369 +class FauxDAVResponse:
  45.370 +    def setHeader(self, key, value, lock=False):
  45.371 +        pass  # stub this out to mollify webdav.Resource
  45.372 +    def setStatus(self, value, reason=None):
  45.373 +        pass  # stub this out to mollify webdav.Resource
  45.374 +
  45.375 +class DAVAwareFileAdapter(object):
  45.376 +    """ Exporter/importer for content who handle their own FTP / DAV PUTs.
  45.377 +    """
  45.378 +    implements(IFilesystemExporter, IFilesystemImporter)
  45.379 +
  45.380 +    def __init__(self, context):
  45.381 +        self.context = context
  45.382 +
  45.383 +    def export(self, export_context, subdir, root=False):
  45.384 +        """ See IFilesystemExporter.
  45.385 +        """
  45.386 +        export_context.writeDataFile('%s' % self.context.getId(),
  45.387 +                                     self.context.manage_FTPget(),
  45.388 +                                     'text/plain',
  45.389 +                                     subdir,
  45.390 +                                    )
  45.391 +
  45.392 +    def listExportableItems(self):
  45.393 +        """ See IFilesystemExporter.
  45.394 +        """
  45.395 +        return ()
  45.396 +
  45.397 +    def import_(self, import_context, subdir, root=False):
  45.398 +        """ See IFilesystemImporter.
  45.399 +        """
  45.400 +        cid = self.context.getId()
  45.401 +        data = import_context.readDataFile('%s' % cid, subdir)
  45.402 +        if data is None:
  45.403 +            logger = import_context.getLogger('SGAIFA')
  45.404 +            logger.info('no .ini file for %s/%s' % (subdir, cid))
  45.405 +        else:
  45.406 +            request = FauxDAVRequest(BODY=data, BODYFILE=StringIO(data))
  45.407 +            response = FauxDAVResponse()
  45.408 +            self.context.PUT(request, response)
    46.1 new file mode 100644
    46.2 --- /dev/null
    46.3 +++ b/context.py
    46.4 @@ -0,0 +1,620 @@
    46.5 +##############################################################################
    46.6 +#
    46.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    46.8 +#
    46.9 +# This software is subject to the provisions of the Zope Public License,
   46.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   46.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   46.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   46.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   46.14 +# FOR A PARTICULAR PURPOSE.
   46.15 +#
   46.16 +##############################################################################
   46.17 +""" Various context implementations for export / import of configurations.
   46.18 +
   46.19 +Wrappers representing the state of an import / export operation.
   46.20 +
   46.21 +$Id: context.py 40715 2005-12-12 10:33:40Z yuppie $
   46.22 +"""
   46.23 +
   46.24 +import logging
   46.25 +import os
   46.26 +import time
   46.27 +from StringIO import StringIO
   46.28 +from tarfile import TarFile
   46.29 +from tarfile import TarInfo
   46.30 +
   46.31 +from AccessControl import ClassSecurityInfo
   46.32 +from Acquisition import aq_inner
   46.33 +from Acquisition import aq_parent
   46.34 +from Acquisition import aq_self
   46.35 +from Acquisition import Implicit
   46.36 +from DateTime.DateTime import DateTime
   46.37 +from Globals import InitializeClass
   46.38 +from OFS.DTMLDocument import DTMLDocument
   46.39 +from OFS.Folder import Folder
   46.40 +from OFS.Image import File
   46.41 +from OFS.Image import Image
   46.42 +from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
   46.43 +from Products.PythonScripts.PythonScript import PythonScript
   46.44 +from zope.interface import implements
   46.45 +
   46.46 +from interfaces import IExportContext
   46.47 +from interfaces import IImportContext
   46.48 +from interfaces import IWriteLogger
   46.49 +from permissions import ManagePortal
   46.50 +
   46.51 +
   46.52 +class Logger:
   46.53 +
   46.54 +    implements(IWriteLogger)
   46.55 +
   46.56 +    def __init__(self, id, messages):
   46.57 +        """Initialize the logger with a name and an optional level.
   46.58 +        """
   46.59 +        self._id = id
   46.60 +        self._messages = messages
   46.61 +        self._logger = logging.getLogger('GenericSetup.%s' % id)
   46.62 +
   46.63 +    def debug(self, msg, *args, **kwargs):
   46.64 +        """Log 'msg % args' with severity 'DEBUG'.
   46.65 +        """
   46.66 +        self.log(logging.DEBUG, msg, *args, **kwargs)
   46.67 +
   46.68 +    def info(self, msg, *args, **kwargs):
   46.69 +        """Log 'msg % args' with severity 'INFO'.
   46.70 +        """
   46.71 +        self.log(logging.INFO, msg, *args, **kwargs)
   46.72 +
   46.73 +    def warning(self, msg, *args, **kwargs):
   46.74 +        """Log 'msg % args' with severity 'WARNING'.
   46.75 +        """
   46.76 +        self.log(logging.WARNING, msg, *args, **kwargs)
   46.77 +
   46.78 +    def error(self, msg, *args, **kwargs):
   46.79 +        """Log 'msg % args' with severity 'ERROR'.
   46.80 +        """
   46.81 +        self.log(logging.ERROR, msg, *args, **kwargs)
   46.82 +
   46.83 +    def exception(self, msg, *args):
   46.84 +        """Convenience method for logging an ERROR with exception information.
   46.85 +        """
   46.86 +        self.error(msg, *args, **{'exc_info': 1})
   46.87 +
   46.88 +    def critical(self, msg, *args, **kwargs):
   46.89 +        """Log 'msg % args' with severity 'CRITICAL'.
   46.90 +        """
   46.91 +        self.log(logging.CRITICAL, msg, *args, **kwargs)
   46.92 +
   46.93 +    def log(self, level, msg, *args, **kwargs):
   46.94 +        """Log 'msg % args' with the integer severity 'level'.
   46.95 +        """
   46.96 +        self._messages.append((level, self._id, msg))
   46.97 +        self._logger.log(level, msg, *args, **kwargs)
   46.98 +
   46.99 +
  46.100 +class BaseContext( Implicit ):
  46.101 +
  46.102 +    security = ClassSecurityInfo()
  46.103 +
  46.104 +    def __init__( self, tool, encoding ):
  46.105 +
  46.106 +        self._tool = tool
  46.107 +        self._site = aq_parent( aq_inner( tool ) )
  46.108 +        self._loggers = {}
  46.109 +        self._messages = []
  46.110 +        self._encoding = encoding
  46.111 +        self._should_purge = True
  46.112 +
  46.113 +    security.declareProtected( ManagePortal, 'getSite' )
  46.114 +    def getSite( self ):
  46.115 +
  46.116 +        """ See ISetupContext.
  46.117 +        """
  46.118 +        return aq_self(self._site)
  46.119 +
  46.120 +    security.declareProtected( ManagePortal, 'getSetupTool' )
  46.121 +    def getSetupTool( self ):
  46.122 +
  46.123 +        """ See ISetupContext.
  46.124 +        """
  46.125 +        return self._tool
  46.126 +
  46.127 +    security.declareProtected( ManagePortal, 'getEncoding' )
  46.128 +    def getEncoding( self ):
  46.129 +
  46.130 +        """ See ISetupContext.
  46.131 +        """
  46.132 +        return self._encoding
  46.133 +
  46.134 +    security.declareProtected( ManagePortal, 'getLogger' )
  46.135 +    def getLogger( self, name ):
  46.136 +        """ See ISetupContext.
  46.137 +        """
  46.138 +        return self._loggers.setdefault(name, Logger(name, self._messages))
  46.139 +
  46.140 +    security.declareProtected( ManagePortal, 'listNotes' )
  46.141 +    def listNotes(self):
  46.142 +
  46.143 +        """ See ISetupContext.
  46.144 +        """
  46.145 +        return self._messages[:]
  46.146 +
  46.147 +    security.declareProtected( ManagePortal, 'clearNotes' )
  46.148 +    def clearNotes(self):
  46.149 +
  46.150 +        """ See ISetupContext.
  46.151 +        """
  46.152 +        self._messages[:] = []
  46.153 +
  46.154 +    security.declareProtected( ManagePortal, 'shouldPurge' )
  46.155 +    def shouldPurge( self ):
  46.156 +
  46.157 +        """ See ISetupContext.
  46.158 +        """
  46.159 +        return self._should_purge
  46.160 +
  46.161 +
  46.162 +class DirectoryImportContext( BaseContext ):
  46.163 +
  46.164 +    implements(IImportContext)
  46.165 +
  46.166 +    security = ClassSecurityInfo()
  46.167 +
  46.168 +    def __init__( self
  46.169 +                , tool
  46.170 +                , profile_path
  46.171 +                , should_purge=False
  46.172 +                , encoding=None
  46.173 +                ):
  46.174 +
  46.175 +        BaseContext.__init__( self, tool, encoding )
  46.176 +        self._profile_path = profile_path
  46.177 +        self._should_purge = bool( should_purge )
  46.178 +
  46.179 +    security.declareProtected( ManagePortal, 'readDataFile' )
  46.180 +    def readDataFile( self, filename, subdir=None ):
  46.181 +
  46.182 +        """ See IImportContext.
  46.183 +        """
  46.184 +        if subdir is None:
  46.185 +            full_path = os.path.join( self._profile_path, filename )
  46.186 +        else:
  46.187 +            full_path = os.path.join( self._profile_path, subdir, filename )
  46.188 +
  46.189 +        if not os.path.exists( full_path ):
  46.190 +            return None
  46.191 +
  46.192 +        file = open( full_path, 'rb' )
  46.193 +        result = file.read()
  46.194 +        file.close()
  46.195 +
  46.196 +        return result
  46.197 +
  46.198 +    security.declareProtected( ManagePortal, 'getLastModified' )
  46.199 +    def getLastModified( self, path ):
  46.200 +
  46.201 +        """ See IImportContext.
  46.202 +        """
  46.203 +        full_path = os.path.join( self._profile_path, path )
  46.204 +
  46.205 +        if not os.path.exists( full_path ):
  46.206 +            return None
  46.207 +
  46.208 +        return DateTime( os.path.getmtime( full_path ) )
  46.209 +
  46.210 +    security.declareProtected( ManagePortal, 'isDirectory' )
  46.211 +    def isDirectory( self, path ):
  46.212 +
  46.213 +        """ See IImportContext.
  46.214 +        """
  46.215 +        full_path = os.path.join( self._profile_path, path )
  46.216 +
  46.217 +        if not os.path.exists( full_path ):
  46.218 +            return None
  46.219 +
  46.220 +        return os.path.isdir( full_path )
  46.221 +
  46.222 +    security.declareProtected( ManagePortal, 'listDirectory' )
  46.223 +    def listDirectory( self, path, skip=('CVS', '.svn') ):
  46.224 +
  46.225 +        """ See IImportContext.
  46.226 +        """
  46.227 +        if path is None:
  46.228 +            path = ''
  46.229 +
  46.230 +        full_path = os.path.join( self._profile_path, path )
  46.231 +
  46.232 +        if not os.path.exists( full_path ) or not os.path.isdir( full_path ):
  46.233 +            return None
  46.234 +
  46.235 +        names = os.listdir( full_path )
  46.236 +
  46.237 +        return [ name for name in names if name not in skip ]
  46.238 +
  46.239 +InitializeClass( DirectoryImportContext )
  46.240 +
  46.241 +
  46.242 +class DirectoryExportContext( BaseContext ):
  46.243 +
  46.244 +    implements(IExportContext)
  46.245 +
  46.246 +    security = ClassSecurityInfo()
  46.247 +
  46.248 +    def __init__( self, tool, profile_path, encoding=None ):
  46.249 +
  46.250 +        BaseContext.__init__( self, tool, encoding )
  46.251 +        self._profile_path = profile_path
  46.252 +
  46.253 +    security.declareProtected( ManagePortal, 'writeDataFile' )
  46.254 +    def writeDataFile( self, filename, text, content_type, subdir=None ):
  46.255 +
  46.256 +        """ See IExportContext.
  46.257 +        """
  46.258 +        if subdir is None:
  46.259 +            prefix = self._profile_path
  46.260 +        else:
  46.261 +            prefix = os.path.join( self._profile_path, subdir )
  46.262 +
  46.263 +        full_path = os.path.join( prefix, filename )
  46.264 +
  46.265 +        if not os.path.exists( prefix ):
  46.266 +            os.makedirs( prefix )
  46.267 +
  46.268 +        mode = content_type.startswith( 'text/' ) and 'w' or 'wb'
  46.269 +
  46.270 +        file = open( full_path, mode )
  46.271 +        file.write( text )
  46.272 +        file.close()
  46.273 +
  46.274 +InitializeClass( DirectoryExportContext )
  46.275 +
  46.276 +
  46.277 +class TarballImportContext( BaseContext ):
  46.278 +
  46.279 +    implements(IImportContext)
  46.280 +
  46.281 +    security = ClassSecurityInfo()
  46.282 +
  46.283 +    def __init__( self, tool, archive_bits, encoding=None, should_purge=False ):
  46.284 +
  46.285 +        BaseContext.__init__( self, tool, encoding )
  46.286 +        timestamp = time.gmtime()
  46.287 +        self._archive_stream = StringIO(archive_bits)
  46.288 +        self._archive = TarFile.open( 'foo.bar', 'r:gz'
  46.289 +                                    , self._archive_stream )
  46.290 +        self._should_purge = bool( should_purge )
  46.291 +
  46.292 +    def readDataFile( self, filename, subdir=None ):
  46.293 +
  46.294 +        """ See IImportContext.
  46.295 +        """
  46.296 +        if subdir is not None:
  46.297 +            filename = '/'.join( ( subdir, filename ) )
  46.298 +
  46.299 +        try:
  46.300 +            file = self._archive.extractfile( filename )
  46.301 +        except KeyError:
  46.302 +            return None
  46.303 +
  46.304 +        return file.read()
  46.305 +
  46.306 +    def getLastModified( self, path ):
  46.307 +
  46.308 +        """ See IImportContext.
  46.309 +        """
  46.310 +        info = self._getTarInfo( path )
  46.311 +        return info and info.mtime or None
  46.312 +
  46.313 +    def isDirectory( self, path ):
  46.314 +
  46.315 +        """ See IImportContext.
  46.316 +        """
  46.317 +        info = self._getTarInfo( path )
  46.318 +
  46.319 +        if info is not None:
  46.320 +            return info.isdir()
  46.321 +
  46.322 +    def listDirectory( self, path, skip=('CVS', '.svn') ):
  46.323 +
  46.324 +        """ See IImportContext.
  46.325 +        """
  46.326 +        if path is None:  # root is special case:  no leading '/'
  46.327 +            path = ''
  46.328 +        else:
  46.329 +            if not self.isDirectory(path):
  46.330 +                return None
  46.331 +
  46.332 +            if path[-1] != '/':
  46.333 +                path = path + '/'
  46.334 +
  46.335 +        pfx_len = len(path)
  46.336 +
  46.337 +        beneath = [x[pfx_len:] for x in self._archive.getnames()
  46.338 +                                if x.startswith(path) and x != path]
  46.339 +
  46.340 +        return [x for x in beneath if '/' not in x and x not in skip]
  46.341 +
  46.342 +    def shouldPurge( self ):
  46.343 +
  46.344 +        """ See IImportContext.
  46.345 +        """
  46.346 +        return self._should_purge
  46.347 +
  46.348 +    def _getTarInfo( self, path ):
  46.349 +        if path[-1] == '/':
  46.350 +            path = path[:-1]
  46.351 +        try:
  46.352 +            return self._archive.getmember( path )
  46.353 +        except KeyError:
  46.354 +            pass
  46.355 +        try:
  46.356 +            return self._archive.getmember( path + '/' )
  46.357 +        except KeyError:
  46.358 +            return None
  46.359 +
  46.360 +
  46.361 +class TarballExportContext( BaseContext ):
  46.362 +
  46.363 +    implements(IExportContext)
  46.364 +
  46.365 +    security = ClassSecurityInfo()
  46.366 +
  46.367 +    def __init__( self, tool, encoding=None ):
  46.368 +
  46.369 +        BaseContext.__init__( self, tool, encoding )
  46.370 +
  46.371 +        timestamp = time.gmtime()
  46.372 +        archive_name = ( 'setup_tool-%4d%02d%02d%02d%02d%02d.tar.gz'
  46.373 +                       % timestamp[:6] )
  46.374 +
  46.375 +        self._archive_stream = StringIO()
  46.376 +        self._archive_filename = archive_name
  46.377 +        self._archive = TarFile.open( archive_name, 'w:gz'
  46.378 +                                    , self._archive_stream )
  46.379 +
  46.380 +    security.declareProtected( ManagePortal, 'writeDataFile' )
  46.381 +    def writeDataFile( self, filename, text, content_type, subdir=None ):
  46.382 +
  46.383 +        """ See IExportContext.
  46.384 +        """
  46.385 +        if subdir is not None:
  46.386 +            filename = '/'.join( ( subdir, filename ) )
  46.387 +
  46.388 +        stream = StringIO( text )
  46.389 +        info = TarInfo( filename )
  46.390 +        info.size = len( text )
  46.391 +        info.mtime = time.time()
  46.392 +        self._archive.addfile( info, stream )
  46.393 +
  46.394 +    security.declareProtected( ManagePortal, 'getArchive' )
  46.395 +    def getArchive( self ):
  46.396 +
  46.397 +        """ Close the archive, and return it as a big string.
  46.398 +        """
  46.399 +        self._archive.close()
  46.400 +        return self._archive_stream.getvalue()
  46.401 +
  46.402 +    security.declareProtected( ManagePortal, 'getArchiveFilename' )
  46.403 +    def getArchiveFilename( self ):
  46.404 +
  46.405 +        """ Close the archive, and return it as a big string.
  46.406 +        """
  46.407 +        return self._archive_filename
  46.408 +
  46.409 +InitializeClass( TarballExportContext )
  46.410 +
  46.411 +
  46.412 +class SnapshotExportContext( BaseContext ):
  46.413 +
  46.414 +    implements(IExportContext)
  46.415 +
  46.416 +    security = ClassSecurityInfo()
  46.417 +
  46.418 +    def __init__( self, tool, snapshot_id, encoding=None ):
  46.419 +
  46.420 +        BaseContext.__init__( self, tool, encoding )
  46.421 +        self._snapshot_id = snapshot_id
  46.422 +
  46.423 +    security.declareProtected( ManagePortal, 'writeDataFile' )
  46.424 +    def writeDataFile( self, filename, text, content_type, subdir=None ):
  46.425 +
  46.426 +        """ See IExportContext.
  46.427 +        """
  46.428 +        if subdir is not None:
  46.429 +            filename = '/'.join( ( subdir, filename ) )
  46.430 +
  46.431 +        sep = filename.rfind('/')
  46.432 +        if sep != -1:
  46.433 +            subdir = filename[:sep]
  46.434 +            filename = filename[sep+1:]
  46.435 +        folder = self._ensureSnapshotsFolder( subdir )
  46.436 +
  46.437 +        # TODO: switch on content_type
  46.438 +        ob = self._createObjectByType( filename, text, content_type )
  46.439 +        folder._setObject( str( filename ), ob ) # No Unicode IDs!
  46.440 +
  46.441 +    security.declareProtected( ManagePortal, 'getSnapshotURL' )
  46.442 +    def getSnapshotURL( self ):
  46.443 +
  46.444 +        """ See IExportContext.
  46.445 +        """
  46.446 +        return '%s/%s' % ( self._tool.absolute_url(), self._snapshot_id )
  46.447 +
  46.448 +    security.declareProtected( ManagePortal, 'getSnapshotFolder' )
  46.449 +    def getSnapshotFolder( self ):
  46.450 +
  46.451 +        """ See IExportContext.
  46.452 +        """
  46.453 +        return self._ensureSnapshotsFolder()
  46.454 +
  46.455 +    #
  46.456 +    #   Helper methods
  46.457 +    #
  46.458 +    security.declarePrivate( '_createObjectByType' )
  46.459 +    def _createObjectByType( self, name, body, content_type ):
  46.460 +
  46.461 +        if isinstance( body, unicode ):
  46.462 +            encoding = self.getEncoding()
  46.463 +            if encoding is None:
  46.464 +                body = body.encode()
  46.465 +            else:
  46.466 +                body = body.encode( encoding )
  46.467 +
  46.468 +        if name.endswith('.py'):
  46.469 +
  46.470 +            ob = PythonScript( name )
  46.471 +            ob.write( body )
  46.472 +
  46.473 +        elif name.endswith('.dtml'):
  46.474 +
  46.475 +            ob = DTMLDocument( '', __name__=name )
  46.476 +            ob.munge( body )
  46.477 +
  46.478 +        elif content_type in ('text/html', 'text/xml' ):
  46.479 +
  46.480 +            ob = ZopePageTemplate( name, body
  46.481 +                                 , content_type=content_type )
  46.482 +
  46.483 +        elif content_type[:6]=='image/':
  46.484 +
  46.485 +            ob=Image( name, '', body, content_type=content_type )
  46.486 +
  46.487 +        else:
  46.488 +            ob=File( name, '', body, content_type=content_type )
  46.489 +
  46.490 +        return ob
  46.491 +
  46.492 +    security.declarePrivate( '_ensureSnapshotsFolder' )
  46.493 +    def _ensureSnapshotsFolder( self, subdir=None ):
  46.494 +
  46.495 +        """ Ensure that the appropriate snapshot folder exists.
  46.496 +        """
  46.497 +        path = [ 'snapshots', self._snapshot_id ]
  46.498 +
  46.499 +        if subdir is not None:
  46.500 +            path.extend( subdir.split( '/' ) )
  46.501 +
  46.502 +        current = self._tool
  46.503 +
  46.504 +        for element in path:
  46.505 +
  46.506 +            if element not in current.objectIds():
  46.507 +                # No Unicode IDs!
  46.508 +                current._setObject( str( element ), Folder( element ) )
  46.509 +
  46.510 +            current = current._getOb( element )
  46.511 +
  46.512 +        return current
  46.513 +
  46.514 +InitializeClass( SnapshotExportContext )
  46.515 +
  46.516 +
  46.517 +class SnapshotImportContext( BaseContext ):
  46.518 +
  46.519 +    implements(IImportContext)
  46.520 +
  46.521 +    security = ClassSecurityInfo()
  46.522 +
  46.523 +    def __init__( self
  46.524 +                , tool
  46.525 +                , snapshot_id
  46.526 +                , should_purge=False
  46.527 +                , encoding=None
  46.528 +                ):
  46.529 +
  46.530 +        BaseContext.__init__( self, tool, encoding )
  46.531 +        self._snapshot_id = snapshot_id
  46.532 +        self._encoding = encoding
  46.533 +        self._should_purge = bool( should_purge )
  46.534 +
  46.535 +    security.declareProtected( ManagePortal, 'readDataFile' )
  46.536 +    def readDataFile( self, filename, subdir=None ):
  46.537 +
  46.538 +        """ See IImportContext.
  46.539 +        """
  46.540 +        if subdir is not None:
  46.541 +            filename = '/'.join( ( subdir, filename ) )
  46.542 +
  46.543 +        sep = filename.rfind('/')
  46.544 +        if sep != -1:
  46.545 +            subdir = filename[:sep]
  46.546 +            filename = filename[sep+1:]
  46.547 +        try:
  46.548 +            snapshot = self._getSnapshotFolder( subdir )
  46.549 +            object = snapshot._getOb( filename )
  46.550 +        except ( AttributeError, KeyError ):
  46.551 +            return None
  46.552 +
  46.553 +        try:
  46.554 +            return object.read()
  46.555 +        except AttributeError:
  46.556 +            return object.manage_FTPget()
  46.557 +
  46.558 +    security.declareProtected( ManagePortal, 'getLastModified' )
  46.559 +    def getLastModified( self, path ):
  46.560 +
  46.561 +        """ See IImportContext.
  46.562 +        """
  46.563 +        try:
  46.564 +            snapshot = self._getSnapshotFolder()
  46.565 +            object = snapshot.restrictedTraverse( path )
  46.566 +        except ( AttributeError, KeyError ):
  46.567 +            return None
  46.568 +        else:
  46.569 +            return object.bobobase_modification_time()
  46.570 +
  46.571 +    security.declareProtected( ManagePortal, 'isDirectory' )
  46.572 +    def isDirectory( self, path ):
  46.573 +
  46.574 +        """ See IImportContext.
  46.575 +        """
  46.576 +        try:
  46.577 +            snapshot = self._getSnapshotFolder()
  46.578 +            object = snapshot.restrictedTraverse( path )
  46.579 +        except ( AttributeError, KeyError ):
  46.580 +            return None
  46.581 +        else:
  46.582 +            folderish = getattr( object, 'isPrincipiaFolderish', False )
  46.583 +            return bool( folderish )
  46.584 +
  46.585 +    security.declareProtected( ManagePortal, 'listDirectory' )
  46.586 +    def listDirectory( self, path, skip=() ):
  46.587 +
  46.588 +        """ See IImportContext.
  46.589 +        """
  46.590 +        try:
  46.591 +            snapshot = self._getSnapshotFolder()
  46.592 +            subdir = snapshot.restrictedTraverse( path )
  46.593 +        except ( AttributeError, KeyError ):
  46.594 +            return None
  46.595 +        else:
  46.596 +            if not getattr( subdir, 'isPrincipiaFolderish', False ):
  46.597 +                return None
  46.598 +
  46.599 +            object_ids = subdir.objectIds()
  46.600 +            return [ x for x in object_ids if x not in skip ]
  46.601 +
  46.602 +    security.declareProtected( ManagePortal, 'shouldPurge' )
  46.603 +    def shouldPurge( self ):
  46.604 +
  46.605 +        """ See IImportContext.
  46.606 +        """
  46.607 +        return self._should_purge
  46.608 +
  46.609 +    #
  46.610 +    #   Helper methods
  46.611 +    #
  46.612 +    security.declarePrivate( '_getSnapshotFolder' )
  46.613 +    def _getSnapshotFolder( self, subdir=None ):
  46.614 +
  46.615 +        """ Return the appropriate snapshot (sub)folder.
  46.616 +        """
  46.617 +        path = [ 'snapshots', self._snapshot_id ]
  46.618 +
  46.619 +        if subdir is not None:
  46.620 +            path.extend( subdir.split( '/' ) )
  46.621 +
  46.622 +        return self._tool.restrictedTraverse( path )
  46.623 +
  46.624 +InitializeClass( SnapshotImportContext )
    47.1 new file mode 100644
    47.2 --- /dev/null
    47.3 +++ b/differ.py
    47.4 @@ -0,0 +1,229 @@
    47.5 +##############################################################################
    47.6 +#
    47.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    47.8 +#
    47.9 +# This software is subject to the provisions of the Zope Public License,
   47.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   47.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   47.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   47.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   47.14 +# FOR A PARTICULAR PURPOSE.
   47.15 +#
   47.16 +##############################################################################
   47.17 +""" Diff utilities for comparing configurations.
   47.18 +
   47.19 +$Id: differ.py 38575 2005-09-24 09:01:32Z yuppie $
   47.20 +"""
   47.21 +
   47.22 +from difflib import unified_diff
   47.23 +import re
   47.24 +
   47.25 +from Globals import InitializeClass
   47.26 +from AccessControl import ClassSecurityInfo
   47.27 +
   47.28 +BLANKS_REGEX = re.compile( r'^\s*$' )
   47.29 +
   47.30 +def unidiff( a
   47.31 +           , b
   47.32 +           , filename_a='original'
   47.33 +           , timestamp_a=None
   47.34 +           , filename_b='modified'
   47.35 +           , timestamp_b=None
   47.36 +           , ignore_blanks=False
   47.37 +           ):
   47.38 +    r"""Compare two sequences of lines; generate the resulting delta.
   47.39 +
   47.40 +    Each sequence must contain individual single-line strings
   47.41 +    ending with newlines. Such sequences can be obtained from the
   47.42 +    `readlines()` method of file-like objects.  The delta
   47.43 +    generated also consists of newline-terminated strings, ready
   47.44 +    to be printed as-is via the writeline() method of a file-like
   47.45 +    object.
   47.46 +
   47.47 +    Note that the last line of a file may *not* have a newline;
   47.48 +    this is reported in the same way that GNU diff reports this.
   47.49 +    *This method only supports UNIX line ending conventions.*
   47.50 +
   47.51 +        filename_a and filename_b are used to generate the header,
   47.52 +        allowing other tools to determine what 'files' were used
   47.53 +        to generate this output.
   47.54 +
   47.55 +        timestamp_a and timestamp_b, when supplied, are expected
   47.56 +        to be last-modified timestamps to be inserted in the
   47.57 +        header, as floating point values since the epoch.
   47.58 +
   47.59 +    Example:
   47.60 +
   47.61 +    >>> print ''.join(UniDiffer().compare(
   47.62 +    ...     'one\ntwo\nthree\n'.splitlines(1),
   47.63 +    ...     'ore\ntree\nemu\n'.splitlines(1))),
   47.64 +    +++ original
   47.65 +    --- modified
   47.66 +    @@ -1,3 +1,3 @@
   47.67 +    -one
   47.68 +    +ore
   47.69 +    -two
   47.70 +    -three
   47.71 +    +tree
   47.72 +    +emu
   47.73 +    """
   47.74 +    if isinstance( a, basestring ):
   47.75 +        a = a.splitlines()
   47.76 +
   47.77 +    if isinstance( b, basestring ):
   47.78 +        b = b.splitlines()
   47.79 +
   47.80 +    if ignore_blanks:
   47.81 +        a = [ x for x in a if not BLANKS_REGEX.match( x ) ]
   47.82 +        b = [ x for x in b if not BLANKS_REGEX.match( x ) ]
   47.83 +
   47.84 +    return unified_diff( a
   47.85 +                       , b
   47.86 +                       , filename_a
   47.87 +                       , filename_b
   47.88 +                       , timestamp_a
   47.89 +                       , timestamp_b
   47.90 +                       , lineterm=""
   47.91 +                       )
   47.92 +
   47.93 +class ConfigDiff:
   47.94 +
   47.95 +    security = ClassSecurityInfo()
   47.96 +
   47.97 +    def __init__( self
   47.98 +                , lhs
   47.99 +                , rhs
  47.100 +                , missing_as_empty=False
  47.101 +                , ignore_blanks=False
  47.102 +                , skip=('CVS','.svn')
  47.103 +                ):
  47.104 +        self._lhs = lhs
  47.105 +        self._rhs = rhs
  47.106 +        self._missing_as_empty = missing_as_empty
  47.107 +        self._ignore_blanks=ignore_blanks
  47.108 +        self._skip = skip
  47.109 +
  47.110 +    security.declarePrivate( 'compareDirectories' )
  47.111 +    def compareDirectories( self, subdir=None ):
  47.112 +
  47.113 +        lhs_files = self._lhs.listDirectory( subdir, self._skip )
  47.114 +        if lhs_files is None:
  47.115 +            lhs_files = []
  47.116 +
  47.117 +        rhs_files = self._rhs.listDirectory( subdir, self._skip )
  47.118 +        if rhs_files is None:
  47.119 +            rhs_files = []
  47.120 +
  47.121 +        added = [ f for f in rhs_files if f not in lhs_files ]
  47.122 +        removed = [ f for f in lhs_files if f not in rhs_files ]
  47.123 +        all_files = lhs_files + added
  47.124 +        all_files.sort()
  47.125 +
  47.126 +        result = []
  47.127 +
  47.128 +        for filename in all_files:
  47.129 +
  47.130 +            if subdir is None:
  47.131 +                pathname = filename
  47.132 +            else:
  47.133 +                pathname = '%s/%s' % ( subdir, filename )
  47.134 +
  47.135 +            if filename not in added:
  47.136 +                isDirectory = self._lhs.isDirectory( pathname )
  47.137 +            else:
  47.138 +                isDirectory = self._rhs.isDirectory( pathname )
  47.139 +
  47.140 +            if not self._missing_as_empty and filename in removed:
  47.141 +
  47.142 +                if isDirectory:
  47.143 +                    result.append( '** Directory %s removed\n' % pathname )
  47.144 +                    result.extend( self.compareDirectories( pathname ) )
  47.145 +                else:
  47.146 +                    result.append( '** File %s removed\n' % pathname )
  47.147 +
  47.148 +            elif not self._missing_as_empty and filename in added:
  47.149 +
  47.150 +                if isDirectory:
  47.151 +                    result.append( '** Directory %s added\n' % pathname )
  47.152 +                    result.extend( self.compareDirectories( pathname ) )
  47.153 +                else:
  47.154 +                    result.append( '** File %s added\n' % pathname )
  47.155 +
  47.156 +            elif isDirectory:
  47.157 +
  47.158 +                result.extend( self.compareDirectories( pathname ) )
  47.159 +
  47.160 +                if ( filename not in added + removed and
  47.161 +                    not self._rhs.isDirectory( pathname ) ):
  47.162 +
  47.163 +                    result.append( '** Directory %s replaced with a file of '
  47.164 +                                   'the same name\n' % pathname )
  47.165 +
  47.166 +                    if self._missing_as_empty:
  47.167 +                        result.extend( self.compareFiles( filename, subdir ) )
  47.168 +            else:
  47.169 +                if ( filename not in added + removed and
  47.170 +                     self._rhs.isDirectory( pathname ) ):
  47.171 +
  47.172 +                    result.append( '** File %s replaced with a directory of '
  47.173 +                                   'the same name\n' % pathname )
  47.174 +
  47.175 +                    if self._missing_as_empty:
  47.176 +                        result.extend( self.compareFiles( filename, subdir ) )
  47.177 +
  47.178 +                    result.extend( self.compareDirectories( pathname ) )
  47.179 +                else:
  47.180 +                    result.extend( self.compareFiles( filename, subdir ) )
  47.181 +
  47.182 +        return result
  47.183 +
  47.184 +    security.declarePrivate( 'compareFiles' )
  47.185 +    def compareFiles( self, filename, subdir=None ):
  47.186 +
  47.187 +        if subdir is None:
  47.188 +            path = filename
  47.189 +        else:
  47.190 +            path = '%s/%s' % ( subdir, filename )
  47.191 +
  47.192 +        lhs_file = self._lhs.readDataFile( filename, subdir )
  47.193 +        lhs_time = self._lhs.getLastModified( path )
  47.194 +
  47.195 +        if lhs_file is None:
  47.196 +            assert self._missing_as_empty
  47.197 +            lhs_file = ''
  47.198 +            lhs_time = 0
  47.199 +
  47.200 +        rhs_file = self._rhs.readDataFile( filename, subdir )
  47.201 +        rhs_time = self._rhs.getLastModified( path )
  47.202 +
  47.203 +        if rhs_file is None:
  47.204 +            assert self._missing_as_empty
  47.205 +            rhs_file = ''
  47.206 +            rhs_time = 0
  47.207 +
  47.208 +        if lhs_file == rhs_file:
  47.209 +            diff_lines = []
  47.210 +        else:
  47.211 +            diff_lines = unidiff( lhs_file
  47.212 +                                , rhs_file
  47.213 +                                , filename_a=path
  47.214 +                                , timestamp_a=lhs_time
  47.215 +                                , filename_b=path
  47.216 +                                , timestamp_b=rhs_time
  47.217 +                                , ignore_blanks=self._ignore_blanks
  47.218 +                                )
  47.219 +            diff_lines = list( diff_lines ) # generator
  47.220 +
  47.221 +        if len( diff_lines ) == 0: # No *real* difference found
  47.222 +            return []
  47.223 +
  47.224 +        diff_lines.insert( 0, 'Index: %s' % path )
  47.225 +        diff_lines.insert( 1, '=' * 67 )
  47.226 +
  47.227 +        return diff_lines
  47.228 +
  47.229 +    security.declarePrivate( 'compare' )
  47.230 +    def compare( self ):
  47.231 +        return '\n'.join( self.compareDirectories() )
  47.232 +
  47.233 +InitializeClass( ConfigDiff )
    48.1 new file mode 100644
    48.2 --- /dev/null
    48.3 +++ b/exceptions.py
    48.4 @@ -0,0 +1,22 @@
    48.5 +##############################################################################
    48.6 +#
    48.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    48.8 +#
    48.9 +# This software is subject to the provisions of the Zope Public License,
   48.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   48.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   48.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   48.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   48.14 +# FOR A PARTICULAR PURPOSE.
   48.15 +#
   48.16 +##############################################################################
   48.17 +""" GenericSetup product exceptions.
   48.18 +
   48.19 +$Id: exceptions.py 38575 2005-09-24 09:01:32Z yuppie $
   48.20 +"""
   48.21 +
   48.22 +from AccessControl import ModuleSecurityInfo
   48.23 +security = ModuleSecurityInfo('Products.GenericSetup.exceptions')
   48.24 +
   48.25 +security.declarePublic('BadRequest')
   48.26 +from zExceptions import BadRequest
    49.1 new file mode 100644
    49.2 --- /dev/null
    49.3 +++ b/interfaces.py
    49.4 @@ -0,0 +1,733 @@
    49.5 +##############################################################################
    49.6 +#
    49.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    49.8 +#
    49.9 +# This software is subject to the provisions of the Zope Public License,
   49.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   49.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   49.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   49.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   49.14 +# FOR A PARTICULAR PURPOSE.
   49.15 +#
   49.16 +##############################################################################
   49.17 +""" GenericSetup product interfaces
   49.18 +
   49.19 +$Id: interfaces.py 40878 2005-12-18 21:57:56Z yuppie $
   49.20 +"""
   49.21 +
   49.22 +from zope.interface import Interface
   49.23 +from zope.schema import Text
   49.24 +from zope.schema import TextLine
   49.25 +
   49.26 +BASE, EXTENSION = range(1, 3)
   49.27 +
   49.28 +
   49.29 +class IPseudoInterface( Interface ):
   49.30 +
   49.31 +    """ API documentation;  not testable / enforceable.
   49.32 +    """
   49.33 +
   49.34 +
   49.35 +class ISetupEnviron(Interface):
   49.36 +
   49.37 +    """Context for im- and export adapters.
   49.38 +    """
   49.39 +
   49.40 +    def getLogger(name):
   49.41 +        """Get a logger with the specified name, creating it if necessary.
   49.42 +        """
   49.43 +
   49.44 +    def shouldPurge():
   49.45 +        """When installing, should the existing setup be purged?
   49.46 +        """
   49.47 +
   49.48 +
   49.49 +class ISetupContext(ISetupEnviron):
   49.50 +
   49.51 +    """ Context used for export / import plugins.
   49.52 +    """
   49.53 +    def getSite():
   49.54 +
   49.55 +        """ Return the site object being configured / dumped.
   49.56 +        """
   49.57 +
   49.58 +    def getSetupTool():
   49.59 +
   49.60 +        """ Return the site object being configured / dumped.
   49.61 +        """
   49.62 +
   49.63 +    def getEncoding():
   49.64 +
   49.65 +        """ Get the encoding used for configuration data within the site.
   49.66 +
   49.67 +        o Return None if the data should not be encoded.
   49.68 +        """
   49.69 +
   49.70 +    def listNotes():
   49.71 +        """ Return notes recorded by this context.
   49.72 +        
   49.73 +        o Result a sequence of (component, message) tuples
   49.74 +        """
   49.75 +
   49.76 +    def clearNotes():
   49.77 +        """ Clear all notes recorded by this context.
   49.78 +        """
   49.79 +
   49.80 +class IImportContext( ISetupContext ):
   49.81 +
   49.82 +    def readDataFile( filename, subdir=None ):
   49.83 +
   49.84 +        """ Search the current configuration for the requested file.
   49.85 +
   49.86 +        o 'filename' is the name (without path elements) of the file.
   49.87 +
   49.88 +        o 'subdir' is an optional subdirectory;  if not supplied, search
   49.89 +          only the "root" directory.
   49.90 +
   49.91 +        o Return the file contents as a string, or None if the
   49.92 +          file cannot be found.
   49.93 +        """
   49.94 +
   49.95 +    def getLastModified( path ):
   49.96 +
   49.97 +        """ Return the modification timestamp of the item at 'path'.
   49.98 +
   49.99 +        o Result will be a DateTime instance.
  49.100 +
  49.101 +        o Search profiles in the configuration in order.
  49.102 +
  49.103 +        o If the context is filesystem based, return the 'stat' timestamp
  49.104 +          of the file / directory to which 'path' points.
  49.105 +
  49.106 +        o If the context is ZODB-based, return the Zope modification time
  49.107 +          of the object to which 'path' points.
  49.108 +
  49.109 +        o Return None if 'path' does not point to any object.
  49.110 +        """
  49.111 +
  49.112 +    def isDirectory( path ):
  49.113 +
  49.114 +        """ Test whether path points to a directory / folder.
  49.115 +
  49.116 +        o If the context is filesystem based, check that 'path' points to
  49.117 +          a subdirectory within the "root" directory.
  49.118 +
  49.119 +        o If the context is ZODB-based, check that 'path' points to a
  49.120 +          "container" under the context's tool.
  49.121 +
  49.122 +        o Return None if 'path' does not resolve;  otherwise, return a
  49.123 +          bool.
  49.124 +        """
  49.125 +
  49.126 +    def listDirectory( path, skip=('CVS', '.svn') ):
  49.127 +
  49.128 +        """ List IDs of the contents of a  directory / folder.
  49.129 +
  49.130 +        o Omit names in 'skip'.
  49.131 +
  49.132 +        o If 'path' does not point to a directory / folder, return None.
  49.133 +        """
  49.134 +
  49.135 +
  49.136 +class IImportPlugin( IPseudoInterface ):
  49.137 +
  49.138 +    """ Signature for callables used to import portions of site configuration.
  49.139 +    """
  49.140 +    def __call__( context ):
  49.141 +
  49.142 +        """ Perform the setup step.
  49.143 +
  49.144 +        o Return a message describing the work done.
  49.145 +
  49.146 +        o 'context' must implement IImportContext.
  49.147 +        """
  49.148 +
  49.149 +class IExportContext( ISetupContext ):
  49.150 +
  49.151 +    def writeDataFile( filename, text, content_type, subdir=None ):
  49.152 +
  49.153 +        """ Write data into the specified location.
  49.154 +
  49.155 +        o 'filename' is the unqualified name of the file.
  49.156 +
  49.157 +        o 'text' is the content of the file.
  49.158 +
  49.159 +        o 'content_type' is the MIMEtype of the file.
  49.160 +
  49.161 +        o 'subdir', if passed, is a path to a subdirectory / folder in
  49.162 +          which to write the file;  if not passed, write the file to the
  49.163 +          "root" of the target.
  49.164 +        """
  49.165 +
  49.166 +class IExportPlugin( IPseudoInterface ):
  49.167 +
  49.168 +    """ Signature for callables used to export portions of site configuration.
  49.169 +    """
  49.170 +    def __call__( context ):
  49.171 +
  49.172 +        """ Write export data for the site wrapped by context.
  49.173 +
  49.174 +        o Return a message describing the work done.
  49.175 +
  49.176 +        o 'context' must implement IExportContext.  The plugin will use
  49.177 +          its 'writeDataFile' method for each file to be exported.
  49.178 +        """
  49.179 +
  49.180 +class IStepRegistry( Interface ):
  49.181 +
  49.182 +    """ Base interface for step registries.
  49.183 +    """
  49.184 +    def listSteps():
  49.185 +
  49.186 +        """ Return a sequence of IDs of registered steps.
  49.187 +
  49.188 +        o Order is not significant.
  49.189 +        """
  49.190 +
  49.191 +    def listStepMetadata():
  49.192 +
  49.193 +        """ Return a sequence of mappings describing registered steps.
  49.194 +
  49.195 +        o Mappings will be ordered alphabetically.
  49.196 +        """
  49.197 +
  49.198 +    def getStepMetadata( key, default=None ):
  49.199 +
  49.200 +        """ Return a mapping of metadata for the step identified by 'key'.
  49.201 +
  49.202 +        o Return 'default' if no such step is registered.
  49.203 +
  49.204 +        o The 'handler' metadata is available via 'getStep'.
  49.205 +        """
  49.206 +
  49.207 +    def generateXML():
  49.208 +
  49.209 +        """ Return a round-trippable XML representation of the registry.
  49.210 +
  49.211 +        o 'handler' values are serialized using their dotted names.
  49.212 +        """
  49.213 +
  49.214 +    def parseXML( text ):
  49.215 +
  49.216 +        """ Parse 'text'.
  49.217 +        """
  49.218 +
  49.219 +class IImportStepRegistry( IStepRegistry ):
  49.220 +
  49.221 +    """ API for import step registry.
  49.222 +    """
  49.223 +    def sortSteps():
  49.224 +
  49.225 +        """ Return a sequence of registered step IDs
  49.226 +
  49.227 +        o Sequence is sorted topologically by dependency, with the dependent
  49.228 +          steps *after* the steps they depend on.
  49.229 +        """
  49.230 +
  49.231 +    def checkComplete():
  49.232 +
  49.233 +        """ Return a sequence of ( node, edge ) tuples for unsatisifed deps.
  49.234 +        """
  49.235 +
  49.236 +    def getStep( key, default=None ):
  49.237 +
  49.238 +        """ Return the IImportPlugin registered for 'key'.
  49.239 +
  49.240 +        o Return 'default' if no such step is registered.
  49.241 +        """
  49.242 +
  49.243 +    def registerStep( id
  49.244 +                    , version
  49.245 +                    , handler
  49.246 +                    , dependencies=()
  49.247 +                    , title=None
  49.248 +                    , description=None
  49.249 +                    ):
  49.250 +        """ Register a setup step.
  49.251 +
  49.252 +        o 'id' is a unique name for this step,
  49.253 +
  49.254 +        o 'version' is a string for comparing versions, it is preferred to
  49.255 +          be a yyyy/mm/dd-ii formatted string (date plus two-digit
  49.256 +          ordinal).  when comparing two version strings, the version with
  49.257 +          the lower sort order is considered the older version.
  49.258 +
  49.259 +          - Newer versions of a step supplant older ones.
  49.260 +
  49.261 +          - Attempting to register an older one after a newer one results
  49.262 +            in a KeyError.
  49.263 +
  49.264 +        o 'handler' should implement IImportPlugin.
  49.265 +
  49.266 +        o 'dependencies' is a tuple of step ids which have to run before
  49.267 +          this step in order to be able to run at all. Registration of
  49.268 +          steps that have unmet dependencies are deferred until the
  49.269 +          dependencies have been registered.
  49.270 +
  49.271 +        o 'title' is a one-line UI description for this step.
  49.272 +          If None, the first line of the documentation string of the handler
  49.273 +          is used, or the id if no docstring can be found.
  49.274 +
  49.275 +        o 'description' is a one-line UI description for this step.
  49.276 +          If None, the remaining line of the documentation string of
  49.277 +          the handler is used, or default to ''.
  49.278 +        """
  49.279 +
  49.280 +class IExportStepRegistry( IStepRegistry ):
  49.281 +
  49.282 +    """ API for export step registry.
  49.283 +    """
  49.284 +    def getStep( key, default=None ):
  49.285 +
  49.286 +        """ Return the IExportPlugin registered for 'key'.
  49.287 +
  49.288 +        o Return 'default' if no such step is registered.
  49.289 +        """
  49.290 +
  49.291 +    def registerStep( id, handler, title=None, description=None ):
  49.292 +
  49.293 +        """ Register an export step.
  49.294 +
  49.295 +        o 'id' is the unique identifier for this step
  49.296 +
  49.297 +        o 'handler' should implement IExportPlugin.
  49.298 +
  49.299 +        o 'title' is a one-line UI description for this step.
  49.300 +          If None, the first line of the documentation string of the step
  49.301 +          is used, or the id if no docstring can be found.
  49.302 +
  49.303 +        o 'description' is a one-line UI description for this step.
  49.304 +          If None, the remaining line of the documentation string of
  49.305 +          the step is used, or default to ''.
  49.306 +        """
  49.307 +
  49.308 +class IToolsetRegistry( Interface ):
  49.309 +
  49.310 +    """ API for toolset registry.
  49.311 +    """
  49.312 +    def listForbiddenTools():
  49.313 +
  49.314 +        """ Return a list of IDs of tools which must be removed, if present.
  49.315 +        """
  49.316 +
  49.317 +    def addForbiddenTool(tool_id ):
  49.318 +
  49.319 +        """ Add 'tool_id' to the list of forbidden tools.
  49.320 +
  49.321 +        o Raise KeyError if 'tool_id' is already in the list.
  49.322 +
  49.323 +        o Raise ValueError if 'tool_id' is in the "required" list.
  49.324 +        """
  49.325 +
  49.326 +    def listRequiredTools():
  49.327 +
  49.328 +        """ Return a list of IDs of tools which must be present.
  49.329 +        """
  49.330 +
  49.331 +    def getRequiredToolInfo( tool_id ):
  49.332 +
  49.333 +        """ Return a mapping describing a partiuclar required tool.
  49.334 +
  49.335 +        o Keys include:
  49.336 +
  49.337 +          'id' -- the ID of the tool
  49.338 +
  49.339 +          'class' -- a dotted path to its class
  49.340 +
  49.341 +        o Raise KeyError if 'tool_id' id not a known tool.
  49.342 +        """
  49.343 +
  49.344 +    def listRequiredToolInfo():
  49.345 +
  49.346 +        """ Return a list of IDs of tools which must be present.
  49.347 +        """
  49.348 +
  49.349 +    def addRequiredTool( tool_id, dotted_name ):
  49.350 +
  49.351 +        """ Add a tool to our "required" list.
  49.352 +
  49.353 +        o 'tool_id' is the tool's ID.
  49.354 +
  49.355 +        o 'dotted_name' is a dotted (importable) name of the tool's class.
  49.356 +
  49.357 +        o Raise KeyError if we have already registered a class for 'tool_id'.
  49.358 +
  49.359 +        o Raise ValueError if 'tool_id' is in the "forbidden" list.
  49.360 +        """
  49.361 +
  49.362 +class IProfileRegistry( Interface ):
  49.363 +
  49.364 +    """ API for profile registry.
  49.365 +    """
  49.366 +    def getProfileInfo( profile_id, for_=None ):
  49.367 +
  49.368 +        """ Return a mapping describing a registered filesystem profile.
  49.369 +
  49.370 +        o Keys include:
  49.371 +
  49.372 +          'id' -- the ID of the profile
  49.373 +
  49.374 +          'title' -- its title
  49.375 +
  49.376 +          'description' -- a textual description of the profile
  49.377 +
  49.378 +          'path' -- a path to the profile on the filesystem.
  49.379 +
  49.380 +          'product' -- the name of the product to which 'path' is
  49.381 +             relative (None for absolute paths).
  49.382 +
  49.383 +          'type' -- either BASE or EXTENSION
  49.384 +        
  49.385 +        o 'for_', if passed, should be the interface specifying the "site
  49.386 +            type" for which the profile is relevant, e.g.
  49.387 +            Products.CMFCore.interfaces.ISiteRoot or
  49.388 +            Products.PluggableAuthService.interfaces.IPluggableAuthService.
  49.389 +            If 'None', list all profiles.
  49.390 +        """
  49.391 +
  49.392 +    def listProfiles( for_=None ):
  49.393 +
  49.394 +        """ Return a list of IDs for registered profiles.
  49.395 +        
  49.396 +        o 'for_', if passed, should be the interface specifying the "site
  49.397 +            type" for which the profile is relevant, e.g.
  49.398 +            Products.CMFCore.interfaces.ISiteRoot or
  49.399 +            Products.PluggableAuthService.interfaces.IPluggableAuthService.
  49.400 +            If 'None', list all profiles.
  49.401 +        """
  49.402 +
  49.403 +    def listProfileInfo( for_=None ):
  49.404 +
  49.405 +        """ Return a list of mappings describing registered profiles.
  49.406 +
  49.407 +        o See 'getProfileInfo' for a description of the mappings' keys.
  49.408 +        
  49.409 +        o 'for_', if passed, should be the interface specifying the "site
  49.410 +            type" for which the profile is relevant, e.g.
  49.411 +            Products.CMFCore.interfaces.ISiteRoot or
  49.412 +            Products.PluggableAuthService.interfaces.IPluggableAuthService.
  49.413 +            If 'None', list all profiles.
  49.414 +        """
  49.415 +
  49.416 +    def registerProfile( name
  49.417 +                       , title
  49.418 +                       , description
  49.419 +                       , path
  49.420 +                       , product=None
  49.421 +                       , profile_type=BASE
  49.422 +                       , for_=None
  49.423 +                       ):
  49.424 +        """ Add a new profile to the registry.
  49.425 +
  49.426 +        o If an existing profile is already registered for 'product:name',
  49.427 +          raise KeyError.
  49.428 +
  49.429 +        o If 'product' is passed, then 'path' should be interpreted as
  49.430 +          relative to the corresponding product directory.
  49.431 +        
  49.432 +        o 'for_', if passed, should be the interface specifying the "site
  49.433 +          type" for which the profile is relevant, e.g.
  49.434 +          Products.CMFCore.interfaces.ISiteRoot or
  49.435 +          Products.PluggableAuthService.interfaces.IPluggableAuthService.
  49.436 +          If 'None', the profile might be used in any site.
  49.437 +        """
  49.438 +
  49.439 +class ISetupTool( Interface ):
  49.440 +
  49.441 +    """ API for SetupTool.
  49.442 +    """
  49.443 +
  49.444 +    def getEncoding():
  49.445 +
  49.446 +        """ Get the encoding used for configuration data within the site.
  49.447 +
  49.448 +        o Return None if the data should not be encoded.
  49.449 +        """
  49.450 +
  49.451 +    def getImportContextID():
  49.452 +
  49.453 +        """ Get the ID of the active import context.
  49.454 +        """
  49.455 +
  49.456 +    def setImportContext( context_id ):
  49.457 +
  49.458 +        """ Set the ID of the active import context and update the registries.
  49.459 +        """
  49.460 +
  49.461 +    def getImportStepRegistry():
  49.462 +
  49.463 +        """ Return the IImportStepRegistry for the tool.
  49.464 +        """
  49.465 +
  49.466 +    def getExportStepRegistry():
  49.467 +
  49.468 +        """ Return the IExportStepRegistry for the tool.
  49.469 +        """
  49.470 +
  49.471 +    def getToolsetRegistry():
  49.472 +
  49.473 +        """ Return the IToolsetRegistry for the tool.
  49.474 +        """
  49.475 +
  49.476 +    def runImportStep( step_id, run_dependencies=True, purge_old=None ):
  49.477 +
  49.478 +        """ Execute a given setup step
  49.479 +
  49.480 +        o 'step_id' is the ID of the step to run.
  49.481 +
  49.482 +        o If 'purge_old' is True, then run the step after purging any
  49.483 +          "old" setup first (this is the responsibility of the step,
  49.484 +          which must check the context we supply).
  49.485 +
  49.486 +        o If 'run_dependencies' is True, then run any out-of-date
  49.487 +          dependency steps first.
  49.488 +
  49.489 +        o Return a mapping, with keys:
  49.490 +
  49.491 +          'steps' -- a sequence of IDs of the steps run.
  49.492 +
  49.493 +          'messages' -- a dictionary holding messages returned from each
  49.494 +            step
  49.495 +        """
  49.496 +
  49.497 +    def runAllImportSteps( purge_old=None ):
  49.498 +
  49.499 +        """ Run all setup steps in dependency order.
  49.500 +
  49.501 +        o If 'purge_old' is True, then run each step after purging any
  49.502 +          "old" setup first (this is the responsibility of the step,
  49.503 +          which must check the context we supply).
  49.504 +
  49.505 +        o Return a mapping, with keys:
  49.506 +
  49.507 +          'steps' -- a sequence of IDs of the steps run.
  49.508 +
  49.509 +          'messages' -- a dictionary holding messages returned from each
  49.510 +            step
  49.511 +        """
  49.512 +
  49.513 +    def runExportStep( step_id ):
  49.514 +
  49.515 +        """ Generate a tarball containing artifacts from one export step.
  49.516 +
  49.517 +        o 'step_id' identifies the export step.
  49.518 +
  49.519 +        o Return a mapping, with keys:
  49.520 +
  49.521 +          'steps' -- a sequence of IDs of the steps run.
  49.522 +
  49.523 +          'messages' -- a dictionary holding messages returned from each
  49.524 +            step
  49.525 +
  49.526 +          'tarball' -- the stringified tar-gz data.
  49.527 +        """
  49.528 +
  49.529 +    def runAllExportSteps():
  49.530 +
  49.531 +        """ Generate a tarball containing artifacts from all export steps.
  49.532 +
  49.533 +        o Return a mapping, with keys:
  49.534 +
  49.535 +          'steps' -- a sequence of IDs of the steps run.
  49.536 +
  49.537 +          'messages' -- a dictionary holding messages returned from each
  49.538 +            step
  49.539 +
  49.540 +          'tarball' -- the stringified tar-gz data.
  49.541 +        """
  49.542 +
  49.543 +    def createSnapshot( snapshot_id ):
  49.544 +
  49.545 +        """ Create a snapshot folder using all steps.
  49.546 +
  49.547 +        o 'snapshot_id' is the ID of the new folder.
  49.548 +        """
  49.549 +
  49.550 +    def compareConfigurations( lhs_context
  49.551 +                             , rhs_context
  49.552 +                             , missing_as_empty=False
  49.553 +                             , ignore_whitespace=False
  49.554 +                             ):
  49.555 +        """ Compare two configurations.
  49.556 +
  49.557 +        o 'lhs_context' and 'rhs_context' must implement IImportContext.
  49.558 +
  49.559 +        o If 'missing_as_empty', then compare files not present as though
  49.560 +          they were zero-length;  otherwise, omit such files.
  49.561 +
  49.562 +        o If 'ignore_whitespace', then suppress diffs due only to whitespace
  49.563 +          (c.f:  'diff -wbB')
  49.564 +        """
  49.565 +
  49.566 +
  49.567 +class IWriteLogger(Interface):
  49.568 +
  49.569 +    """Write methods used by the python logging Logger.
  49.570 +    """
  49.571 +
  49.572 +    def debug(msg, *args, **kwargs):
  49.573 +        """Log 'msg % args' with severity 'DEBUG'.
  49.574 +        """
  49.575 +
  49.576 +    def info(msg, *args, **kwargs):
  49.577 +        """Log 'msg % args' with severity 'INFO'.
  49.578 +        """
  49.579 +
  49.580 +    def warning(msg, *args, **kwargs):
  49.581 +        """Log 'msg % args' with severity 'WARNING'.
  49.582 +        """
  49.583 +
  49.584 +    def error(msg, *args, **kwargs):
  49.585 +        """Log 'msg % args' with severity 'ERROR'.
  49.586 +        """
  49.587 +
  49.588 +    def exception(msg, *args):
  49.589 +        """Convenience method for logging an ERROR with exception information.
  49.590 +        """
  49.591 +
  49.592 +    def critical(msg, *args, **kwargs):
  49.593 +        """Log 'msg % args' with severity 'CRITICAL'.
  49.594 +        """
  49.595 +
  49.596 +    def log(level, msg, *args, **kwargs):
  49.597 +        """Log 'msg % args' with the integer severity 'level'.
  49.598 +        """
  49.599 +
  49.600 +
  49.601 +class INode(Interface):
  49.602 +
  49.603 +    """Node im- and exporter.
  49.604 +    """
  49.605 +
  49.606 +    node = Text(description=u'Im- and export the object as a DOM node.')
  49.607 +
  49.608 +
  49.609 +class IBody(INode):
  49.610 +
  49.611 +    """Body im- and exporter.
  49.612 +    """
  49.613 +
  49.614 +    body = Text(description=u'Im- and export the object as a file body.')
  49.615 +
  49.616 +    mime_type = TextLine(description=u'MIME type of the file body.')
  49.617 +
  49.618 +    name = TextLine(description=u'Enforce this name for the file.')
  49.619 +
  49.620 +    suffix = TextLine(description=u'Suffix for the file.')
  49.621 +
  49.622 +
  49.623 +class IFilesystemExporter(Interface):
  49.624 +    """ Plugin interface for site structure export.
  49.625 +    """
  49.626 +    def export(export_context, subdir, root=False):
  49.627 +        """ Export our 'context' using the API of 'export_context'.
  49.628 +
  49.629 +        o 'export_context' must implement
  49.630 +          Products.GenericSupport.interfaces.IExportContext.
  49.631 +
  49.632 +        o 'subdir', if passed, is the relative subdirectory containing our
  49.633 +          context within the site.
  49.634 +
  49.635 +        o 'root', if true, indicates that the current context is the
  49.636 +          "root" of an import (this may be used to adjust paths when
  49.637 +          interacting with the context).
  49.638 +        """
  49.639 +
  49.640 +    def listExportableItems():
  49.641 +        """ Return a sequence of the child items to be exported.
  49.642 +
  49.643 +        o Each item in the returned sequence will be a tuple,
  49.644 +          (id, object, adapter) where adapter must implement
  49.645 +          IFilesystemExporter.
  49.646 +        """
  49.647 +
  49.648 +class IFilesystemImporter(Interface):
  49.649 +    """ Plugin interface for site structure export.
  49.650 +    """
  49.651 +    def import_(import_context, subdir, root=False):
  49.652 +        """ Import our 'context' using the API of 'import_context'.
  49.653 +
  49.654 +        o 'import_context' must implement
  49.655 +          Products.GenericSupport.interfaces.IImportContext.
  49.656 +
  49.657 +        o 'subdir', if passed, is the relative subdirectory containing our
  49.658 +          context within the site.
  49.659 +
  49.660 +        o 'root', if true, indicates that the current context is the
  49.661 +          "root" of an import (this may be used to adjust paths when
  49.662 +          interacting with the context).
  49.663 +        """
  49.664 +
  49.665 +class IContentFactory(Interface):
  49.666 +    """ Adapter interface for factories specific to a container.
  49.667 +    """
  49.668 +    def __call__(id):
  49.669 +        """ Return a new instance, seated in the context under 'id'.
  49.670 +        """
  49.671 +
  49.672 +class IContentFactoryName(Interface):
  49.673 +    """ Adapter interface for finding the name of the ICF for an object.
  49.674 +    """
  49.675 +    def __call__():
  49.676 +        """ Return a string, suitable for looking up an IContentFactory.
  49.677 +        
  49.678 +        o The string should allow finding a factory for our context's
  49.679 +          container which would create an "empty" instance of the same
  49.680 +          type as our context.
  49.681 +        """
  49.682 +
  49.683 +class ICSVAware(Interface):
  49.684 +    """ Interface for objects which dump / load 'text/comma-separated-values'.
  49.685 +    """
  49.686 +    def getId():
  49.687 +        """ Return the Zope id of the object.
  49.688 +        """
  49.689 +
  49.690 +    def as_csv():
  49.691 +        """ Return a string representing the object as CSV.
  49.692 +        """
  49.693 +
  49.694 +    def put_csv(fd):
  49.695 +        """ Parse CSV and update the object.
  49.696 +
  49.697 +        o 'fd' must be a file-like object whose 'read' method returns
  49.698 +          CSV text parseable by the 'csv.reader'.
  49.699 +        """
  49.700 +
  49.701 +class IINIAware(Interface):
  49.702 +    """ Interface for objects which dump / load INI-format files..
  49.703 +    """
  49.704 +    def getId():
  49.705 +        """ Return the Zope id of the object.
  49.706 +        """
  49.707 +
  49.708 +    def as_ini():
  49.709 +        """ Return a string representing the object as INI.
  49.710 +        """
  49.711 +
  49.712 +    def put_ini(stream_or_text):
  49.713 +        """ Parse INI-formatted text and update the object.
  49.714 +
  49.715 +        o 'stream_or_text' must be either a string, or else a stream
  49.716 +          directly parseable by ConfigParser.
  49.717 +        """
  49.718 +
  49.719 +class IDAVAware(Interface):
  49.720 +    """ Interface for objects which handle their own FTP / DAV operations.
  49.721 +    """
  49.722 +    def getId():
  49.723 +        """ Return the Zope id of the object.
  49.724 +        """
  49.725 +
  49.726 +    def manage_FTPget():
  49.727 +        """ Return a string representing the object as a file.
  49.728 +        """
  49.729 +
  49.730 +    def PUT(REQUEST, RESPONSE):
  49.731 +        """ Parse file content and update the object.
  49.732 +
  49.733 +        o 'REQUEST' will have a 'get' method, which will have the 
  49.734 +          content object in its "BODY" key.  It will also have 'get_header'
  49.735 +          method, whose headers (e.g., "Content-Type") may affect the
  49.736 +          processing of the body.
  49.737 +        """
    50.1 new file mode 100644
    50.2 --- /dev/null
    50.3 +++ b/permissions.py
    50.4 @@ -0,0 +1,18 @@
    50.5 +##############################################################################
    50.6 +#
    50.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    50.8 +#
    50.9 +# This software is subject to the provisions of the Zope Public License,
   50.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   50.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   50.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   50.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   50.14 +# FOR A PARTICULAR PURPOSE.
   50.15 +#
   50.16 +##############################################################################
   50.17 +""" GenericSetup product permissions.
   50.18 +
   50.19 +$Id: permissions.py 38575 2005-09-24 09:01:32Z yuppie $
   50.20 +"""
   50.21 +
   50.22 +ManagePortal = 'Manage Site'
    51.1 new file mode 100644
    51.2 --- /dev/null
    51.3 +++ b/registry.py
    51.4 @@ -0,0 +1,767 @@
    51.5 +##############################################################################
    51.6 +#
    51.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    51.8 +#
    51.9 +# This software is subject to the provisions of the Zope Public License,
   51.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   51.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   51.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   51.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   51.14 +# FOR A PARTICULAR PURPOSE.
   51.15 +#
   51.16 +##############################################################################
   51.17 +""" Classes:  ImportStepRegistry, ExportStepRegistry
   51.18 +
   51.19 +$Id: registry.py 40339 2005-11-23 11:43:31Z yuppie $
   51.20 +"""
   51.21 +
   51.22 +from xml.sax import parseString
   51.23 +
   51.24 +from AccessControl import ClassSecurityInfo
   51.25 +from Acquisition import Implicit
   51.26 +from Globals import InitializeClass
   51.27 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   51.28 +from zope.interface import implements
   51.29 +
   51.30 +from interfaces import BASE
   51.31 +from interfaces import IImportStepRegistry
   51.32 +from interfaces import IExportStepRegistry
   51.33 +from interfaces import IToolsetRegistry
   51.34 +from interfaces import IProfileRegistry
   51.35 +from permissions import ManagePortal
   51.36 +from utils import HandlerBase
   51.37 +from utils import _xmldir
   51.38 +from utils import _getDottedName
   51.39 +from utils import _resolveDottedName
   51.40 +from utils import _extractDocstring
   51.41 +
   51.42 +
   51.43 +class ImportStepRegistry( Implicit ):
   51.44 +
   51.45 +    """ Manage knowledge about steps to create / configure site.
   51.46 +
   51.47 +    o Steps are composed together to define a site profile.
   51.48 +    """
   51.49 +    implements(IImportStepRegistry)
   51.50 +
   51.51 +    security = ClassSecurityInfo()
   51.52 +
   51.53 +    def __init__( self ):
   51.54 +
   51.55 +        self.clear()
   51.56 +
   51.57 +    security.declareProtected( ManagePortal, 'listSteps' )
   51.58 +    def listSteps( self ):
   51.59 +
   51.60 +        """ Return a sequence of IDs of registered steps.
   51.61 +
   51.62 +        o Order is not significant.
   51.63 +        """
   51.64 +        return self._registered.keys()
   51.65 +
   51.66 +    security.declareProtected( ManagePortal, 'sortSteps' )
   51.67 +    def sortSteps( self ):
   51.68 +
   51.69 +        """ Return a sequence of registered step IDs
   51.70 +
   51.71 +        o Sequence is sorted topologically by dependency, with the dependent
   51.72 +          steps *after* the steps they depend on.
   51.73 +        """
   51.74 +        return self._computeTopologicalSort()
   51.75 +
   51.76 +    security.declareProtected( ManagePortal, 'checkComplete' )
   51.77 +    def checkComplete( self ):
   51.78 +
   51.79 +        """ Return a sequence of ( node, edge ) tuples for unsatisifed deps.
   51.80 +        """
   51.81 +        result = []
   51.82 +        seen = {}
   51.83 +
   51.84 +        graph = self._computeTopologicalSort()
   51.85 +
   51.86 +        for node in graph:
   51.87 +
   51.88 +            dependencies = self.getStepMetadata( node )[ 'dependencies' ]
   51.89 +
   51.90 +            for dependency in dependencies:
   51.91 +
   51.92 +                if seen.get( dependency ) is None:
   51.93 +                    result.append( ( node, dependency ) )
   51.94 +
   51.95 +            seen[ node ] = 1
   51.96 +
   51.97 +        return result
   51.98 +
   51.99 +    security.declareProtected( ManagePortal, 'getStepMetadata' )
  51.100 +    def getStepMetadata( self, key, default=None ):
  51.101 +
  51.102 +        """ Return a mapping of metadata for the step identified by 'key'.
  51.103 +
  51.104 +        o Return 'default' if no such step is registered.
  51.105 +
  51.106 +        o The 'handler' metadata is available via 'getStep'.
  51.107 +        """
  51.108 +        result = {}
  51.109 +
  51.110 +        info = self._registered.get( key )
  51.111 +
  51.112 +        if info is None:
  51.113 +            return default
  51.114 +
  51.115 +        return info.copy()
  51.116 +
  51.117 +    security.declareProtected( ManagePortal, 'listStepMetadata' )
  51.118 +    def listStepMetadata( self ):
  51.119 +
  51.120 +        """ Return a sequence of mappings describing registered steps.
  51.121 +
  51.122 +        o Mappings will be ordered alphabetically.
  51.123 +        """
  51.124 +        step_ids = self.listSteps()
  51.125 +        step_ids.sort()
  51.126 +        return [ self.getStepMetadata( x ) for x in step_ids ]
  51.127 +
  51.128 +    security.declareProtected( ManagePortal, 'generateXML' )
  51.129 +    def generateXML( self ):
  51.130 +
  51.131 +        """ Return a round-trippable XML representation of the registry.
  51.132 +
  51.133 +        o 'handler' values are serialized using their dotted names.
  51.134 +        """
  51.135 +        return self._exportTemplate()
  51.136 +
  51.137 +    security.declarePrivate( 'getStep' )
  51.138 +    def getStep( self, key, default=None ):
  51.139 +
  51.140 +        """ Return the IImportPlugin registered for 'key'.
  51.141 +
  51.142 +        o Return 'default' if no such step is registered.
  51.143 +        """
  51.144 +        marker = object()
  51.145 +        info = self._registered.get( key, marker )
  51.146 +
  51.147 +        if info is marker:
  51.148 +            return default
  51.149 +
  51.150 +        return _resolveDottedName( info[ 'handler' ] )
  51.151 +
  51.152 +    security.declarePrivate( 'registerStep' )
  51.153 +    def registerStep( self
  51.154 +                    , id
  51.155 +                    , version
  51.156 +                    , handler
  51.157 +                    , dependencies=()
  51.158 +                    , title=None
  51.159 +                    , description=None
  51.160 +                    ):
  51.161 +        """ Register a setup step.
  51.162 +
  51.163 +        o 'id' is a unique name for this step,
  51.164 +
  51.165 +        o 'version' is a string for comparing versions, it is preferred to
  51.166 +          be a yyyy/mm/dd-ii formatted string (date plus two-digit
  51.167 +          ordinal).  when comparing two version strings, the version with
  51.168 +          the lower sort order is considered the older version.
  51.169 +
  51.170 +          - Newer versions of a step supplant older ones.
  51.171 +
  51.172 +          - Attempting to register an older one after a newer one results
  51.173 +            in a KeyError.
  51.174 +
  51.175 +        o 'handler' should implement IImportPlugin.
  51.176 +
  51.177 +        o 'dependencies' is a tuple of step ids which have to run before
  51.178 +          this step in order to be able to run at all. Registration of
  51.179 +          steps that have unmet dependencies are deferred until the
  51.180 +          dependencies have been registered.
  51.181 +
  51.182 +        o 'title' is a one-line UI description for this step.
  51.183 +          If None, the first line of the documentation string of the handler
  51.184 +          is used, or the id if no docstring can be found.
  51.185 +
  51.186 +        o 'description' is a one-line UI description for this step.
  51.187 +          If None, the remaining line of the documentation string of
  51.188 +          the handler is used, or default to ''.
  51.189 +        """
  51.190 +        already = self.getStepMetadata( id )
  51.191 +
  51.192 +        if already and already[ 'version' ] > version:
  51.193 +            raise KeyError( 'Existing registration for step %s, version %s'
  51.194 +                          % ( id, already[ 'version' ] ) )
  51.195 +
  51.196 +        if title is None or description is None:
  51.197 +
  51.198 +            t, d = _extractDocstring( handler, id, '' )
  51.199 +
  51.200 +            title = title or t
  51.201 +            description = description or d
  51.202 +
  51.203 +        info = { 'id'           : id
  51.204 +               , 'version'      : version
  51.205 +               , 'handler'      : _getDottedName( handler )
  51.206 +               , 'dependencies' : dependencies
  51.207 +               , 'title'        : title
  51.208 +               , 'description'  : description
  51.209 +               }
  51.210 +
  51.211 +        self._registered[ id ] = info
  51.212 +
  51.213 +    security.declarePrivate( 'parseXML' )
  51.214 +    def parseXML( self, text, encoding=None ):
  51.215 +
  51.216 +        """ Parse 'text'.
  51.217 +        """
  51.218 +        reader = getattr( text, 'read', None )
  51.219 +
  51.220 +        if reader is not None:
  51.221 +            text = reader()
  51.222 +
  51.223 +        parser = _ImportStepRegistryParser( encoding )
  51.224 +        parseString( text, parser )
  51.225 +
  51.226 +        return parser._parsed
  51.227 +
  51.228 +    security.declarePrivate( 'clear' )
  51.229 +    def clear( self ):
  51.230 +
  51.231 +        self._registered = {}
  51.232 +
  51.233 +    #
  51.234 +    #   Helper methods
  51.235 +    #
  51.236 +    security.declarePrivate( '_computeTopologicalSort' )
  51.237 +    def _computeTopologicalSort( self ):
  51.238 +
  51.239 +        result = []
  51.240 +
  51.241 +        graph = [ ( x[ 'id' ], x[ 'dependencies' ] )
  51.242 +                    for x in self._registered.values() ]
  51.243 +
  51.244 +        for node, edges in graph:
  51.245 +
  51.246 +            after = -1
  51.247 +
  51.248 +            for edge in edges:
  51.249 +
  51.250 +                if edge in result:
  51.251 +                    after = max( after, result.index( edge ) )
  51.252 +
  51.253 +            result.insert( after + 1, node )
  51.254 +
  51.255 +        return result
  51.256 +
  51.257 +    security.declarePrivate( '_exportTemplate' )
  51.258 +    _exportTemplate = PageTemplateFile( 'isrExport.xml', _xmldir )
  51.259 +
  51.260 +InitializeClass( ImportStepRegistry )
  51.261 +
  51.262 +
  51.263 +class ExportStepRegistry( Implicit ):
  51.264 +
  51.265 +    """ Registry of known site-configuration export steps.
  51.266 +
  51.267 +    o Each step is registered with a unique id.
  51.268 +
  51.269 +    o When called, with the portal object passed in as an argument,
  51.270 +      the step must return a sequence of three-tuples,
  51.271 +      ( 'data', 'content_type', 'filename' ), one for each file exported
  51.272 +      by the step.
  51.273 +
  51.274 +      - 'data' is a string containing the file data;
  51.275 +
  51.276 +      - 'content_type' is the MIME type of the data;
  51.277 +
  51.278 +      - 'filename' is a suggested filename for use when downloading.
  51.279 +
  51.280 +    """
  51.281 +    implements(IExportStepRegistry)
  51.282 +
  51.283 +    security = ClassSecurityInfo()
  51.284 +
  51.285 +    def __init__( self ):
  51.286 +
  51.287 +        self.clear()
  51.288 +
  51.289 +    security.declareProtected( ManagePortal, 'listSteps' )
  51.290 +    def listSteps( self ):
  51.291 +
  51.292 +        """ Return a list of registered step IDs.
  51.293 +        """
  51.294 +        return self._registered.keys()
  51.295 +
  51.296 +    security.declareProtected( ManagePortal, 'getStepMetadata' )
  51.297 +    def getStepMetadata( self, key, default=None ):
  51.298 +
  51.299 +        """ Return a mapping of metadata for the step identified by 'key'.
  51.300 +
  51.301 +        o Return 'default' if no such step is registered.
  51.302 +
  51.303 +        o The 'handler' metadata is available via 'getStep'.
  51.304 +        """
  51.305 +        info = self._registered.get( key )
  51.306 +
  51.307 +        if info is None:
  51.308 +            return default
  51.309 +
  51.310 +        return info.copy()
  51.311 +
  51.312 +    security.declareProtected( ManagePortal, 'listStepMetadata' )
  51.313 +    def listStepMetadata( self ):
  51.314 +
  51.315 +        """ Return a sequence of mappings describing registered steps.
  51.316 +
  51.317 +        o Steps will be alphabetical by ID.
  51.318 +        """
  51.319 +        step_ids = self.listSteps()
  51.320 +        step_ids.sort()
  51.321 +        return [ self.getStepMetadata( x ) for x in step_ids ]
  51.322 +
  51.323 +    security.declareProtected( ManagePortal, 'generateXML' )
  51.324 +    def generateXML( self ):
  51.325 +
  51.326 +        """ Return a round-trippable XML representation of the registry.
  51.327 +
  51.328 +        o 'handler' values are serialized using their dotted names.
  51.329 +        """
  51.330 +        return self._exportTemplate()
  51.331 +
  51.332 +    security.declarePrivate( 'getStep' )
  51.333 +    def getStep( self, key, default=None ):
  51.334 +
  51.335 +        """ Return the IExportPlugin registered for 'key'.
  51.336 +
  51.337 +        o Return 'default' if no such step is registered.
  51.338 +        """
  51.339 +        marker = object()
  51.340 +        info = self._registered.get( key, marker )
  51.341 +
  51.342 +        if info is marker:
  51.343 +            return default
  51.344 +
  51.345 +        return _resolveDottedName( info[ 'handler' ] )
  51.346 +
  51.347 +    security.declarePrivate( 'registerStep' )
  51.348 +    def registerStep( self, id, handler, title=None, description=None ):
  51.349 +
  51.350 +        """ Register an export step.
  51.351 +
  51.352 +        o 'id' is the unique identifier for this step
  51.353 +
  51.354 +        o 'step' should implement IExportPlugin.
  51.355 +
  51.356 +        o 'title' is a one-line UI description for this step.
  51.357 +          If None, the first line of the documentation string of the step
  51.358 +          is used, or the id if no docstring can be found.
  51.359 +
  51.360 +        o 'description' is a one-line UI description for this step.
  51.361 +          If None, the remaining line of the documentation string of
  51.362 +          the step is used, or default to ''.
  51.363 +        """
  51.364 +        if title is None or description is None:
  51.365 +
  51.366 +            t, d = _extractDocstring( handler, id, '' )
  51.367 +
  51.368 +            title = title or t
  51.369 +            description = description or d
  51.370 +
  51.371 +        info = { 'id'           : id
  51.372 +               , 'handler'      : _getDottedName( handler )
  51.373 +               , 'title'        : title
  51.374 +               , 'description'  : description
  51.375 +               }
  51.376 +
  51.377 +        self._registered[ id ] = info
  51.378 +
  51.379 +    security.declarePrivate( 'parseXML' )
  51.380 +    def parseXML( self, text, encoding=None ):
  51.381 +
  51.382 +        """ Parse 'text'.
  51.383 +        """
  51.384 +        reader = getattr( text, 'read', None )
  51.385 +
  51.386 +        if reader is not None:
  51.387 +            text = reader()
  51.388 +
  51.389 +        parser = _ExportStepRegistryParser( encoding )
  51.390 +        parseString( text, parser )
  51.391 +
  51.392 +        return parser._parsed
  51.393 +
  51.394 +    security.declarePrivate( 'clear' )
  51.395 +    def clear( self ):
  51.396 +
  51.397 +        self._registered = {}
  51.398 +
  51.399 +    #
  51.400 +    #   Helper methods
  51.401 +    #
  51.402 +    security.declarePrivate( '_exportTemplate' )
  51.403 +    _exportTemplate = PageTemplateFile( 'esrExport.xml', _xmldir )
  51.404 +
  51.405 +InitializeClass( ExportStepRegistry )
  51.406 +
  51.407 +class ToolsetRegistry( Implicit ):
  51.408 +
  51.409 +    """ Track required / forbidden tools.
  51.410 +    """
  51.411 +    implements(IToolsetRegistry)
  51.412 +
  51.413 +    security = ClassSecurityInfo()
  51.414 +    security.setDefaultAccess( 'allow' )
  51.415 +
  51.416 +    def __init__( self ):
  51.417 +
  51.418 +        self.clear()
  51.419 +
  51.420 +    #
  51.421 +    #   Toolset API
  51.422 +    #
  51.423 +    security.declareProtected( ManagePortal, 'listForbiddenTools' )
  51.424 +    def listForbiddenTools( self ):
  51.425 +
  51.426 +        """ See IToolsetRegistry.
  51.427 +        """
  51.428 +        result = list( self._forbidden )
  51.429 +        result.sort()
  51.430 +        return result
  51.431 +
  51.432 +    security.declareProtected( ManagePortal, 'addForbiddenTool' )
  51.433 +    def addForbiddenTool( self, tool_id ):
  51.434 +
  51.435 +        """ See IToolsetRegistry.
  51.436 +        """
  51.437 +        if tool_id in self._forbidden:
  51.438 +            return
  51.439 +
  51.440 +        if self._required.get( tool_id ) is not None:
  51.441 +            raise ValueError, 'Tool %s is required!' % tool_id
  51.442 +
  51.443 +        self._forbidden.append( tool_id )
  51.444 +
  51.445 +    security.declareProtected( ManagePortal, 'listRequiredTools' )
  51.446 +    def listRequiredTools( self ):
  51.447 +
  51.448 +        """ See IToolsetRegistry.
  51.449 +        """
  51.450 +        result = list( self._required.keys() )
  51.451 +        result.sort()
  51.452 +        return result
  51.453 +
  51.454 +    security.declareProtected( ManagePortal, 'getRequiredToolInfo' )
  51.455 +    def getRequiredToolInfo( self, tool_id ):
  51.456 +
  51.457 +        """ See IToolsetRegistry.
  51.458 +        """
  51.459 +        return self._required[ tool_id ]
  51.460 +
  51.461 +    security.declareProtected( ManagePortal, 'listRequiredToolInfo' )
  51.462 +    def listRequiredToolInfo( self ):
  51.463 +
  51.464 +        """ See IToolsetRegistry.
  51.465 +        """
  51.466 +        return [ self.getRequiredToolInfo( x )
  51.467 +                        for x in self.listRequiredTools() ]
  51.468 +
  51.469 +    security.declareProtected( ManagePortal, 'addRequiredTool' )
  51.470 +    def addRequiredTool( self, tool_id, dotted_name ):
  51.471 +
  51.472 +        """ See IToolsetRegistry.
  51.473 +        """
  51.474 +        if tool_id in self._forbidden:
  51.475 +            raise ValueError, "Forbidden tool ID: %s" % tool_id
  51.476 +
  51.477 +        self._required[ tool_id ] = { 'id' : tool_id
  51.478 +                                    , 'class' : dotted_name
  51.479 +                                    }
  51.480 +
  51.481 +    security.declareProtected( ManagePortal, 'generateXML' )
  51.482 +    def generateXML( self ):
  51.483 +
  51.484 +        """ Pseudo API.
  51.485 +        """
  51.486 +        return self._toolsetConfig()
  51.487 +
  51.488 +    security.declareProtected( ManagePortal, 'parseXML' )
  51.489 +    def parseXML( self, text, encoding=None ):
  51.490 +
  51.491 +        """ Pseudo-API
  51.492 +        """
  51.493 +        reader = getattr( text, 'read', None )
  51.494 +
  51.495 +        if reader is not None:
  51.496 +            text = reader()
  51.497 +
  51.498 +        parser = _ToolsetParser( encoding )
  51.499 +        parseString( text, parser )
  51.500 +
  51.501 +        for tool_id in parser._forbidden:
  51.502 +            self.addForbiddenTool( tool_id )
  51.503 +
  51.504 +        for tool_id, dotted_name in parser._required.items():
  51.505 +            self.addRequiredTool( tool_id, dotted_name )
  51.506 +
  51.507 +    security.declarePrivate( 'clear' )
  51.508 +    def clear( self ):
  51.509 +
  51.510 +        self._forbidden = []
  51.511 +        self._required = {}
  51.512 +
  51.513 +    #
  51.514 +    #   Helper methods.
  51.515 +    #
  51.516 +    security.declarePrivate( '_toolsetConfig' )
  51.517 +    _toolsetConfig = PageTemplateFile( 'tscExport.xml'
  51.518 +                                     , _xmldir
  51.519 +                                     , __name__='toolsetConfig'
  51.520 +                                     )
  51.521 +
  51.522 +InitializeClass( ToolsetRegistry )
  51.523 +
  51.524 +class ProfileRegistry( Implicit ):
  51.525 +
  51.526 +    """ Track registered profiles.
  51.527 +    """
  51.528 +    implements(IProfileRegistry)
  51.529 +
  51.530 +    security = ClassSecurityInfo()
  51.531 +    security.setDefaultAccess( 'allow' )
  51.532 +
  51.533 +    def __init__( self ):
  51.534 +
  51.535 +        self.clear()
  51.536 +
  51.537 +    security.declareProtected( ManagePortal, '' )
  51.538 +    def getProfileInfo( self, profile_id, for_=None ):
  51.539 +
  51.540 +        """ See IProfileRegistry.
  51.541 +        """
  51.542 +        result = self._profile_info[ profile_id ]
  51.543 +        if for_ is not None:
  51.544 +            if not issubclass( for_, result['for'] ):
  51.545 +                raise KeyError, profile_id
  51.546 +        return result.copy()
  51.547 +
  51.548 +    security.declareProtected( ManagePortal, 'listProfiles' )
  51.549 +    def listProfiles( self, for_=None ):
  51.550 +
  51.551 +        """ See IProfileRegistry.
  51.552 +        """
  51.553 +        result = []
  51.554 +        for profile_id in self._profile_ids:
  51.555 +            info = self.getProfileInfo( profile_id )
  51.556 +            if for_ is None or issubclass( for_, info['for'] ):
  51.557 +                result.append( profile_id )
  51.558 +        return tuple( result )
  51.559 +
  51.560 +    security.declareProtected( ManagePortal, 'listProfileInfo' )
  51.561 +    def listProfileInfo( self, for_=None ):
  51.562 +
  51.563 +        """ See IProfileRegistry.
  51.564 +        """
  51.565 +        candidates = [ self.getProfileInfo( id )
  51.566 +                        for id in self.listProfiles() ]
  51.567 +        return [ x for x in candidates if for_ is None or x['for'] is None or
  51.568 +                 issubclass( for_, x['for'] ) ]
  51.569 +
  51.570 +    security.declareProtected( ManagePortal, 'registerProfile' )
  51.571 +    def registerProfile( self
  51.572 +                       , name
  51.573 +                       , title
  51.574 +                       , description
  51.575 +                       , path
  51.576 +                       , product=None
  51.577 +                       , profile_type=BASE
  51.578 +                       , for_=None
  51.579 +                       ):
  51.580 +        """ See IProfileRegistry.
  51.581 +        """
  51.582 +        profile_id = '%s:%s' % (product or 'other', name)
  51.583 +        if self._profile_info.get( profile_id ) is not None:
  51.584 +            raise KeyError, 'Duplicate profile ID: %s' % profile_id
  51.585 +
  51.586 +        self._profile_ids.append( profile_id )
  51.587 +
  51.588 +        info = { 'id' : profile_id
  51.589 +               , 'title' : title
  51.590 +               , 'description' : description
  51.591 +               , 'path' : path
  51.592 +               , 'product' : product
  51.593 +               , 'type': profile_type
  51.594 +               , 'for': for_
  51.595 +               }
  51.596 +
  51.597 +        self._profile_info[ profile_id ] = info
  51.598 +
  51.599 +    security.declarePrivate( 'clear' )
  51.600 +    def clear( self ):
  51.601 +
  51.602 +        self._profile_info = {}
  51.603 +        self._profile_ids = []
  51.604 +
  51.605 +InitializeClass( ProfileRegistry )
  51.606 +
  51.607 +_profile_registry = ProfileRegistry()
  51.608 +
  51.609 +class _ImportStepRegistryParser( HandlerBase ):
  51.610 +
  51.611 +    security = ClassSecurityInfo()
  51.612 +    security.declareObjectPrivate()
  51.613 +    security.setDefaultAccess( 'deny' )
  51.614 +
  51.615 +    def __init__( self, encoding ):
  51.616 +
  51.617 +        self._encoding = encoding
  51.618 +        self._started = False
  51.619 +        self._pending = None
  51.620 +        self._parsed = []
  51.621 +
  51.622 +    def startElement( self, name, attrs ):
  51.623 +
  51.624 +        if name == 'import-steps':
  51.625 +
  51.626 +            if self._started:
  51.627 +                raise ValueError, 'Duplicated setup-steps element: %s' % name
  51.628 +
  51.629 +            self._started = True
  51.630 +
  51.631 +        elif name == 'import-step':
  51.632 +
  51.633 +            if self._pending is not None:
  51.634 +                raise ValueError, 'Cannot nest setup-step elements'
  51.635 +
  51.636 +            self._pending = dict( [ ( k, self._extract( attrs, k ) )
  51.637 +                                    for k in attrs.keys() ] )
  51.638 +
  51.639 +            self._pending[ 'dependencies' ] = []
  51.640 +
  51.641 +        elif name == 'dependency':
  51.642 +
  51.643 +            if not self._pending:
  51.644 +                raise ValueError, 'Dependency outside of step'
  51.645 +
  51.646 +            depended = self._extract( attrs, 'step' )
  51.647 +            self._pending[ 'dependencies' ].append( depended )
  51.648 +
  51.649 +        else:
  51.650 +            raise ValueError, 'Unknown element %s' % name
  51.651 +
  51.652 +    def characters( self, content ):
  51.653 +
  51.654 +        if self._pending is not None:
  51.655 +            content = self._encode( content )
  51.656 +            self._pending.setdefault( 'description', [] ).append( content )
  51.657 +
  51.658 +    def endElement(self, name):
  51.659 +
  51.660 +        if name == 'import-steps':
  51.661 +            pass
  51.662 +
  51.663 +        elif name == 'import-step':
  51.664 +
  51.665 +            if self._pending is None:
  51.666 +                raise ValueError, 'No pending step!'
  51.667 +
  51.668 +            deps = tuple( self._pending[ 'dependencies' ] )
  51.669 +            self._pending[ 'dependencies' ] = deps
  51.670 +
  51.671 +            desc = ''.join( self._pending[ 'description' ] )
  51.672 +            self._pending[ 'description' ] = desc
  51.673 +
  51.674 +            self._parsed.append( self._pending )
  51.675 +            self._pending = None
  51.676 +
  51.677 +InitializeClass( _ImportStepRegistryParser )
  51.678 +
  51.679 +class _ExportStepRegistryParser( HandlerBase ):
  51.680 +
  51.681 +    security = ClassSecurityInfo()
  51.682 +    security.declareObjectPrivate()
  51.683 +    security.setDefaultAccess( 'deny' )
  51.684 +
  51.685 +    def __init__( self, encoding ):
  51.686 +
  51.687 +        self._encoding = encoding
  51.688 +        self._started = False
  51.689 +        self._pending = None
  51.690 +        self._parsed = []
  51.691 +
  51.692 +    def startElement( self, name, attrs ):
  51.693 +
  51.694 +        if name == 'export-steps':
  51.695 +
  51.696 +            if self._started:
  51.697 +                raise ValueError, 'Duplicated export-steps element: %s' % name
  51.698 +
  51.699 +            self._started = True
  51.700 +
  51.701 +        elif name == 'export-step':
  51.702 +
  51.703 +            if self._pending is not None:
  51.704 +                raise ValueError, 'Cannot nest export-step elements'
  51.705 +
  51.706 +            self._pending = dict( [ ( k, self._extract( attrs, k ) )
  51.707 +                                    for k in attrs.keys() ] )
  51.708 +
  51.709 +        else:
  51.710 +            raise ValueError, 'Unknown element %s' % name
  51.711 +
  51.712 +    def characters( self, content ):
  51.713 +
  51.714 +        if self._pending is not None:
  51.715 +            content = self._encode( content )
  51.716 +            self._pending.setdefault( 'description', [] ).append( content )
  51.717 +
  51.718 +    def endElement(self, name):
  51.719 +
  51.720 +        if name == 'export-steps':
  51.721 +            pass
  51.722 +
  51.723 +        elif name == 'export-step':
  51.724 +
  51.725 +            if self._pending is None:
  51.726 +                raise ValueError, 'No pending step!'
  51.727 +
  51.728 +            desc = ''.join( self._pending[ 'description' ] )
  51.729 +            self._pending[ 'description' ] = desc
  51.730 +
  51.731 +            self._parsed.append( self._pending )
  51.732 +            self._pending = None
  51.733 +
  51.734 +InitializeClass( _ExportStepRegistryParser )
  51.735 +
  51.736 +
  51.737 +class _ToolsetParser( HandlerBase ):
  51.738 +
  51.739 +    security = ClassSecurityInfo()
  51.740 +    security.declareObjectPrivate()
  51.741 +    security.setDefaultAccess( 'deny' )
  51.742 +
  51.743 +    def __init__( self, encoding ):
  51.744 +
  51.745 +        self._encoding = encoding
  51.746 +        self._required = {}
  51.747 +        self._forbidden = []
  51.748 +
  51.749 +    def startElement( self, name, attrs ):
  51.750 +
  51.751 +        if name == 'tool-setup':
  51.752 +            pass
  51.753 +
  51.754 +        elif name == 'forbidden':
  51.755 +
  51.756 +            tool_id = self._extract( attrs, 'tool_id' )
  51.757 +
  51.758 +            if tool_id not in self._forbidden:
  51.759 +                self._forbidden.append( tool_id )
  51.760 +
  51.761 +        elif name == 'required':
  51.762 +
  51.763 +            tool_id = self._extract( attrs, 'tool_id' )
  51.764 +            dotted_name = self._extract( attrs, 'class' )
  51.765 +            self._required[ tool_id ] = dotted_name
  51.766 +
  51.767 +        else:
  51.768 +            raise ValueError, 'Unknown element %s' % name
  51.769 +
  51.770 +
  51.771 +InitializeClass( _ToolsetParser )
    52.1 new file mode 100644
    52.2 --- /dev/null
    52.3 +++ b/rolemap.py
    52.4 @@ -0,0 +1,215 @@
    52.5 +##############################################################################
    52.6 +#
    52.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    52.8 +#
    52.9 +# This software is subject to the provisions of the Zope Public License,
   52.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   52.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   52.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   52.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   52.14 +# FOR A PARTICULAR PURPOSE.
   52.15 +#
   52.16 +##############################################################################
   52.17 +""" GenericSetup:  Role-permission export / import
   52.18 +
   52.19 +$Id: rolemap.py 40234 2005-11-18 21:22:13Z tseaver $
   52.20 +"""
   52.21 +
   52.22 +from AccessControl import ClassSecurityInfo
   52.23 +from AccessControl.Permission import Permission
   52.24 +from Globals import InitializeClass
   52.25 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   52.26 +
   52.27 +from permissions import ManagePortal
   52.28 +from utils import _xmldir
   52.29 +from utils import ConfiguratorBase
   52.30 +from utils import CONVERTER, DEFAULT, KEY
   52.31 +
   52.32 +
   52.33 +#
   52.34 +#   Configurator entry points
   52.35 +#
   52.36 +_FILENAME = 'rolemap.xml'
   52.37 +
   52.38 +def importRolemap( context ):
   52.39 +
   52.40 +    """ Import roles / permission map from an XML file.
   52.41 +
   52.42 +    o 'context' must implement IImportContext.
   52.43 +
   52.44 +    o Register via Python:
   52.45 +
   52.46 +      registry = site.setup_tool.setup_steps
   52.47 +      registry.registerStep( 'importRolemap'
   52.48 +                           , '20040518-01'
   52.49 +                           , Products.GenericSetup.rolemap.importRolemap
   52.50 +                           , ()
   52.51 +                           , 'Role / Permission import'
   52.52 +                           , 'Import additional roles, and map '
   52.53 +                           'roles to permissions'
   52.54 +                           )
   52.55 +
   52.56 +    o Register via XML:
   52.57 +
   52.58 +      <setup-step id="importRolemap"
   52.59 +                  version="20040518-01"
   52.60 +                  handler="Products.GenericSetup.rolemap.importRolemap"
   52.61 +                  title="Role / Permission import"
   52.62 +      >Import additional roles, and map roles to permissions.</setup-step>
   52.63 +
   52.64 +    """
   52.65 +    site = context.getSite()
   52.66 +    encoding = context.getEncoding()
   52.67 +    logger = context.getLogger('rolemap')
   52.68 +
   52.69 +    if context.shouldPurge():
   52.70 +
   52.71 +        items = site.__dict__.items()
   52.72 +
   52.73 +        for k, v in items: # XXX: WAAA
   52.74 +
   52.75 +            if k == '__ac_roles__':
   52.76 +                delattr( site, k )
   52.77 +
   52.78 +            if k.startswith( '_' ) and k.endswith( '_Permission' ):
   52.79 +                delattr( site, k )
   52.80 +
   52.81 +    text = context.readDataFile( _FILENAME )
   52.82 +
   52.83 +    if text is not None:
   52.84 +
   52.85 +        rc = RolemapConfigurator( site, encoding )
   52.86 +        rolemap_info = rc.parseXML( text )
   52.87 +
   52.88 +        immediate_roles = list( getattr( site, '__ac_roles__', [] ) )[:]
   52.89 +        already = {}
   52.90 +
   52.91 +        for role in site.valid_roles():
   52.92 +            already[ role ] = 1
   52.93 +
   52.94 +        for role in rolemap_info[ 'roles' ]:
   52.95 +
   52.96 +            if already.get( role ) is None:
   52.97 +                immediate_roles.append( role )
   52.98 +                already[ role ] = 1
   52.99 +
  52.100 +        immediate_roles.sort()
  52.101 +        site.__ac_roles__ = tuple( immediate_roles )
  52.102 +
  52.103 +        for permission in rolemap_info[ 'permissions' ]:
  52.104 +
  52.105 +            site.manage_permission( permission[ 'name' ]
  52.106 +                                  , permission[ 'roles' ]
  52.107 +                                  , permission[ 'acquire' ]
  52.108 +                                  )
  52.109 +
  52.110 +    logger.info('Role / permission map imported.')
  52.111 +
  52.112 +
  52.113 +def exportRolemap( context ):
  52.114 +
  52.115 +    """ Export roles / permission map as an XML file
  52.116 +
  52.117 +    o 'context' must implement IExportContext.
  52.118 +
  52.119 +    o Register via Python:
  52.120 +
  52.121 +      registry = site.setup_tool.export_steps
  52.122 +      registry.registerStep( 'exportRolemap'
  52.123 +                           , Products.GenericSetup.rolemap.exportRolemap
  52.124 +                           , 'Role / Permission export'
  52.125 +                           , 'Export additional roles, and '
  52.126 +                             'role / permission map '
  52.127 +                           )
  52.128 +
  52.129 +    o Register via XML:
  52.130 +
  52.131 +      <export-script id="exportRolemap"
  52.132 +                     version="20040518-01"
  52.133 +                     handler="Products.GenericSetup.rolemap.exportRolemap"
  52.134 +                     title="Role / Permission export"
  52.135 +      >Export additional roles, and role / permission map.</export-script>
  52.136 +
  52.137 +    """
  52.138 +    site = context.getSite()
  52.139 +    logger = context.getLogger('rolemap')
  52.140 +
  52.141 +    rc = RolemapConfigurator( site ).__of__( site )
  52.142 +    text = rc.generateXML()
  52.143 +
  52.144 +    context.writeDataFile( _FILENAME, text, 'text/xml' )
  52.145 +
  52.146 +    logger.info('Role / permission map exported.')
  52.147 +
  52.148 +
  52.149 +class RolemapConfigurator(ConfiguratorBase):
  52.150 +    """ Synthesize XML description of sitewide role-permission settings.
  52.151 +    """
  52.152 +    security = ClassSecurityInfo()
  52.153 +
  52.154 +    security.declareProtected( ManagePortal, 'listRoles' )
  52.155 +    def listRoles( self ):
  52.156 +
  52.157 +        """ List the valid role IDs for our site.
  52.158 +        """
  52.159 +        return self._site.valid_roles()
  52.160 +
  52.161 +    security.declareProtected( ManagePortal, 'listPermissions' )
  52.162 +    def listPermissions( self ):
  52.163 +
  52.164 +        """ List permissions for export.
  52.165 +
  52.166 +        o Returns a sqeuence of mappings describing locally-modified
  52.167 +          permission / role settings.  Keys include:
  52.168 +
  52.169 +          'permission' -- the name of the permission
  52.170 +
  52.171 +          'acquire' -- a flag indicating whether to acquire roles from the
  52.172 +              site's container
  52.173 +
  52.174 +          'roles' -- the list of roles which have the permission.
  52.175 +
  52.176 +        o Do not include permissions which both acquire and which define
  52.177 +          no local changes to the acquired policy.
  52.178 +        """
  52.179 +        permissions = []
  52.180 +        valid_roles = self.listRoles()
  52.181 +
  52.182 +        for perm in self._site.ac_inherited_permissions( 1 ):
  52.183 +
  52.184 +            name = perm[ 0 ]
  52.185 +            p = Permission( name, perm[ 1 ], self._site )
  52.186 +            roles = p.getRoles( default=[] )
  52.187 +            acquire = isinstance( roles, list )  # tuple means don't acquire
  52.188 +            roles = [ r for r in roles if r in valid_roles ]
  52.189 +
  52.190 +            if roles or not acquire:
  52.191 +                permissions.append( { 'name'    : name
  52.192 +                                    , 'acquire' : acquire
  52.193 +                                    , 'roles'   : roles
  52.194 +                                    } )
  52.195 +
  52.196 +        return permissions
  52.197 +
  52.198 +    def _getExportTemplate(self):
  52.199 +
  52.200 +        return PageTemplateFile('rmeExport.xml', _xmldir)
  52.201 +
  52.202 +    def _getImportMapping(self):
  52.203 +
  52.204 +        return {
  52.205 +          'rolemap':
  52.206 +            { 'roles':       {CONVERTER: self._convertToUnique},
  52.207 +              'permissions': {CONVERTER: self._convertToUnique} },
  52.208 +          'roles':
  52.209 +            { 'role':        {KEY: None} },
  52.210 +          'role':
  52.211 +            { 'name':        {KEY: None} },
  52.212 +          'permissions':
  52.213 +            { 'permission':  {KEY: None, DEFAULT: ()} },
  52.214 +          'permission':
  52.215 +            { 'name':        {},
  52.216 +              'role':        {KEY: 'roles'},
  52.217 +              'acquire':     {CONVERTER: self._convertToBoolean} } }
  52.218 +
  52.219 +InitializeClass(RolemapConfigurator)
    53.1 new file mode 100644
    53.2 --- /dev/null
    53.3 +++ b/testing.py
    53.4 @@ -0,0 +1,119 @@
    53.5 +##############################################################################
    53.6 +#
    53.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    53.8 +#
    53.9 +# This software is subject to the provisions of the Zope Public License,
   53.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   53.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   53.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   53.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   53.14 +# FOR A PARTICULAR PURPOSE.
   53.15 +#
   53.16 +##############################################################################
   53.17 +"""Node adapter testing utils.
   53.18 +
   53.19 +$Id: testing.py 40715 2005-12-12 10:33:40Z yuppie $
   53.20 +"""
   53.21 +
   53.22 +import unittest
   53.23 +import Testing
   53.24 +
   53.25 +from xml.dom.minidom import parseString
   53.26 +
   53.27 +import Products.Five
   53.28 +from Products.Five import zcml
   53.29 +from zope.app import zapi
   53.30 +from zope.interface import implements
   53.31 +from zope.interface.verify import verifyClass
   53.32 +
   53.33 +from interfaces import IBody
   53.34 +from interfaces import INode
   53.35 +from interfaces import ISetupEnviron
   53.36 +
   53.37 +try:
   53.38 +    from zope.app.testing.placelesssetup import PlacelessSetup
   53.39 +except ImportError:  # BBB, Zope3 < 3.1
   53.40 +    from zope.app.tests.placelesssetup import PlacelessSetup
   53.41 +
   53.42 +
   53.43 +class DummyLogger:
   53.44 +
   53.45 +    def __init__(self, id, messages):
   53.46 +        self._id = id
   53.47 +        self._messages = messages
   53.48 +
   53.49 +    def info(self, msg, *args, **kwargs):
   53.50 +        self._messages.append((20, self._id, msg))
   53.51 +
   53.52 +    def warning(self, msg, *args, **kwargs):
   53.53 +        self._messages.append((30, self._id, msg))
   53.54 +
   53.55 +
   53.56 +class DummySetupEnviron(object):
   53.57 +
   53.58 +    """Context for body im- and exporter.
   53.59 +    """
   53.60 +
   53.61 +    implements(ISetupEnviron)
   53.62 +
   53.63 +    def __init__(self):
   53.64 +        self._notes = []
   53.65 +        self._should_purge = True
   53.66 +
   53.67 +    def getLogger(self, name):
   53.68 +        return DummyLogger(name, self._notes)
   53.69 +
   53.70 +    def shouldPurge(self):
   53.71 +        return self._should_purge
   53.72 +
   53.73 +
   53.74 +class _AdapterTestCaseBase(PlacelessSetup, unittest.TestCase):
   53.75 +
   53.76 +    def _populate(self, obj):
   53.77 +        pass
   53.78 +
   53.79 +    def _verifyImport(self, obj):
   53.80 +        pass
   53.81 +
   53.82 +    def setUp(self):
   53.83 +        PlacelessSetup.setUp(self)
   53.84 +        zcml.load_config('meta.zcml', Products.Five)
   53.85 +        zcml.load_config('permissions.zcml', Products.Five)
   53.86 +
   53.87 +
   53.88 +class BodyAdapterTestCase(_AdapterTestCaseBase):
   53.89 +
   53.90 +    def test_z3interfaces(self):
   53.91 +        verifyClass(IBody, self._getTargetClass())
   53.92 +
   53.93 +    def test_body_get(self):
   53.94 +        self._populate(self._obj)
   53.95 +        context = DummySetupEnviron()
   53.96 +        adapted = zapi.getMultiAdapter((self._obj, context), IBody)
   53.97 +        self.assertEqual(adapted.body, self._BODY)
   53.98 +
   53.99 +    def test_body_set(self):
  53.100 +        context = DummySetupEnviron()
  53.101 +        adapted = zapi.getMultiAdapter((self._obj, context), IBody)
  53.102 +        adapted.body = self._BODY
  53.103 +        self._verifyImport(self._obj)
  53.104 +        self.assertEqual(adapted.body, self._BODY)
  53.105 +
  53.106 +
  53.107 +class NodeAdapterTestCase(_AdapterTestCaseBase):
  53.108 +
  53.109 +    def test_z3interfaces(self):
  53.110 +        verifyClass(INode, self._getTargetClass())
  53.111 +
  53.112 +    def test_node_get(self):
  53.113 +        self._populate(self._obj)
  53.114 +        context = DummySetupEnviron()
  53.115 +        adapted = zapi.getMultiAdapter((self._obj, context), INode)
  53.116 +        self.assertEqual(adapted.node.toprettyxml(' '), self._XML)
  53.117 +
  53.118 +    def test_node_set(self):
  53.119 +        context = DummySetupEnviron()
  53.120 +        adapted = zapi.getMultiAdapter((self._obj, context), INode)
  53.121 +        adapted.node = parseString(self._XML).documentElement
  53.122 +        self._verifyImport(self._obj)
  53.123 +        self.assertEqual(adapted.node.toprettyxml(' '), self._XML)
    54.1 new file mode 100644
    54.2 --- /dev/null
    54.3 +++ b/tests/__init__.py
    54.4 @@ -0,0 +1,16 @@
    54.5 +##############################################################################
    54.6 +#
    54.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    54.8 +#
    54.9 +# This software is subject to the provisions of the Zope Public License,
   54.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   54.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   54.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   54.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   54.14 +# FOR A PARTICULAR PURPOSE.
   54.15 +#
   54.16 +##############################################################################
   54.17 +""" GenericSetup product unit tests.
   54.18 +
   54.19 +$Id: __init__.py 38575 2005-09-24 09:01:32Z yuppie $
   54.20 +"""
    55.1 new file mode 100644
    55.2 --- /dev/null
    55.3 +++ b/tests/common.py
    55.4 @@ -0,0 +1,246 @@
    55.5 +##############################################################################
    55.6 +#
    55.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    55.8 +#
    55.9 +# This software is subject to the provisions of the Zope Public License,
   55.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   55.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   55.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   55.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   55.14 +# FOR A PARTICULAR PURPOSE.
   55.15 +#
   55.16 +##############################################################################
   55.17 +""" GenericSetup product:  unit test utilities.
   55.18 +
   55.19 +$Id: common.py 40715 2005-12-12 10:33:40Z yuppie $
   55.20 +"""
   55.21 +
   55.22 +import os
   55.23 +import shutil
   55.24 +from tarfile import TarFile
   55.25 +
   55.26 +from Acquisition import Implicit
   55.27 +from Testing.ZopeTestCase import ZopeTestCase
   55.28 +from zope.interface import implements
   55.29 +
   55.30 +from Products.GenericSetup.interfaces import IExportContext
   55.31 +from Products.GenericSetup.interfaces import IImportContext
   55.32 +from Products.GenericSetup.testing import DummyLogger
   55.33 +
   55.34 +
   55.35 +class OmnipotentUser(Implicit):
   55.36 +    """ Omnipotent User for unit testing purposes.
   55.37 +    """
   55.38 +    def getId(self):
   55.39 +        return 'all_powerful_Oz'
   55.40 +
   55.41 +    getUserName = getId
   55.42 +
   55.43 +    def getRoles(self):
   55.44 +        return ('Manager',)
   55.45 +
   55.46 +    def allowed(self, object, object_roles=None):
   55.47 +        return 1
   55.48 +
   55.49 +    def getRolesInContext(self, object):
   55.50 +        return ('Manager',)
   55.51 +
   55.52 +class SecurityRequestTest(ZopeTestCase):
   55.53 +    def setUp(self):
   55.54 +        from AccessControl.SecurityManagement import newSecurityManager
   55.55 +        ZopeTestCase.setUp(self)
   55.56 +        self.root = self.app
   55.57 +        newSecurityManager(None, OmnipotentUser().__of__(self.app.acl_users))
   55.58 +
   55.59 +    def tearDown(self):
   55.60 +        from AccessControl.SecurityManagement import noSecurityManager
   55.61 +        noSecurityManager()
   55.62 +        ZopeTestCase.tearDown(self)
   55.63 +
   55.64 +class DOMComparator:
   55.65 +
   55.66 +    def _compareDOM( self, found_text, expected_text, debug=False ):
   55.67 +
   55.68 +        found_lines = [ x.strip() for x in found_text.splitlines() ]
   55.69 +        found_text = '\n'.join( filter( None, found_lines ) )
   55.70 +
   55.71 +        expected_lines = [ x.strip() for x in expected_text.splitlines() ]
   55.72 +        expected_text = '\n'.join( filter( None, expected_lines ) )
   55.73 +
   55.74 +        from xml.dom.minidom import parseString
   55.75 +        found = parseString( found_text )
   55.76 +        expected = parseString( expected_text )
   55.77 +        fxml = found.toxml()
   55.78 +        exml = expected.toxml()
   55.79 +
   55.80 +        if fxml != exml:
   55.81 +
   55.82 +            if debug:
   55.83 +                zipped = zip( fxml, exml )
   55.84 +                diff = [ ( i, zipped[i][0], zipped[i][1] )
   55.85 +                        for i in range( len( zipped ) )
   55.86 +                        if zipped[i][0] != zipped[i][1]
   55.87 +                    ]
   55.88 +                import pdb; pdb.set_trace()
   55.89 +
   55.90 +            print 'Found:'
   55.91 +            print fxml
   55.92 +            print
   55.93 +            print 'Expected:'
   55.94 +            print exml
   55.95 +            print
   55.96 +
   55.97 +        self.assertEqual( found.toxml(), expected.toxml() )
   55.98 +
   55.99 +class BaseRegistryTests( SecurityRequestTest, DOMComparator ):
  55.100 +
  55.101 +    def _makeOne( self, *args, **kw ):
  55.102 +
  55.103 +        # Derived classes must implement _getTargetClass
  55.104 +        return self._getTargetClass()( *args, **kw )
  55.105 +
  55.106 +def _clearTestDirectory( root_path ):
  55.107 +
  55.108 +    if os.path.exists( root_path ):
  55.109 +        shutil.rmtree( root_path )
  55.110 +
  55.111 +def _makeTestFile( filename, root_path, contents ):
  55.112 +
  55.113 +    path, filename = os.path.split( filename )
  55.114 +
  55.115 +    subdir = os.path.join( root_path, path )
  55.116 +
  55.117 +    if not os.path.exists( subdir ):
  55.118 +        os.makedirs( subdir )
  55.119 +
  55.120 +    fqpath = os.path.join( subdir, filename )
  55.121 +
  55.122 +    file = open( fqpath, 'wb' )
  55.123 +    file.write( contents )
  55.124 +    file.close()
  55.125 +    return fqpath
  55.126 +
  55.127 +class FilesystemTestBase( SecurityRequestTest ):
  55.128 +
  55.129 +    def _makeOne( self, *args, **kw ):
  55.130 +
  55.131 +        return self._getTargetClass()( *args, **kw )
  55.132 +
  55.133 +    def setUp( self ):
  55.134 +
  55.135 +        SecurityRequestTest.setUp( self )
  55.136 +        self._clearTempDir()
  55.137 +
  55.138 +    def tearDown( self ):
  55.139 +
  55.140 +        self._clearTempDir()
  55.141 +        SecurityRequestTest.tearDown( self )
  55.142 +
  55.143 +    def _clearTempDir( self ):
  55.144 +
  55.145 +        _clearTestDirectory( self._PROFILE_PATH )
  55.146 +
  55.147 +    def _makeFile( self, filename, contents ):
  55.148 +
  55.149 +        return _makeTestFile( filename, self._PROFILE_PATH, contents )
  55.150 +
  55.151 +
  55.152 +class TarballTester( DOMComparator ):
  55.153 +
  55.154 +    def _verifyTarballContents( self, fileish, toc_list, when=None ):
  55.155 +
  55.156 +        fileish.seek( 0L )
  55.157 +        tarfile = TarFile.open( 'foo.tar.gz', fileobj=fileish, mode='r:gz' )
  55.158 +        items = tarfile.getnames()
  55.159 +        items.sort()
  55.160 +        toc_list.sort()
  55.161 +
  55.162 +        self.assertEqual( len( items ), len( toc_list ) )
  55.163 +        for i in range( len( items ) ):
  55.164 +            self.assertEqual( items[ i ], toc_list[ i ] )
  55.165 +
  55.166 +        if when is not None:
  55.167 +            for tarinfo in tarfile:
  55.168 +                self.failIf( tarinfo.mtime < when )
  55.169 +
  55.170 +    def _verifyTarballEntry( self, fileish, entry_name, data ):
  55.171 +
  55.172 +        fileish.seek( 0L )
  55.173 +        tarfile = TarFile.open( 'foo.tar.gz', fileobj=fileish, mode='r:gz' )
  55.174 +        extract = tarfile.extractfile( entry_name )
  55.175 +        found = extract.read()
  55.176 +        self.assertEqual( found, data )
  55.177 +
  55.178 +    def _verifyTarballEntryXML( self, fileish, entry_name, data ):
  55.179 +
  55.180 +        fileish.seek( 0L )
  55.181 +        tarfile = TarFile.open( 'foo.tar.gz', fileobj=fileish, mode='r:gz' )
  55.182 +        extract = tarfile.extractfile( entry_name )
  55.183 +        found = extract.read()
  55.184 +        self._compareDOM( found, data )
  55.185 +
  55.186 +
  55.187 +class DummyExportContext:
  55.188 +
  55.189 +    implements(IExportContext)
  55.190 +
  55.191 +    def __init__( self, site, tool=None ):
  55.192 +        self._site = site
  55.193 +        self._tool = tool
  55.194 +        self._wrote = []
  55.195 +        self._notes = []
  55.196 +
  55.197 +    def getSite( self ):
  55.198 +        return self._site
  55.199 +
  55.200 +    def getSetupTool( self ):
  55.201 +        return self._tool
  55.202 +
  55.203 +    def getLogger(self, name):
  55.204 +        return DummyLogger(name, self._notes)
  55.205 +
  55.206 +    def writeDataFile( self, filename, text, content_type, subdir=None ):
  55.207 +        if subdir is not None:
  55.208 +            filename = '%s/%s' % ( subdir, filename )
  55.209 +        self._wrote.append( ( filename, text, content_type ) )
  55.210 +
  55.211 +
  55.212 +class DummyImportContext:
  55.213 +
  55.214 +    implements(IImportContext)
  55.215 +
  55.216 +    def __init__( self, site, purge=True, encoding=None, tool=None ):
  55.217 +        self._site = site
  55.218 +        self._tool = tool
  55.219 +        self._purge = purge
  55.220 +        self._encoding = encoding
  55.221 +        self._files = {}
  55.222 +        self._notes = []
  55.223 +
  55.224 +    def getSite( self ):
  55.225 +        return self._site
  55.226 +
  55.227 +    def getSetupTool( self ):
  55.228 +        return self._tool
  55.229 +
  55.230 +    def getEncoding( self ):
  55.231 +        return self._encoding
  55.232 +
  55.233 +    def getLogger(self, name):
  55.234 +        return DummyLogger(name, self._notes)
  55.235 +
  55.236 +    def readDataFile( self, filename, subdir=None ):
  55.237 +
  55.238 +        if subdir is not None:
  55.239 +            filename = '/'.join( (subdir, filename) )
  55.240 +
  55.241 +        return self._files.get( filename )
  55.242 +
  55.243 +    def shouldPurge( self ):
  55.244 +
  55.245 +        return self._purge
  55.246 +
  55.247 +
  55.248 +def dummy_handler( context ):
  55.249 +
  55.250 +    pass
    56.1 new file mode 100644
    56.2 --- /dev/null
    56.3 +++ b/tests/conformance.py
    56.4 @@ -0,0 +1,163 @@
    56.5 +##############################################################################
    56.6 +#
    56.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    56.8 +#
    56.9 +# This software is subject to the provisions of the Zope Public License,
   56.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   56.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   56.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   56.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   56.14 +# FOR A PARTICULAR PURPOSE.
   56.15 +#
   56.16 +##############################################################################
   56.17 +""" Base classes for testing interface conformance.
   56.18 +
   56.19 +Derived testcase classes should define '_getTargetClass()', which must
   56.20 +return the class being tested for conformance.
   56.21 +
   56.22 +$Id: conformance.py 40140 2005-11-15 18:53:19Z tseaver $
   56.23 +"""
   56.24 +
   56.25 +class ConformsToISetupContext:
   56.26 +
   56.27 +    def test_ISetupContext_conformance( self ):
   56.28 +
   56.29 +        from Products.GenericSetup.interfaces import ISetupContext
   56.30 +        from zope.interface.verify import verifyClass
   56.31 +
   56.32 +        verifyClass( ISetupContext, self._getTargetClass() )
   56.33 +
   56.34 +class ConformsToIImportContext:
   56.35 +
   56.36 +    def test_IImportContext_conformance( self ):
   56.37 +
   56.38 +        from Products.GenericSetup.interfaces import IImportContext
   56.39 +        from zope.interface.verify import verifyClass
   56.40 +
   56.41 +        verifyClass( IImportContext, self._getTargetClass() )
   56.42 +
   56.43 +class ConformsToIExportContext:
   56.44 +
   56.45 +    def test_IExportContext_conformance( self ):
   56.46 +
   56.47 +        from Products.GenericSetup.interfaces import IExportContext
   56.48 +        from zope.interface.verify import verifyClass
   56.49 +
   56.50 +        verifyClass( IExportContext, self._getTargetClass() )
   56.51 +
   56.52 +class ConformsToIStepRegistry:
   56.53 +
   56.54 +    def test_IStepRegistry_conformance( self ):
   56.55 +
   56.56 +        from Products.GenericSetup.interfaces import IStepRegistry
   56.57 +        from zope.interface.verify import verifyClass
   56.58 +
   56.59 +        verifyClass( IStepRegistry, self._getTargetClass() )
   56.60 +
   56.61 +class ConformsToIImportStepRegistry:
   56.62 +
   56.63 +    def test_IImportStepRegistry_conformance( self ):
   56.64 +
   56.65 +        from Products.GenericSetup.interfaces import IImportStepRegistry
   56.66 +        from zope.interface.verify import verifyClass
   56.67 +
   56.68 +        verifyClass( IImportStepRegistry, self._getTargetClass() )
   56.69 +
   56.70 +class ConformsToIExportStepRegistry:
   56.71 +
   56.72 +    def test_IExportStepRegistry_conformance( self ):
   56.73 +
   56.74 +        from Products.GenericSetup.interfaces import IExportStepRegistry
   56.75 +        from zope.interface.verify import verifyClass
   56.76 +
   56.77 +        verifyClass( IExportStepRegistry, self._getTargetClass() )
   56.78 +
   56.79 +class ConformsToIToolsetRegistry:
   56.80 +
   56.81 +    def test_IToolsetRegistry_conformance( self ):
   56.82 +
   56.83 +        from Products.GenericSetup.interfaces import IToolsetRegistry
   56.84 +        from zope.interface.verify import verifyClass
   56.85 +
   56.86 +        verifyClass( IToolsetRegistry, self._getTargetClass() )
   56.87 +
   56.88 +class ConformsToIProfileRegistry:
   56.89 +
   56.90 +    def test_IProfileRegistry_conformance( self ):
   56.91 +
   56.92 +        from Products.GenericSetup.interfaces import IProfileRegistry
   56.93 +        from zope.interface.verify import verifyClass
   56.94 +
   56.95 +        verifyClass( IProfileRegistry, self._getTargetClass() )
   56.96 +
   56.97 +class ConformsToISetupTool:
   56.98 +
   56.99 +    def test_ISetupTool_conformance( self ):
  56.100 +
  56.101 +        from Products.GenericSetup.interfaces import ISetupTool
  56.102 +        from zope.interface.verify import verifyClass
  56.103 +
  56.104 +        verifyClass( ISetupTool, self._getTargetClass() )
  56.105 +
  56.106 +class ConformsToIContentFactory:
  56.107 +
  56.108 +    def test_conforms_to_IContentFactory(self):
  56.109 +
  56.110 +        from Products.GenericSetup.interfaces import IContentFactory
  56.111 +        from zope.interface.verify import verifyClass
  56.112 +
  56.113 +        verifyClass( IContentFactory, self._getTargetClass() )
  56.114 +
  56.115 +class ConformsToIContentFactoryName:
  56.116 +
  56.117 +    def test_conforms_to_IContentFactory(self):
  56.118 +
  56.119 +        from Products.GenericSetup.interfaces import IContentFactoryName
  56.120 +        from zope.interface.verify import verifyClass
  56.121 +
  56.122 +        verifyClass( IContentFactoryName, self._getTargetClass() )
  56.123 +
  56.124 +class ConformsToIFilesystemExporter:
  56.125 +
  56.126 +    def test_conforms_to_IFilesystemExporter(self):
  56.127 +
  56.128 +        from Products.GenericSetup.interfaces import IFilesystemExporter
  56.129 +        from zope.interface.verify import verifyClass
  56.130 +
  56.131 +        verifyClass( IFilesystemExporter, self._getTargetClass() )
  56.132 +
  56.133 +class ConformsToIFilesystemImporter:
  56.134 +
  56.135 +    def test_conforms_to_IFilesystemImporter(self):
  56.136 +
  56.137 +        from Products.GenericSetup.interfaces import IFilesystemImporter
  56.138 +        from zope.interface.verify import verifyClass
  56.139 +
  56.140 +        verifyClass( IFilesystemImporter, self._getTargetClass() )
  56.141 +
  56.142 +class ConformsToIINIAware:
  56.143 +
  56.144 +    def test_conforms_to_IINIAware(self):
  56.145 +
  56.146 +        from Products.GenericSetup.interfaces import IINIAware
  56.147 +        from zope.interface.verify import verifyClass
  56.148 +
  56.149 +        verifyClass (IINIAware, self._getTargetClass() )
  56.150 +
  56.151 +class ConformsToICSVAware:
  56.152 +
  56.153 +    def test_conforms_to_ICSVAware(self):
  56.154 +
  56.155 +        from Products.GenericSetup.interfaces import ICSVAware
  56.156 +        from zope.interface.verify import verifyClass
  56.157 +
  56.158 +        verifyClass( ICSVAware, self._getTargetClass() )
  56.159 +
  56.160 +class ConformsToIDAVAware:
  56.161 +
  56.162 +    def test_conforms_to_IDAVAware(self):
  56.163 +
  56.164 +        from Products.GenericSetup.interfaces import IDAVAware
  56.165 +        from zope.interface.verify import verifyClass
  56.166 +
  56.167 +        verifyClass( IDAVAware, self._getTargetClass() )
    57.1 new file mode 100644
    57.2 --- /dev/null
    57.3 +++ b/tests/default_profile/export_steps.xml
    57.4 @@ -0,0 +1,8 @@
    57.5 +<?xml version="1.0"?>
    57.6 +<export-steps>
    57.7 + <export-step id="one"
    57.8 +                handler="Products.GenericSetup.tests.common.dummy_handler"
    57.9 +                title="One Step">
   57.10 +  One small step
   57.11 + </export-step>
   57.12 +</export-steps>
    58.1 new file mode 100644
    58.2 --- /dev/null
    58.3 +++ b/tests/default_profile/import_steps.xml
    58.4 @@ -0,0 +1,9 @@
    58.5 +<?xml version="1.0"?>
    58.6 +<import-steps>
    58.7 + <import-step id="one"
    58.8 +             version="1"
    58.9 +             handler="Products.GenericSetup.tests.common.dummy_handler"
   58.10 +             title="One Step">
   58.11 +  One small step
   58.12 + </import-step>
   58.13 +</import-steps>
    59.1 new file mode 100644
    59.2 --- /dev/null
    59.3 +++ b/tests/default_profile/profile.ini
    59.4 @@ -0,0 +1,2 @@
    59.5 +[Metadata]
    59.6 +Title=Unit Test Profile Data
    60.1 new file mode 100644
    60.2 --- /dev/null
    60.3 +++ b/tests/default_profile/toolset.xml
    60.4 @@ -0,0 +1,6 @@
    60.5 +<?xml version="1.0"?>
    60.6 +<tool-setup>
    60.7 + <forbidden tool_id="doomed" />
    60.8 + <required tool_id="mandatory" class="path.to.one" />
    60.9 + <required tool_id="obligatory" class="path.to.another" />
   60.10 +</tool-setup>
    61.1 new file mode 100644
    61.2 --- /dev/null
    61.3 +++ b/tests/faux_objects.py
    61.4 @@ -0,0 +1,82 @@
    61.5 +""" Simple, importable content classes.
    61.6 +
    61.7 +$Id: faux_objects.py 40140 2005-11-15 18:53:19Z tseaver $
    61.8 +"""
    61.9 +from OFS.Folder import Folder
   61.10 +from OFS.PropertyManager import PropertyManager
   61.11 +from OFS.SimpleItem import SimpleItem
   61.12 +from zope.interface import implements
   61.13 +
   61.14 +try:
   61.15 +    from OFS.interfaces import IObjectManager
   61.16 +    from OFS.interfaces import ISimpleItem
   61.17 +    from OFS.interfaces import IPropertyManager
   61.18 +except ImportError: # BBB
   61.19 +    from Products.Five.interfaces import IObjectManager
   61.20 +    from Products.Five.interfaces import ISimpleItem
   61.21 +    from Products.Five.interfaces import IPropertyManager
   61.22 +
   61.23 +class TestSimpleItem(SimpleItem):
   61.24 +    implements(ISimpleItem)
   61.25 +
   61.26 +class TestSimpleItemWithProperties(SimpleItem, PropertyManager):
   61.27 +    implements(ISimpleItem, IPropertyManager)
   61.28 +
   61.29 +KNOWN_CSV = """\
   61.30 +one,two,three
   61.31 +four,five,six
   61.32 +"""
   61.33 +
   61.34 +from Products.GenericSetup.interfaces import ICSVAware
   61.35 +class TestCSVAware(SimpleItem):
   61.36 +    implements(ICSVAware)
   61.37 +    _was_put = None
   61.38 +    _csv = KNOWN_CSV
   61.39 +
   61.40 +    def as_csv(self):
   61.41 +        return self._csv
   61.42 +
   61.43 +    def put_csv(self, text):
   61.44 +        self._was_put = text
   61.45 +
   61.46 +KNOWN_INI = """\
   61.47 +[DEFAULT]
   61.48 +title = %s
   61.49 +description = %s
   61.50 +"""
   61.51 +
   61.52 +from Products.GenericSetup.interfaces import IINIAware
   61.53 +class TestINIAware(SimpleItem):
   61.54 +    implements(IINIAware)
   61.55 +    _was_put = None
   61.56 +    title = 'INI title'
   61.57 +    description = 'INI description'
   61.58 +
   61.59 +    def as_ini(self):
   61.60 +        return KNOWN_INI % (self.title, self.description)
   61.61 +
   61.62 +    def put_ini(self, text):
   61.63 +        self._was_put = text
   61.64 +
   61.65 +KNOWN_DAV = """\
   61.66 +Title: %s
   61.67 +Description: %s
   61.68 +
   61.69 +%s
   61.70 +"""
   61.71 +
   61.72 +from Products.GenericSetup.interfaces import IDAVAware
   61.73 +class TestDAVAware(SimpleItem):
   61.74 +    implements(IDAVAware)
   61.75 +    _was_put = None
   61.76 +    title = 'DAV title'
   61.77 +    description = 'DAV description'
   61.78 +    body = 'DAV body'
   61.79 +
   61.80 +    def manage_FTPget(self):
   61.81 +        return KNOWN_DAV % (self.title, self.description, self.body)
   61.82 +
   61.83 +    def PUT(self, REQUEST, RESPONSE):
   61.84 +        self._was_put = REQUEST.get('BODY', '')
   61.85 +        stream = REQUEST.get('BODYFILE', None)
   61.86 +        self._was_put_as_read = stream.read()
    62.1 new file mode 100644
    62.2 index 0000000000000000000000000000000000000000..a28a1350ebef24c6f6a393faf5056c07d119cf44
    62.3 GIT binary patch
    62.4 literal 840
    62.5 zc$@)91GoH%P)<h;3K|Lk000e1NJLTq002Ay001or1^@s6@^%PJ00006VoOIv0RI60
    62.6 z0RN!9r;`8x010qNS#tmY3labT3lag+-G2N4000McNliru(h46BC=K~o-Npa_0@X=G
    62.7 zK~#90?O4%q+b|4$>U9jNb12zE-9zmhO7?K}kTZvzJ<tyU&=W}8O)r*nDc_yX6bXR@
    62.8 z2!eP5Dk>@}zBZ)O-vN*bkaX`AU5504jLSdJ6a+c|J%Bb0905E4I4%Fp<82rE5s)50
    62.9 z0-ER2U{X(X)Hmo(_WJ7pnax|D8<GH#O9EqVh#VH9|60i>g7nM!&kIPwU<o_kFXz_V
   62.10 zbC6GKzsseqGwnHB+#H;1(Ix9VnXWN6$_y3R>bq*Nbop+|L4P_C8_b4SB-r9a*5sB&
   62.11 z%|Yc;km<;%QO5_;J8063PO|r|9le-s&BL3%iHr&%BlA{=MTex|7?N{mL`P3yPL2)<
   62.12 zMkN~K_uhN-;!Z>*mdt$NJQ+m?J0=?MixzTZi~ydq;AC%)>w9rME54OF6&-+=?PNHH
   62.13 z@uPaU2k=H?J%A@_zfk*C^=Sw<KXm*T=~Z1ataJKxY0p8|W(|pSY$Er2*;W|<JOa(z
   62.14 zGEwuYO4#65jj}!pEwn)MlMFcNf*(}IcIgD*x3*2bT^8?QZn@!Eb+Kc3tX=wio}=19
   62.15 zo0c1zC^1=bGDEnn=f$5jggMMzCuLvL31H34;X0IwjsS(X-2g6EN`8g+Dk>_@L;C#8
   62.16 z0`N-b)^qO$FW;#2^8_|*E1sLpKVO&oxXGv%_Hs{++#@^gnuSX>s$WUx3VE)Mi~vlb
   62.17 z!qSq`uPd(NBC!Z%>`hT(A^!D)cfQwMr|`?_-B}H_uM%2UOTzk9B@&CE<K~bd_pYC1
   62.18 zCJ$z0q;p(IT&j}`#KQXnz1$sKK>_gMovSlo>y*sE5?Kkv0?2jV1sOFQk+Y}3uZ_!k
   62.19 z$VMH^jb*Fr-*JXKya%FesA0B59{`>4XrJ$vF&w}Ni0N>dKrF-<S{vru6Qu;=WMW!(
   62.20 z#L2sz=b6Sr0dtmEr2Hn>4Sv1c`#jwezH@f(er01TDk>@}Dk>@}Dk>`eBm4or%ud3<
   62.21 Sil1%(0000<MNUMnLSTZVwtlDp
   62.22 
    63.1 new file mode 100644
    63.2 --- /dev/null
    63.3 +++ b/tests/test_content.py
    63.4 @@ -0,0 +1,897 @@
    63.5 +##############################################################################
    63.6 +#
    63.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    63.8 +#
    63.9 +# This software is subject to the provisions of the Zope Public License,
   63.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   63.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   63.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   63.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   63.14 +# FOR A PARTICULAR PURPOSE.
   63.15 +#
   63.16 +##############################################################################
   63.17 +"""Filesystem exporter / importer adapter unit tests.
   63.18 +
   63.19 +$Id: test_content.py 40234 2005-11-18 21:22:13Z tseaver $
   63.20 +"""
   63.21 +
   63.22 +import unittest
   63.23 +import Testing
   63.24 +import Zope2
   63.25 +Zope2.startup()
   63.26 +
   63.27 +from csv import reader
   63.28 +from ConfigParser import ConfigParser
   63.29 +from StringIO import StringIO
   63.30 +
   63.31 +try:
   63.32 +    from OFS.interfaces import IObjectManager
   63.33 +    from OFS.interfaces import ISimpleItem
   63.34 +    from OFS.interfaces import IPropertyManager
   63.35 +except ImportError: # BBB
   63.36 +    from Products.Five.interfaces import IObjectManager
   63.37 +    from Products.Five.interfaces import ISimpleItem
   63.38 +    from Products.Five.interfaces import IPropertyManager
   63.39 +
   63.40 +from Products.GenericSetup.testing import PlacelessSetup
   63.41 +from Products.GenericSetup.tests.common import DummyExportContext
   63.42 +from Products.GenericSetup.tests.common import DummyImportContext
   63.43 +
   63.44 +from conformance import ConformsToIINIAware
   63.45 +from conformance import ConformsToIFilesystemExporter
   63.46 +from conformance import ConformsToIFilesystemImporter
   63.47 +
   63.48 +class SimpleINIAwareTests(unittest.TestCase, ConformsToIINIAware):
   63.49 +
   63.50 +    def _getTargetClass(self):
   63.51 +        from Products.GenericSetup.content import SimpleINIAware
   63.52 +        return SimpleINIAware
   63.53 +
   63.54 +    def test_as_ini_no_properties(self):
   63.55 +        context = _makePropertied('no_properties')
   63.56 +        context._properties = ()
   63.57 +        adapter = self._getTargetClass()(context)
   63.58 +        text = adapter.as_ini()
   63.59 +        parser = ConfigParser()
   63.60 +        parser.readfp(StringIO(text))
   63.61 +        self.failIf(parser.sections())
   63.62 +        default_options = parser.defaults()
   63.63 +        self.assertEqual(len(default_options), 0)
   63.64 +
   63.65 +    def test_as_ini_string_property(self):
   63.66 +        TITLE = 'String Property'
   63.67 +        DESCR = 'Another property'
   63.68 +        context = _makePropertied('string_property')
   63.69 +        context.title = TITLE
   63.70 +        context._setProperty('description', DESCR)
   63.71 +        adapter = self._getTargetClass()(context)
   63.72 +        text = adapter.as_ini()
   63.73 +        parser = ConfigParser()
   63.74 +        parser.readfp(StringIO(text))
   63.75 +        self.failIf(parser.sections())
   63.76 +        default_options = parser.defaults()
   63.77 +        self.assertEqual(len(default_options), 2)
   63.78 +        self.assertEqual(default_options['title'].strip(), TITLE)
   63.79 +        self.assertEqual(default_options['description'].strip(), DESCR)
   63.80 +
   63.81 +    def test_as_ini_other_properties(self):
   63.82 +        from DateTime.DateTime import DateTime
   63.83 +        INTPROP = 42
   63.84 +        FLOATPROP = 3.1415926
   63.85 +        DATESTR = '2005-11-07T12:00:00.000Z'
   63.86 +        context = _makePropertied('string_property')
   63.87 +        context._properties = ()
   63.88 +        context._setProperty('int_prop', INTPROP, 'int')
   63.89 +        context._setProperty('float_prop', FLOATPROP, 'float')
   63.90 +        context._setProperty('date_prop', DateTime(DATESTR), 'date')
   63.91 +        adapter = self._getTargetClass()(context)
   63.92 +        text = adapter.as_ini()
   63.93 +        parser = ConfigParser()
   63.94 +        parser.readfp(StringIO(text))
   63.95 +        self.failIf(parser.sections())
   63.96 +        default_options = parser.defaults()
   63.97 +        self.assertEqual(len(default_options), 3)
   63.98 +        self.assertEqual(default_options['int_prop'], str(INTPROP))
   63.99 +        self.assertEqual(default_options['float_prop'], str(FLOATPROP))
  63.100 +        self.assertEqual(default_options['date_prop'], str(DateTime(DATESTR)))
  63.101 +
  63.102 +    def test_put_ini_empty(self):
  63.103 +        context = _makePropertied('empty_ini')
  63.104 +        adapter = self._getTargetClass()(context)
  63.105 +        context._properties = ()
  63.106 +        self.failIf(context.propertyItems())
  63.107 +        adapter.put_ini('')
  63.108 +        self.failIf(context.propertyItems())
  63.109 +
  63.110 +    def test_put_ini_with_values_stripped(self):
  63.111 +        context = _makePropertied('empty_ini')
  63.112 +        adapter = self._getTargetClass()(context)
  63.113 +        adapter.put_ini('[DEFAULT]\ntitle = Foo \ndescription = bar ')
  63.114 +        props = context.propdict()
  63.115 +        self.assertEqual(len(props), 2)
  63.116 +        self.failUnless('title' in props)
  63.117 +        self.failUnless('description' in props)
  63.118 +        self.assertEqual(context.title, 'Foo')
  63.119 +        self.assertEqual(context.description, 'bar')
  63.120 +
  63.121 +    def test_put_ini_other_properties(self):
  63.122 +        from DateTime.DateTime import DateTime
  63.123 +        INTPROP = 42
  63.124 +        FLOATPROP = 3.1415926
  63.125 +        DATESTR = '2005-11-07T12:00:00.000Z'
  63.126 +        DATESTR2 = '2005-11-09T12:00:00.000Z'
  63.127 +        context = _makePropertied('string_property')
  63.128 +        context._properties = ()
  63.129 +        context._setProperty('int_prop', INTPROP, 'int')
  63.130 +        context._setProperty('float_prop', FLOATPROP, 'float')
  63.131 +        context._setProperty('date_prop', DateTime(DATESTR), 'date')
  63.132 +        adapter = self._getTargetClass()(context)
  63.133 +        adapter.put_ini('''\
  63.134 +[DEFAULT]
  63.135 +int_prop = 13 
  63.136 +\nfloat_prop = 2.818
  63.137 +\ndate_prop = %s''' % DATESTR2)
  63.138 +        self.assertEqual(len(context.propertyIds()), 3)
  63.139 +        self.assertEqual(context.int_prop, 13)
  63.140 +        self.assertEqual(context.float_prop, 2.818)
  63.141 +        self.assertEqual(context.date_prop, DateTime(DATESTR2))
  63.142 +
  63.143 +class FolderishExporterImporterTests(PlacelessSetup,
  63.144 +                                     unittest.TestCase,
  63.145 +                                    ):
  63.146 +
  63.147 +    def _getExporter(self):
  63.148 +        from Products.GenericSetup.content import exportSiteStructure
  63.149 +        return exportSiteStructure
  63.150 +
  63.151 +    def _getImporter(self):
  63.152 +        from Products.GenericSetup.content import importSiteStructure
  63.153 +        return importSiteStructure
  63.154 +
  63.155 +    def _makeSetupTool(self):
  63.156 +        from Products.GenericSetup.tool import SetupTool
  63.157 +        return SetupTool('portal_setup')
  63.158 +
  63.159 +    def _setUpAdapters(self):
  63.160 +        from OFS.Folder import Folder
  63.161 +        from zope.app.tests import ztapi
  63.162 +        #from OFS.Image import File
  63.163 +
  63.164 +        from Products.GenericSetup.interfaces import IFilesystemExporter
  63.165 +        from Products.GenericSetup.interfaces import IFilesystemImporter
  63.166 +        from Products.GenericSetup.interfaces import ICSVAware
  63.167 +        from Products.GenericSetup.interfaces import IINIAware
  63.168 +        from Products.GenericSetup.interfaces import IDAVAware
  63.169 +
  63.170 +        from Products.GenericSetup.content import \
  63.171 +             SimpleINIAware
  63.172 +        from Products.GenericSetup.content import \
  63.173 +             FolderishExporterImporter
  63.174 +        from Products.GenericSetup.content import \
  63.175 +             CSVAwareFileAdapter
  63.176 +        from Products.GenericSetup.content import \
  63.177 +             INIAwareFileAdapter
  63.178 +        from Products.GenericSetup.content import \
  63.179 +             DAVAwareFileAdapter
  63.180 +
  63.181 +        ztapi.provideAdapter(IObjectManager,
  63.182 +                             IFilesystemExporter,
  63.183 +                             FolderishExporterImporter,
  63.184 +                            )
  63.185 +
  63.186 +        ztapi.provideAdapter(IObjectManager,
  63.187 +                             IFilesystemImporter,
  63.188 +                             FolderishExporterImporter,
  63.189 +                            )
  63.190 +
  63.191 +        ztapi.provideAdapter(IPropertyManager,
  63.192 +                             IINIAware,
  63.193 +                             SimpleINIAware,
  63.194 +                            )
  63.195 +
  63.196 +        ztapi.provideAdapter(ICSVAware,
  63.197 +                             IFilesystemExporter,
  63.198 +                             CSVAwareFileAdapter,
  63.199 +                            )
  63.200 +
  63.201 +        ztapi.provideAdapter(ICSVAware,
  63.202 +                             IFilesystemImporter,
  63.203 +                             CSVAwareFileAdapter,
  63.204 +                            )
  63.205 +
  63.206 +        ztapi.provideAdapter(IINIAware,
  63.207 +                             IFilesystemExporter,
  63.208 +                             INIAwareFileAdapter,
  63.209 +                            )
  63.210 +
  63.211 +        ztapi.provideAdapter(IINIAware,
  63.212 +                             IFilesystemImporter,
  63.213 +                             INIAwareFileAdapter,
  63.214 +                            )
  63.215 +
  63.216 +        ztapi.provideAdapter(IDAVAware,
  63.217 +                             IFilesystemExporter,
  63.218 +                             DAVAwareFileAdapter,
  63.219 +                            )
  63.220 +
  63.221 +        ztapi.provideAdapter(IDAVAware,
  63.222 +                             IFilesystemImporter,
  63.223 +                             DAVAwareFileAdapter,
  63.224 +                            )
  63.225 +
  63.226 +
  63.227 +    def test_export_empty_site(self):
  63.228 +        self._setUpAdapters()
  63.229 +        site = _makeFolder('site')
  63.230 +        site.title = 'test_export_empty_site'
  63.231 +        site.description = 'Testing export of an empty site.'
  63.232 +        context = DummyExportContext(site)
  63.233 +        exporter = self._getExporter()
  63.234 +        exporter(context)
  63.235 +
  63.236 +        self.assertEqual(len(context._wrote), 2)
  63.237 +        filename, text, content_type = context._wrote[0]
  63.238 +        self.assertEqual(filename, 'structure/.objects')
  63.239 +        self.assertEqual(content_type, 'text/comma-separated-values')
  63.240 +
  63.241 +        objects = [x for x in reader(StringIO(text))]
  63.242 +        self.assertEqual(len(objects), 0)
  63.243 +
  63.244 +        filename, text, content_type = context._wrote[1]
  63.245 +        self.assertEqual(filename, 'structure/.properties')
  63.246 +        self.assertEqual(content_type, 'text/plain')
  63.247 +
  63.248 +        parser = ConfigParser()
  63.249 +        parser.readfp(StringIO(text))
  63.250 +
  63.251 +        defaults = parser.defaults()
  63.252 +        self.assertEqual(len(defaults), 1)
  63.253 +        self.assertEqual(defaults['title'], site.title)
  63.254 +
  63.255 +    def test_export_empty_site_with_setup_tool(self):
  63.256 +        self._setUpAdapters()
  63.257 +        site = _makeFolder('site')
  63.258 +        site._setObject('setup_tool', self._makeSetupTool())
  63.259 +        site._updateProperty('title', 'test_export_empty_site_with_setup_tool')
  63.260 +        site._setProperty('description',
  63.261 +                          'Testing export of an empty site with setup tool.')
  63.262 +        context = DummyExportContext(site)
  63.263 +        exporter = self._getExporter()
  63.264 +        exporter(context)
  63.265 +
  63.266 +        self.assertEqual(len(context._wrote), 2)
  63.267 +        filename, text, content_type = context._wrote[0]
  63.268 +        self.assertEqual(filename, 'structure/.objects')
  63.269 +        self.assertEqual(content_type, 'text/comma-separated-values')
  63.270 +
  63.271 +        objects = [x for x in reader(StringIO(text))]
  63.272 +        self.assertEqual(len(objects), 0)
  63.273 +
  63.274 +        filename, text, content_type = context._wrote[1]
  63.275 +        self.assertEqual(filename, 'structure/.properties')
  63.276 +        self.assertEqual(content_type, 'text/plain')
  63.277 +
  63.278 +        parser = ConfigParser()
  63.279 +        parser.readfp(StringIO(text))
  63.280 +
  63.281 +        defaults = parser.defaults()
  63.282 +        self.assertEqual(len(defaults), 2)
  63.283 +        self.assertEqual(defaults['title'], site.title)
  63.284 +        self.assertEqual(defaults['description'], site.description)
  63.285 +
  63.286 +    def test_export_site_with_non_exportable_simple_items(self):
  63.287 +        from Products.GenericSetup.utils import _getDottedName
  63.288 +        self._setUpAdapters()
  63.289 +        ITEM_IDS = ('foo', 'bar', 'baz')
  63.290 +
  63.291 +        site = _makeFolder('site')
  63.292 +        site.title = 'AAA'
  63.293 +        site._setProperty('description', 'BBB')
  63.294 +        item = _makeItem('aside')
  63.295 +        dotted = _getDottedName(item.__class__)
  63.296 +        for id in ITEM_IDS:
  63.297 +            site._setObject(id, _makeItem(id))
  63.298 +
  63.299 +        context = DummyExportContext(site)
  63.300 +        exporter = self._getExporter()
  63.301 +        exporter(context)
  63.302 +
  63.303 +        self.assertEqual(len(context._wrote), 2)
  63.304 +        filename, text, content_type = context._wrote[0]
  63.305 +        self.assertEqual(filename, 'structure/.objects')
  63.306 +        self.assertEqual(content_type, 'text/comma-separated-values')
  63.307 +
  63.308 +        objects = [x for x in reader(StringIO(text))]
  63.309 +        self.assertEqual(len(objects), 3)
  63.310 +        for index in range(len(ITEM_IDS)):
  63.311 +            self.assertEqual(objects[index][0], ITEM_IDS[index])
  63.312 +            self.assertEqual(objects[index][1], dotted)
  63.313 +
  63.314 +        filename, text, content_type = context._wrote[1]
  63.315 +        self.assertEqual(filename, 'structure/.properties')
  63.316 +        self.assertEqual(content_type, 'text/plain')
  63.317 +        parser = ConfigParser()
  63.318 +        parser.readfp(StringIO(text))
  63.319 +
  63.320 +        defaults = parser.defaults()
  63.321 +        self.assertEqual(len(defaults), 2)
  63.322 +        self.assertEqual(defaults['title'], 'AAA')
  63.323 +        self.assertEqual(defaults['description'], 'BBB')
  63.324 +
  63.325 +    def test_export_site_with_exportable_simple_items(self):
  63.326 +        from Products.GenericSetup.utils import _getDottedName
  63.327 +        self._setUpAdapters()
  63.328 +        ITEM_IDS = ('foo', 'bar', 'baz')
  63.329 +
  63.330 +        site = _makeFolder('site')
  63.331 +        site.title = 'AAA'
  63.332 +        site._setProperty('description', 'BBB')
  63.333 +        aware = _makeINIAware('aside')
  63.334 +        dotted = _getDottedName(aware.__class__)
  63.335 +        for id in ITEM_IDS:
  63.336 +            site._setObject(id, _makeINIAware(id))
  63.337 +
  63.338 +        context = DummyExportContext(site)
  63.339 +        exporter = self._getExporter()
  63.340 +        exporter(context)
  63.341 +
  63.342 +        self.assertEqual(len(context._wrote), 2 + len(ITEM_IDS))
  63.343 +        filename, text, content_type = context._wrote[0]
  63.344 +        self.assertEqual(filename, 'structure/.objects')
  63.345 +        self.assertEqual(content_type, 'text/comma-separated-values')
  63.346 +
  63.347 +        objects = [x for x in reader(StringIO(text))]
  63.348 +        self.assertEqual(len(objects), 3)
  63.349 +        for index in range(len(ITEM_IDS)):
  63.350 +            self.assertEqual(objects[index][0], ITEM_IDS[index])
  63.351 +            self.assertEqual(objects[index][1], dotted)
  63.352 +
  63.353 +            filename, text, content_type = context._wrote[index+2]
  63.354 +            self.assertEqual(filename, 'structure/%s.ini' % ITEM_IDS[index])
  63.355 +            object = site._getOb(ITEM_IDS[index])
  63.356 +            self.assertEqual(text.strip(),
  63.357 +                             object.as_ini().strip())
  63.358 +            self.assertEqual(content_type, 'text/plain')
  63.359 +
  63.360 +        filename, text, content_type = context._wrote[1]
  63.361 +        self.assertEqual(filename, 'structure/.properties')
  63.362 +        self.assertEqual(content_type, 'text/plain')
  63.363 +        parser = ConfigParser()
  63.364 +        parser.readfp(StringIO(text))
  63.365 +
  63.366 +        defaults = parser.defaults()
  63.367 +        self.assertEqual(len(defaults), 2)
  63.368 +        self.assertEqual(defaults['title'], 'AAA')
  63.369 +        self.assertEqual(defaults['description'], 'BBB')
  63.370 +
  63.371 +    def test_export_site_with_subfolders(self):
  63.372 +        from Products.GenericSetup.utils import _getDottedName
  63.373 +        self._setUpAdapters()
  63.374 +        FOLDER_IDS = ('foo', 'bar', 'baz')
  63.375 +
  63.376 +        site = _makeFolder('site')
  63.377 +        site.title = 'AAA'
  63.378 +        site._setProperty('description', 'BBB')
  63.379 +        aside = _makeFolder('aside')
  63.380 +        dotted = _getDottedName(aside.__class__)
  63.381 +        for id in FOLDER_IDS:
  63.382 +            folder = _makeFolder(id)
  63.383 +            folder.title = 'Title: %s' % id
  63.384 +            site._setObject(id, folder)
  63.385 +
  63.386 +        context = DummyExportContext(site)
  63.387 +        exporter = self._getExporter()
  63.388 +        exporter(context)
  63.389 +
  63.390 +        self.assertEqual(len(context._wrote), 2 + (2 *len(FOLDER_IDS)))
  63.391 +        filename, text, content_type = context._wrote[0]
  63.392 +        self.assertEqual(filename, 'structure/.objects')
  63.393 +        self.assertEqual(content_type, 'text/comma-separated-values')
  63.394 +
  63.395 +        objects = [x for x in reader(StringIO(text))]
  63.396 +        self.assertEqual(len(objects), 3)
  63.397 +
  63.398 +        for index in range(len(FOLDER_IDS)):
  63.399 +            id = FOLDER_IDS[index]
  63.400 +            self.assertEqual(objects[index][0], id)
  63.401 +            self.assertEqual(objects[index][1], dotted)
  63.402 +
  63.403 +            filename, text, content_type = context._wrote[2 + (2 * index)]
  63.404 +            self.assertEqual(filename, '/'.join(('structure', id, '.objects')))
  63.405 +            self.assertEqual(content_type, 'text/comma-separated-values')
  63.406 +            subobjects = [x for x in reader(StringIO(text))]
  63.407 +            self.assertEqual(len(subobjects), 0)
  63.408 +
  63.409 +            filename, text, content_type = context._wrote[2 + (2 * index) + 1]
  63.410 +            self.assertEqual(filename,
  63.411 +                             '/'.join(('structure', id, '.properties')))
  63.412 +            self.assertEqual(content_type, 'text/plain')
  63.413 +            parser = ConfigParser()
  63.414 +            parser.readfp(StringIO(text))
  63.415 +
  63.416 +            defaults = parser.defaults()
  63.417 +            self.assertEqual(len(defaults), 1)
  63.418 +            self.assertEqual(defaults['title'], 'Title: %s' % id)
  63.419 +
  63.420 +        filename, text, content_type = context._wrote[1]
  63.421 +        self.assertEqual(filename, 'structure/.properties')
  63.422 +        self.assertEqual(content_type, 'text/plain')
  63.423 +
  63.424 +        parser = ConfigParser()
  63.425 +        parser.readfp(StringIO(text))
  63.426 +
  63.427 +        defaults = parser.defaults()
  63.428 +        self.assertEqual(len(defaults), 2)
  63.429 +        self.assertEqual(defaults['title'], 'AAA')
  63.430 +        self.assertEqual(defaults['description'], 'BBB')
  63.431 +
  63.432 +    def test_export_site_with_csvaware(self):
  63.433 +        from Products.GenericSetup.utils import _getDottedName
  63.434 +        self._setUpAdapters()
  63.435 +
  63.436 +        site = _makeFolder('site')
  63.437 +        site.title = 'test_export_site_with_csvaware'
  63.438 +        site._setProperty('description',
  63.439 +                          'Testing export of an site with CSV-aware content.')
  63.440 +
  63.441 +        aware = _makeCSVAware('aware')
  63.442 +        site._setObject('aware', aware)
  63.443 +
  63.444 +        context = DummyExportContext(site)
  63.445 +        exporter = self._getExporter()
  63.446 +        exporter(context)
  63.447 +
  63.448 +        self.assertEqual(len(context._wrote), 3)
  63.449 +        filename, text, content_type = context._wrote[0]
  63.450 +        self.assertEqual(filename, 'structure/.objects')
  63.451 +        self.assertEqual(content_type, 'text/comma-separated-values')
  63.452 +
  63.453 +        objects = [x for x in reader(StringIO(text))]
  63.454 +        self.assertEqual(len(objects), 1)
  63.455 +        self.assertEqual(objects[0][0], 'aware')
  63.456 +        self.assertEqual(objects[0][1], _getDottedName(aware.__class__))
  63.457 +
  63.458 +        filename, text, content_type = context._wrote[1]
  63.459 +        self.assertEqual(filename, 'structure/.properties')
  63.460 +        self.assertEqual(content_type, 'text/plain')
  63.461 +
  63.462 +        parser = ConfigParser()
  63.463 +        parser.readfp(StringIO(text))
  63.464 +
  63.465 +        defaults = parser.defaults()
  63.466 +        self.assertEqual(len(defaults), 2)
  63.467 +        self.assertEqual(defaults['title'], site.title)
  63.468 +        self.assertEqual(defaults['description'], site.description)
  63.469 +
  63.470 +        filename, text, content_type = context._wrote[2]
  63.471 +        self.assertEqual(filename, 'structure/aware.csv')
  63.472 +        self.assertEqual(content_type, 'text/comma-separated-values')
  63.473 +        rows = [x for x in reader(StringIO(text))]
  63.474 +        self.assertEqual(len(rows), 2)
  63.475 +        self.assertEqual(rows[0][0], 'one')
  63.476 +        self.assertEqual(rows[0][1], 'two')
  63.477 +        self.assertEqual(rows[0][2], 'three')
  63.478 +        self.assertEqual(rows[1][0], 'four')
  63.479 +        self.assertEqual(rows[1][1], 'five')
  63.480 +        self.assertEqual(rows[1][2], 'six')
  63.481 +
  63.482 +    def test_import_empty_site(self):
  63.483 +        self._setUpAdapters()
  63.484 +        site = _makeFolder('site')
  63.485 +        context = DummyImportContext(site)
  63.486 +        context._files['structure/.objects'] = ''
  63.487 +        importer = self._getImporter()
  63.488 +        self.assertEqual(len(site.objectIds()), 0)
  63.489 +        importer(context)
  63.490 +        self.assertEqual(len(site.objectIds()), 0)
  63.491 +
  63.492 +    def test_import_empty_site_with_setup_tool(self):
  63.493 +        self._setUpAdapters()
  63.494 +        site = _makeFolder('site')
  63.495 +        site._setObject('setup_tool', self._makeSetupTool())
  63.496 +        context = DummyImportContext(site)
  63.497 +        importer = self._getImporter()
  63.498 +
  63.499 +        self.assertEqual(len(site.objectIds()), 1)
  63.500 +        self.assertEqual(site.objectIds()[0], 'setup_tool')
  63.501 +        importer(context)
  63.502 +        self.assertEqual(len(site.objectIds()), 1)
  63.503 +        self.assertEqual(site.objectIds()[0], 'setup_tool')
  63.504 +
  63.505 +    def test_import_site_with_subfolders(self):
  63.506 +        from Products.GenericSetup.utils import _getDottedName
  63.507 +        self._setUpAdapters()
  63.508 +        FOLDER_IDS = ('foo', 'bar', 'baz')
  63.509 +
  63.510 +        site = _makeFolder('site')
  63.511 +        dotted = _getDottedName(site.__class__)
  63.512 +
  63.513 +        context = DummyImportContext(site)
  63.514 +
  63.515 +        for id in FOLDER_IDS:
  63.516 +            context._files['structure/%s/.objects' % id] = ''
  63.517 +            context._files['structure/%s/.properties' % id] = (
  63.518 +                _PROPERTIES_TEMPLATE % id )
  63.519 +
  63.520 +        _ROOT_OBJECTS = '\n'.join(['%s,%s' % (id, dotted)
  63.521 +                                        for id in FOLDER_IDS])
  63.522 +
  63.523 +        context._files['structure/.objects'] = _ROOT_OBJECTS
  63.524 +        context._files['structure/.properties'] = (
  63.525 +                _PROPERTIES_TEMPLATE % 'Test Site')
  63.526 +
  63.527 +        importer = self._getImporter()
  63.528 +        importer(context)
  63.529 +
  63.530 +        content = site.objectValues()
  63.531 +        self.assertEqual(len(content), len(FOLDER_IDS))
  63.532 +
  63.533 +    def test_import_site_with_subitems(self):
  63.534 +        from Products.GenericSetup.utils import _getDottedName
  63.535 +        from faux_objects import KNOWN_INI
  63.536 +        from faux_objects import TestINIAware
  63.537 +        dotted = _getDottedName(TestINIAware)
  63.538 +        self._setUpAdapters()
  63.539 +        ITEM_IDS = ('foo', 'bar', 'baz')
  63.540 +
  63.541 +        site = _makeFolder('site')
  63.542 +
  63.543 +        context = DummyImportContext(site)
  63.544 +        # We want to add 'baz' to 'foo', without losing 'bar'
  63.545 +        context._files['structure/.objects'] = '\n'.join(
  63.546 +                            ['%s,%s' % (x, dotted) for x in ITEM_IDS])
  63.547 +        for index in range(len(ITEM_IDS)):
  63.548 +            id = ITEM_IDS[index]
  63.549 +            context._files[
  63.550 +                    'structure/%s.ini' % id] = KNOWN_INI % ('Title: %s' % id,
  63.551 +                                                            'xyzzy',
  63.552 +                                                           )
  63.553 +        importer = self._getImporter()
  63.554 +        importer(context)
  63.555 +
  63.556 +        after = site.objectIds()
  63.557 +        self.assertEqual(len(after), len(ITEM_IDS))
  63.558 +        for found_id, expected_id in zip(after, ITEM_IDS):
  63.559 +            self.assertEqual(found_id, expected_id)
  63.560 +
  63.561 +    def test_import_site_with_subitem_unknown_portal_type(self):
  63.562 +        from faux_objects import KNOWN_INI
  63.563 +        self._setUpAdapters()
  63.564 +        ITEM_IDS = ('foo', 'bar', 'baz')
  63.565 +
  63.566 +        site = _makeFolder('site')
  63.567 +
  63.568 +        context = DummyImportContext(site)
  63.569 +        # We want to add 'baz' to 'foo', without losing 'bar'
  63.570 +        context._files['structure/.objects'] = '\n'.join(
  63.571 +                                ['%s,Unknown Type' % x for x in ITEM_IDS])
  63.572 +        for index in range(len(ITEM_IDS)):
  63.573 +            id = ITEM_IDS[index]
  63.574 +            context._files[
  63.575 +                    'structure/%s.ini' % id] = KNOWN_INI % ('Title: %s' % id,
  63.576 +                                                            'xyzzy',
  63.577 +                                                           )
  63.578 +
  63.579 +        importer = self._getImporter()
  63.580 +        importer(context)
  63.581 +
  63.582 +        after = site.objectIds()
  63.583 +        self.assertEqual(len(after), 0)
  63.584 +        self.assertEqual(len(context._notes), len(ITEM_IDS))
  63.585 +        for level, component, message in context._notes:
  63.586 +            self.assertEqual(component, 'SFWA')
  63.587 +            self.failUnless(message.startswith("Couldn't make"))
  63.588 +
  63.589 +    def test_import_site_with_subitems_and_no_preserve(self):
  63.590 +        self._setUpAdapters()
  63.591 +        ITEM_IDS = ('foo', 'bar', 'baz')
  63.592 +
  63.593 +        site = _makeFolder('site')
  63.594 +        for id in ITEM_IDS:
  63.595 +            site._setObject(id, _makeItem(id))
  63.596 +
  63.597 +        context = DummyImportContext(site)
  63.598 +        # We want to add 'baz' to 'foo', without losing 'bar'
  63.599 +        context._files['structure/.objects'] = ''
  63.600 +
  63.601 +        importer = self._getImporter()
  63.602 +        importer(context)
  63.603 +
  63.604 +        self.assertEqual(len(site.objectIds()), 0)
  63.605 +
  63.606 +    def test_import_site_with_subitemss_and_preserve(self):
  63.607 +        self._setUpAdapters()
  63.608 +        ITEM_IDS = ('foo', 'bar', 'baz')
  63.609 +
  63.610 +        site = _makeFolder('site')
  63.611 +        for id in ITEM_IDS:
  63.612 +            site._setObject(id, _makeItem(id))
  63.613 +
  63.614 +        context = DummyImportContext(site)
  63.615 +        # We want to add 'baz' to 'foo', without losing 'bar'
  63.616 +        context._files['structure/.objects'] = ''
  63.617 +        context._files['structure/.preserve'] = '*'
  63.618 +
  63.619 +        importer = self._getImporter()
  63.620 +        importer(context)
  63.621 +
  63.622 +        after = site.objectIds()
  63.623 +        self.assertEqual(len(after), len(ITEM_IDS))
  63.624 +        for i in range(len(ITEM_IDS)):
  63.625 +            self.assertEqual(after[i], ITEM_IDS[i])
  63.626 +
  63.627 +    def test_import_site_with_subitemss_and_preserve_partial(self):
  63.628 +        self._setUpAdapters()
  63.629 +        ITEM_IDS = ('foo', 'bar', 'baz')
  63.630 +
  63.631 +        site = _makeFolder('site')
  63.632 +        for id in ITEM_IDS:
  63.633 +            site._setObject(id, _makeItem(id))
  63.634 +
  63.635 +        context = DummyImportContext(site)
  63.636 +        # We want to add 'baz' to 'foo', without losing 'bar'
  63.637 +        context._files['structure/.objects'] = ''
  63.638 +        context._files['structure/.preserve'] = 'b*'
  63.639 +
  63.640 +        importer = self._getImporter()
  63.641 +        importer(context)
  63.642 +
  63.643 +        after = site.objectIds()
  63.644 +        self.assertEqual(len(after), 2)
  63.645 +        self.assertEqual(after[0], 'bar')
  63.646 +        self.assertEqual(after[1], 'baz')
  63.647 +
  63.648 +    def test_import_site_with_subfolders_and_preserve(self):
  63.649 +        from Products.GenericSetup.utils import _getDottedName
  63.650 +        self._setUpAdapters()
  63.651 +
  63.652 +        site = _makeFolder('site')
  63.653 +        site._setObject('foo', _makeFolder('foo'))
  63.654 +        foo = site._getOb('foo')
  63.655 +        foo._setObject('bar', _makeFolder('bar'))
  63.656 +        bar = foo._getOb('bar')
  63.657 +
  63.658 +        context = DummyImportContext(site)
  63.659 +        # We want to add 'baz' to 'foo', without losing 'bar'
  63.660 +        context._files['structure/.objects'
  63.661 +                      ] = 'foo,%s' % _getDottedName(foo.__class__)
  63.662 +        context._files['structure/.preserve'] = '*'
  63.663 +        context._files['structure/foo/.objects'
  63.664 +                      ] = 'baz,%s' % _getDottedName(bar.__class__)
  63.665 +        context._files['structure/foo/.preserve'] = '*'
  63.666 +        context._files['structure/foo/baz/.objects'] = ''
  63.667 +
  63.668 +        importer = self._getImporter()
  63.669 +        importer(context)
  63.670 +
  63.671 +        self.assertEqual(len(site.objectIds()), 1)
  63.672 +        self.assertEqual(site.objectIds()[0], 'foo')
  63.673 +
  63.674 +        self.assertEqual(len(foo.objectIds()), 2, site.foo.objectIds())
  63.675 +        self.assertEqual(foo.objectIds()[0], 'bar')
  63.676 +        self.assertEqual(foo.objectIds()[1], 'baz')
  63.677 +
  63.678 +
  63.679 +class Test_globpattern(unittest.TestCase):
  63.680 +
  63.681 +    NAMELIST = ('foo', 'bar', 'baz', 'bam', 'qux', 'quxx', 'quxxx')
  63.682 +
  63.683 +    def _checkResults(self, globpattern, namelist, expected):
  63.684 +        from Products.GenericSetup.content import _globtest
  63.685 +        found = _globtest(globpattern, namelist)
  63.686 +        self.assertEqual(len(found), len(expected))
  63.687 +        for found_item, expected_item in zip(found, expected):
  63.688 +            self.assertEqual(found_item, expected_item)
  63.689 +
  63.690 +    def test_star(self):
  63.691 +        self._checkResults('*', self.NAMELIST, self.NAMELIST)
  63.692 +
  63.693 +    def test_simple(self):
  63.694 +        self._checkResults('b*', self.NAMELIST,
  63.695 +                            [x for x in self.NAMELIST if x.startswith('b')])
  63.696 +
  63.697 +    def test_multiple(self):
  63.698 +        self._checkResults('b*\n*x', self.NAMELIST,
  63.699 +                            [x for x in self.NAMELIST
  63.700 +                                if x.startswith('b') or x.endswith('x')])
  63.701 +
  63.702 +
  63.703 +class CSVAwareFileAdapterTests(unittest.TestCase,
  63.704 +                               ConformsToIFilesystemExporter,
  63.705 +                               ConformsToIFilesystemImporter,
  63.706 +                              ):
  63.707 +
  63.708 +    def _getTargetClass(self):
  63.709 +        from Products.GenericSetup.content import CSVAwareFileAdapter
  63.710 +        return CSVAwareFileAdapter
  63.711 +
  63.712 +    def _makeOne(self, context, *args, **kw):
  63.713 +        return self._getTargetClass()(context, *args, **kw)
  63.714 +
  63.715 +    def test_export_with_known_CSV(self):
  63.716 +        from faux_objects import KNOWN_CSV
  63.717 +        sheet = _makeCSVAware('config')
  63.718 +
  63.719 +        adapter = self._makeOne(sheet)
  63.720 +        context = DummyExportContext(None)
  63.721 +        adapter.export(context, 'subpath/to/sheet')
  63.722 +
  63.723 +        self.assertEqual(len(context._wrote), 1)
  63.724 +        filename, text, content_type = context._wrote[0]
  63.725 +        self.assertEqual(filename, 'subpath/to/sheet/config.csv')
  63.726 +        self.assertEqual(content_type, 'text/comma-separated-values')
  63.727 +
  63.728 +        self.assertEqual(text.strip(), KNOWN_CSV.strip())
  63.729 +
  63.730 +    def test_import_with_known_CSV(self):
  63.731 +        ORIG_CSV = """\
  63.732 +one,two,three
  63.733 +four,five,six
  63.734 +"""
  63.735 +        NEW_CSV = """\
  63.736 +four,five,six
  63.737 +one,two,three
  63.738 +"""
  63.739 +        sheet = _makeCSVAware('config', ORIG_CSV)
  63.740 +
  63.741 +        adapter = self._makeOne(sheet)
  63.742 +        context = DummyImportContext(None)
  63.743 +        context._files['subpath/to/sheet/config.csv'] = NEW_CSV
  63.744 +        adapter.import_(context, 'subpath/to/sheet')
  63.745 +
  63.746 +        self.assertEqual(sheet._was_put.getvalue().strip(), NEW_CSV.strip())
  63.747 +
  63.748 +
  63.749 +_PROPERTIES_TEMPLATE = """
  63.750 +[DEFAULT]
  63.751 +Title = %s
  63.752 +Description = This is a test
  63.753 +"""
  63.754 +
  63.755 +class INIAwareFileAdapterTests(unittest.TestCase,
  63.756 +                               ConformsToIFilesystemExporter,
  63.757 +                               ConformsToIFilesystemImporter,
  63.758 +                               ):
  63.759 +
  63.760 +    def _getTargetClass(self):
  63.761 +        from Products.GenericSetup.content import INIAwareFileAdapter
  63.762 +        return INIAwareFileAdapter
  63.763 +
  63.764 +    def _makeOne(self, context, *args, **kw):
  63.765 +        return self._getTargetClass()(context, *args, **kw)
  63.766 +
  63.767 +    def test_export_ini_file(self):
  63.768 +        ini_file = _makeINIAware('ini_file.html')
  63.769 +        adapter = self._makeOne(ini_file)
  63.770 +        context = DummyExportContext(None)
  63.771 +        adapter.export(context, 'subpath/to')
  63.772 +
  63.773 +        self.assertEqual(len(context._wrote), 1)
  63.774 +        filename, text, content_type = context._wrote[0]
  63.775 +        self.assertEqual(filename, 'subpath/to/ini_file.html.ini')
  63.776 +        self.assertEqual(content_type, 'text/plain')
  63.777 +
  63.778 +        self.assertEqual(text.strip(), ini_file.as_ini().strip())
  63.779 +
  63.780 +    def test_import_ini_file(self):
  63.781 +        from faux_objects import KNOWN_INI
  63.782 +        ini_file = _makeINIAware('ini_file.html')
  63.783 +        adapter = self._makeOne(ini_file)
  63.784 +        context = DummyImportContext(None)
  63.785 +        context._files['subpath/to/ini_file.html.ini'] = (
  63.786 +                        KNOWN_INI % ('Title: ini_file', 'abc'))
  63.787 +
  63.788 +        adapter.import_(context, 'subpath/to')
  63.789 +        text = ini_file._was_put
  63.790 +        parser = ConfigParser()
  63.791 +        parser.readfp(StringIO(text))
  63.792 +        self.assertEqual(parser.get('DEFAULT', 'title'), 'Title: ini_file')
  63.793 +        self.assertEqual(parser.get('DEFAULT', 'description'), 'abc')
  63.794 +
  63.795 +
  63.796 +class DAVAwareFileAdapterTests(unittest.TestCase,
  63.797 +                               ConformsToIFilesystemExporter,
  63.798 +                               ConformsToIFilesystemImporter,
  63.799 +                               ):
  63.800 +
  63.801 +    def _getTargetClass(self):
  63.802 +        from Products.GenericSetup.content import DAVAwareFileAdapter
  63.803 +        return DAVAwareFileAdapter
  63.804 +
  63.805 +    def _makeOne(self, context, *args, **kw):
  63.806 +        return self._getTargetClass()(context, *args, **kw)
  63.807 +
  63.808 +    def test_export_dav_file(self):
  63.809 +        dav_file = _makeDAVAware('dav_file.html')
  63.810 +        adapter = self._makeOne(dav_file)
  63.811 +        context = DummyExportContext(None)
  63.812 +        adapter.export(context, 'subpath/to')
  63.813 +
  63.814 +        self.assertEqual(len(context._wrote), 1)
  63.815 +        filename, text, content_type = context._wrote[0]
  63.816 +        self.assertEqual(filename, 'subpath/to/dav_file.html')
  63.817 +        self.assertEqual(content_type, 'text/plain')
  63.818 +        self.assertEqual(text.strip(), dav_file.manage_FTPget().strip())
  63.819 +
  63.820 +    def test_import_dav_file(self):
  63.821 +        from faux_objects import KNOWN_DAV
  63.822 +        VALUES = ('Title: dav_file', 'Description: abc', 'body goes here')
  63.823 +        dav_file = _makeDAVAware('dav_file.html')
  63.824 +        adapter = self._makeOne(dav_file)
  63.825 +        context = DummyImportContext(None)
  63.826 +        context._files['subpath/to/dav_file.html'] = KNOWN_DAV % VALUES
  63.827 +
  63.828 +        adapter.import_(context, 'subpath/to')
  63.829 +        text = dav_file._was_put == KNOWN_DAV % VALUES
  63.830 +
  63.831 +
  63.832 +def _makePropertied(id):
  63.833 +    from faux_objects import TestSimpleItemWithProperties
  63.834 +
  63.835 +    propertied = TestSimpleItemWithProperties()
  63.836 +    propertied._setId(id)
  63.837 +
  63.838 +    return propertied
  63.839 +
  63.840 +def _makeCSVAware(id, csv=None):
  63.841 +    from faux_objects import TestCSVAware
  63.842 +
  63.843 +    aware = TestCSVAware()
  63.844 +    aware._setId(id)
  63.845 +    if csv is not None:
  63.846 +        aware._csv = csv
  63.847 +
  63.848 +    return aware
  63.849 +
  63.850 +
  63.851 +def _makeINIAware(id):
  63.852 +    from faux_objects import TestINIAware
  63.853 +
  63.854 +    aware = TestINIAware()
  63.855 +    aware._setId(id)
  63.856 +
  63.857 +    return aware
  63.858 +
  63.859 +
  63.860 +def _makeDAVAware(id):
  63.861 +    from faux_objects import TestDAVAware
  63.862 +
  63.863 +    aware = TestDAVAware()
  63.864 +    aware._setId(id)
  63.865 +
  63.866 +    return aware
  63.867 +
  63.868 +
  63.869 +def _makeItem(id):
  63.870 +    from faux_objects import TestSimpleItem
  63.871 +
  63.872 +    aware = TestSimpleItem()
  63.873 +    aware._setId(id)
  63.874 +
  63.875 +    return aware
  63.876 +
  63.877 +
  63.878 +def _makeFolder(id):
  63.879 +    from OFS.Folder import Folder
  63.880 +    from zope.interface import directlyProvides
  63.881 +    from zope.interface import providedBy
  63.882 +
  63.883 +    folder = Folder(id)
  63.884 +    directlyProvides(folder, providedBy(folder)
  63.885 +                             + IObjectManager + IPropertyManager)
  63.886 +
  63.887 +    return folder
  63.888 +
  63.889 +
  63.890 +def test_suite():
  63.891 +    suite = unittest.TestSuite()
  63.892 +    suite.addTest(unittest.makeSuite(SimpleINIAwareTests))
  63.893 +    suite.addTest(unittest.makeSuite(FolderishExporterImporterTests))
  63.894 +    suite.addTest(unittest.makeSuite(Test_globpattern))
  63.895 +    suite.addTest(unittest.makeSuite(CSVAwareFileAdapterTests))
  63.896 +    suite.addTest(unittest.makeSuite(INIAwareFileAdapterTests))
  63.897 +    suite.addTest(unittest.makeSuite(DAVAwareFileAdapterTests))
  63.898 +    return suite
  63.899 +
  63.900 +if __name__ == '__main__':
  63.901 +    unittest.main(defaultTest='test_suite')
    64.1 new file mode 100644
    64.2 --- /dev/null
    64.3 +++ b/tests/test_context.py
    64.4 @@ -0,0 +1,1429 @@
    64.5 +##############################################################################
    64.6 +#
    64.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    64.8 +#
    64.9 +# This software is subject to the provisions of the Zope Public License,
   64.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   64.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   64.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   64.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   64.14 +# FOR A PARTICULAR PURPOSE.
   64.15 +#
   64.16 +##############################################################################
   64.17 +""" Unit tests for import / export contexts.
   64.18 +
   64.19 +$Id: test_context.py 40277 2005-11-20 18:34:48Z yuppie $
   64.20 +"""
   64.21 +
   64.22 +import unittest
   64.23 +import Testing
   64.24 +
   64.25 +import logging
   64.26 +import os
   64.27 +import time
   64.28 +from StringIO import StringIO
   64.29 +from tarfile import TarFile
   64.30 +from tarfile import TarInfo
   64.31 +
   64.32 +from DateTime.DateTime import DateTime
   64.33 +from OFS.Folder import Folder
   64.34 +from OFS.Image import File
   64.35 +
   64.36 +from common import FilesystemTestBase
   64.37 +from common import SecurityRequestTest
   64.38 +from common import TarballTester
   64.39 +from common import _makeTestFile
   64.40 +from conformance import ConformsToISetupContext
   64.41 +from conformance import ConformsToIImportContext
   64.42 +from conformance import ConformsToIExportContext
   64.43 +
   64.44 +
   64.45 +class DummySite( Folder ):
   64.46 +
   64.47 +    pass
   64.48 +
   64.49 +class DummyTool( Folder ):
   64.50 +
   64.51 +    pass
   64.52 +
   64.53 +
   64.54 +class DirectoryImportContextTests( FilesystemTestBase
   64.55 +                                 , ConformsToISetupContext
   64.56 +                                 , ConformsToIImportContext
   64.57 +                                 ):
   64.58 +
   64.59 +    _PROFILE_PATH = '/tmp/ICTTexts'
   64.60 +
   64.61 +    def _getTargetClass( self ):
   64.62 +
   64.63 +        from Products.GenericSetup.context import DirectoryImportContext
   64.64 +        return DirectoryImportContext
   64.65 +
   64.66 +    def test_getLogger( self ):
   64.67 +
   64.68 +        site = DummySite( 'site' ).__of__( self.root )
   64.69 +        ctx = self._makeOne( site, self._PROFILE_PATH )
   64.70 +        self.assertEqual( len( ctx.listNotes() ), 0 )
   64.71 +
   64.72 +        logger = ctx.getLogger('foo')
   64.73 +        logger.info('bar')
   64.74 +
   64.75 +        self.assertEqual( len( ctx.listNotes() ), 1 )
   64.76 +        level, component, message = ctx.listNotes()[0]
   64.77 +        self.assertEqual( level, logging.INFO )
   64.78 +        self.assertEqual( component, 'foo' )
   64.79 +        self.assertEqual( message, 'bar' )
   64.80 +
   64.81 +        ctx.clearNotes()
   64.82 +        self.assertEqual( len( ctx.listNotes() ), 0 )
   64.83 +
   64.84 +    def test_readDataFile_nonesuch( self ):
   64.85 +
   64.86 +        FILENAME = 'nonesuch.txt'
   64.87 +
   64.88 +        site = DummySite( 'site' ).__of__( self.root )
   64.89 +        ctx = self._makeOne( site, self._PROFILE_PATH )
   64.90 +
   64.91 +        self.assertEqual( ctx.readDataFile( FILENAME ), None )
   64.92 +
   64.93 +    def test_readDataFile_simple( self ):
   64.94 +
   64.95 +        from string import printable
   64.96 +
   64.97 +        FILENAME = 'simple.txt'
   64.98 +        self._makeFile( FILENAME, printable )
   64.99 +
  64.100 +        site = DummySite( 'site' ).__of__( self.root )
  64.101 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.102 +
  64.103 +        self.assertEqual( ctx.readDataFile( FILENAME ), printable )
  64.104 +
  64.105 +    def test_readDataFile_subdir( self ):
  64.106 +
  64.107 +        from string import printable
  64.108 +
  64.109 +        FILENAME = 'subdir/nested.txt'
  64.110 +        self._makeFile( FILENAME, printable )
  64.111 +
  64.112 +        site = DummySite( 'site' ).__of__( self.root )
  64.113 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.114 +
  64.115 +        self.assertEqual( ctx.readDataFile( FILENAME ), printable )
  64.116 +
  64.117 +    def test_getLastModified_nonesuch( self ):
  64.118 +
  64.119 +        FILENAME = 'nonesuch.txt'
  64.120 +
  64.121 +        site = DummySite( 'site' ).__of__( self.root )
  64.122 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.123 +
  64.124 +        self.assertEqual( ctx.getLastModified( FILENAME ), None )
  64.125 +
  64.126 +    def test_getLastModified_simple( self ):
  64.127 +
  64.128 +        from string import printable
  64.129 +
  64.130 +        FILENAME = 'simple.txt'
  64.131 +        fqpath = self._makeFile( FILENAME, printable )
  64.132 +        timestamp = os.path.getmtime( fqpath )
  64.133 +
  64.134 +        site = DummySite( 'site' ).__of__( self.root )
  64.135 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.136 +
  64.137 +        lm = ctx.getLastModified( FILENAME )
  64.138 +        self.failUnless( isinstance( lm, DateTime ) )
  64.139 +        self.assertEqual( lm, timestamp )
  64.140 +
  64.141 +    def test_getLastModified_subdir( self ):
  64.142 +
  64.143 +        from string import printable
  64.144 +
  64.145 +        SUBDIR = 'subdir'
  64.146 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.147 +        fqpath = self._makeFile( FILENAME, printable )
  64.148 +        timestamp = os.path.getmtime( fqpath )
  64.149 +
  64.150 +        site = DummySite( 'site' ).__of__( self.root )
  64.151 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.152 +
  64.153 +        lm = ctx.getLastModified( FILENAME )
  64.154 +        self.failUnless( isinstance( lm, DateTime ) )
  64.155 +        self.assertEqual( lm, timestamp )
  64.156 +
  64.157 +    def test_getLastModified_directory( self ):
  64.158 +
  64.159 +        from string import printable
  64.160 +
  64.161 +        SUBDIR = 'subdir'
  64.162 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.163 +        fqpath = self._makeFile( FILENAME, printable )
  64.164 +        path, file = os.path.split( fqpath )
  64.165 +        timestamp = os.path.getmtime( path )
  64.166 +
  64.167 +        site = DummySite( 'site' ).__of__( self.root )
  64.168 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.169 +
  64.170 +        lm = ctx.getLastModified( SUBDIR )
  64.171 +        self.failUnless( isinstance( lm, DateTime ) )
  64.172 +        self.assertEqual( lm, timestamp )
  64.173 +
  64.174 +    def test_isDirectory_nonesuch( self ):
  64.175 +
  64.176 +        FILENAME = 'nonesuch.txt'
  64.177 +
  64.178 +        site = DummySite( 'site' ).__of__( self.root )
  64.179 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.180 +
  64.181 +        self.assertEqual( ctx.isDirectory( FILENAME ), None )
  64.182 +
  64.183 +    def test_isDirectory_simple( self ):
  64.184 +
  64.185 +        from string import printable
  64.186 +
  64.187 +        FILENAME = 'simple.txt'
  64.188 +        fqpath = self._makeFile( FILENAME, printable )
  64.189 +
  64.190 +        site = DummySite( 'site' ).__of__( self.root )
  64.191 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.192 +
  64.193 +        self.assertEqual( ctx.isDirectory( FILENAME ), False )
  64.194 +
  64.195 +    def test_isDirectory_nested( self ):
  64.196 +
  64.197 +        from string import printable
  64.198 +
  64.199 +        SUBDIR = 'subdir'
  64.200 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.201 +        fqpath = self._makeFile( FILENAME, printable )
  64.202 +
  64.203 +        site = DummySite( 'site' ).__of__( self.root )
  64.204 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.205 +
  64.206 +        self.assertEqual( ctx.isDirectory( FILENAME ), False )
  64.207 +
  64.208 +    def test_isDirectory_directory( self ):
  64.209 +
  64.210 +        from string import printable
  64.211 +
  64.212 +        SUBDIR = 'subdir'
  64.213 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.214 +        fqpath = self._makeFile( FILENAME, printable )
  64.215 +
  64.216 +        site = DummySite( 'site' ).__of__( self.root )
  64.217 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.218 +
  64.219 +        self.assertEqual( ctx.isDirectory( SUBDIR ), True )
  64.220 +
  64.221 +    def test_listDirectory_nonesuch( self ):
  64.222 +
  64.223 +        FILENAME = 'nonesuch.txt'
  64.224 +
  64.225 +        site = DummySite( 'site' ).__of__( self.root )
  64.226 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.227 +
  64.228 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
  64.229 +
  64.230 +    def test_listDirectory_root( self ):
  64.231 +
  64.232 +        from string import printable
  64.233 +
  64.234 +        site = DummySite( 'site' ).__of__( self.root )
  64.235 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.236 +
  64.237 +        FILENAME = 'simple.txt'
  64.238 +        self._makeFile( FILENAME, printable )
  64.239 +
  64.240 +        self.assertEqual( len( ctx.listDirectory( None ) ), 1 )
  64.241 +        self.failUnless( FILENAME in ctx.listDirectory( None ) )
  64.242 +
  64.243 +    def test_listDirectory_simple( self ):
  64.244 +
  64.245 +        from string import printable
  64.246 +
  64.247 +        FILENAME = 'simple.txt'
  64.248 +        self._makeFile( FILENAME, printable )
  64.249 +
  64.250 +        site = DummySite( 'site' ).__of__( self.root )
  64.251 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.252 +
  64.253 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
  64.254 +
  64.255 +    def test_listDirectory_nested( self ):
  64.256 +
  64.257 +        from string import printable
  64.258 +
  64.259 +        SUBDIR = 'subdir'
  64.260 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.261 +        self._makeFile( FILENAME, printable )
  64.262 +
  64.263 +        site = DummySite( 'site' ).__of__( self.root )
  64.264 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.265 +
  64.266 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
  64.267 +
  64.268 +    def test_listDirectory_single( self ):
  64.269 +
  64.270 +        from string import printable
  64.271 +
  64.272 +        SUBDIR = 'subdir'
  64.273 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.274 +        self._makeFile( FILENAME, printable )
  64.275 +
  64.276 +        site = DummySite( 'site' ).__of__( self.root )
  64.277 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.278 +
  64.279 +        names = ctx.listDirectory( SUBDIR )
  64.280 +        self.assertEqual( len( names ), 1 )
  64.281 +        self.failUnless( 'nested.txt' in names )
  64.282 +
  64.283 +    def test_listDirectory_multiple( self ):
  64.284 +
  64.285 +        from string import printable
  64.286 +        SUBDIR = 'subdir'
  64.287 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.288 +        self._makeFile( FILENAME, printable )
  64.289 +        self._makeFile( os.path.join( SUBDIR, 'another.txt' ), 'ABC' )
  64.290 +
  64.291 +        site = DummySite( 'site' ).__of__( self.root )
  64.292 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.293 +
  64.294 +        names = ctx.listDirectory( SUBDIR )
  64.295 +        self.assertEqual( len( names ), 2 )
  64.296 +        self.failUnless( 'nested.txt' in names )
  64.297 +        self.failUnless( 'another.txt' in names )
  64.298 +
  64.299 +    def test_listDirectory_skip_implicit( self ):
  64.300 +
  64.301 +        from string import printable
  64.302 +        SUBDIR = 'subdir'
  64.303 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.304 +        self._makeFile( FILENAME, printable )
  64.305 +        self._makeFile( os.path.join( SUBDIR, 'another.txt' ), 'ABC' )
  64.306 +        self._makeFile( os.path.join( SUBDIR, 'CVS/skip.txt' ), 'DEF' )
  64.307 +        self._makeFile( os.path.join( SUBDIR, '.svn/skip.txt' ), 'GHI' )
  64.308 +
  64.309 +        site = DummySite( 'site' ).__of__( self.root )
  64.310 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.311 +
  64.312 +        names = ctx.listDirectory( SUBDIR )
  64.313 +        self.assertEqual( len( names ), 2 )
  64.314 +        self.failUnless( 'nested.txt' in names )
  64.315 +        self.failUnless( 'another.txt' in names )
  64.316 +        self.failIf( 'CVS' in names )
  64.317 +        self.failIf( '.svn' in names )
  64.318 +
  64.319 +    def test_listDirectory_skip_explicit( self ):
  64.320 +
  64.321 +        from string import printable
  64.322 +        SUBDIR = 'subdir'
  64.323 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  64.324 +        self._makeFile( FILENAME, printable )
  64.325 +        self._makeFile( os.path.join( SUBDIR, 'another.txt' ), 'ABC' )
  64.326 +        self._makeFile( os.path.join( SUBDIR, 'CVS/skip.txt' ), 'DEF' )
  64.327 +        self._makeFile( os.path.join( SUBDIR, '.svn/skip.txt' ), 'GHI' )
  64.328 +
  64.329 +        site = DummySite( 'site' ).__of__( self.root )
  64.330 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.331 +
  64.332 +        names = ctx.listDirectory( SUBDIR, ( 'nested.txt', ) )
  64.333 +        self.assertEqual( len( names ), 3 )
  64.334 +        self.failIf( 'nested.txt' in names )
  64.335 +        self.failUnless( 'another.txt' in names )
  64.336 +        self.failUnless( 'CVS' in names )
  64.337 +        self.failUnless( '.svn' in names )
  64.338 +
  64.339 +
  64.340 +class DirectoryExportContextTests( FilesystemTestBase
  64.341 +                                 , ConformsToISetupContext
  64.342 +                                 , ConformsToIExportContext
  64.343 +                                 ):
  64.344 +
  64.345 +    _PROFILE_PATH = '/tmp/ECTTexts'
  64.346 +
  64.347 +    def _getTargetClass( self ):
  64.348 +
  64.349 +        from Products.GenericSetup.context import DirectoryExportContext
  64.350 +        return DirectoryExportContext
  64.351 +
  64.352 +    def test_getLogger( self ):
  64.353 +
  64.354 +        site = DummySite( 'site' ).__of__( self.root )
  64.355 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.356 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  64.357 +
  64.358 +        logger = ctx.getLogger('foo')
  64.359 +        logger.info('bar')
  64.360 +
  64.361 +        self.assertEqual( len( ctx.listNotes() ), 1 )
  64.362 +        level, component, message = ctx.listNotes()[0]
  64.363 +        self.assertEqual( level, logging.INFO )
  64.364 +        self.assertEqual( component, 'foo' )
  64.365 +        self.assertEqual( message, 'bar' )
  64.366 +
  64.367 +        ctx.clearNotes()
  64.368 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  64.369 +
  64.370 +    def test_writeDataFile_simple( self ):
  64.371 +
  64.372 +        from string import printable, digits
  64.373 +        FILENAME = 'simple.txt'
  64.374 +        fqname = self._makeFile( FILENAME, printable )
  64.375 +
  64.376 +        site = DummySite( 'site' ).__of__( self.root )
  64.377 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.378 +
  64.379 +        ctx.writeDataFile( FILENAME, digits, 'text/plain' )
  64.380 +
  64.381 +        self.assertEqual( open( fqname, 'rb' ).read(), digits )
  64.382 +
  64.383 +    def test_writeDataFile_new_subdir( self ):
  64.384 +
  64.385 +        from string import printable, digits
  64.386 +        SUBDIR = 'subdir'
  64.387 +        FILENAME = 'nested.txt'
  64.388 +        fqname = os.path.join( self._PROFILE_PATH, SUBDIR, FILENAME )
  64.389 +
  64.390 +        site = DummySite( 'site' ).__of__( self.root )
  64.391 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.392 +
  64.393 +        ctx.writeDataFile( FILENAME, digits, 'text/plain', SUBDIR )
  64.394 +
  64.395 +        self.assertEqual( open( fqname, 'rb' ).read(), digits )
  64.396 +
  64.397 +    def test_writeDataFile_overwrite( self ):
  64.398 +
  64.399 +        from string import printable, digits
  64.400 +        SUBDIR = 'subdir'
  64.401 +        FILENAME = 'nested.txt'
  64.402 +        fqname = self._makeFile( os.path.join( SUBDIR, FILENAME )
  64.403 +                               , printable )
  64.404 +
  64.405 +        site = DummySite( 'site' ).__of__( self.root )
  64.406 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.407 +
  64.408 +        ctx.writeDataFile( FILENAME, digits, 'text/plain', SUBDIR )
  64.409 +
  64.410 +        self.assertEqual( open( fqname, 'rb' ).read(), digits )
  64.411 +
  64.412 +    def test_writeDataFile_existing_subdir( self ):
  64.413 +
  64.414 +        from string import printable, digits
  64.415 +        SUBDIR = 'subdir'
  64.416 +        FILENAME = 'nested.txt'
  64.417 +        self._makeFile( os.path.join( SUBDIR, 'another.txt' ), printable )
  64.418 +        fqname = os.path.join( self._PROFILE_PATH, SUBDIR, FILENAME )
  64.419 +
  64.420 +        site = DummySite( 'site' ).__of__( self.root )
  64.421 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  64.422 +
  64.423 +        ctx.writeDataFile( FILENAME, digits, 'text/plain', SUBDIR )
  64.424 +
  64.425 +        self.assertEqual( open( fqname, 'rb' ).read(), digits )
  64.426 +
  64.427 +
  64.428 +class TarballImportContextTests( SecurityRequestTest
  64.429 +                               , ConformsToISetupContext
  64.430 +                               , ConformsToIImportContext
  64.431 +                               ):
  64.432 +
  64.433 +    def _getTargetClass( self ):
  64.434 +
  64.435 +        from Products.GenericSetup.context import TarballImportContext
  64.436 +        return TarballImportContext
  64.437 +
  64.438 +    def _makeOne( self, file_dict={}, mod_time=None, *args, **kw ):
  64.439 +
  64.440 +        archive_stream = StringIO()
  64.441 +        archive = TarFile.open('test.tar.gz', 'w:gz', archive_stream)
  64.442 +
  64.443 +        def _addOneMember(path, data, modtime):
  64.444 +            stream = StringIO(v)
  64.445 +            info = TarInfo(k)
  64.446 +            info.size = len(v)
  64.447 +            info.mtime = mod_time
  64.448 +            archive.addfile(info, stream)
  64.449 +
  64.450 +        def _addMember(path, data, modtime):
  64.451 +            from tarfile import DIRTYPE
  64.452 +            elements = path.split('/')
  64.453 +            parents = filter(None, [elements[x] for x in range(len(elements))])
  64.454 +            for parent in parents:
  64.455 +                info = TarInfo()
  64.456 +                info.name = parent
  64.457 +                info.size = 0
  64.458 +                info.mtime = mod_time
  64.459 +                info.type = DIRTYPE
  64.460 +                archive.addfile(info, StringIO())
  64.461 +            _addOneMember(path, data, modtime)
  64.462 +
  64.463 +        file_items = file_dict.items() or [('dummy', '')] # empty archive barfs
  64.464 +
  64.465 +        if mod_time is None:
  64.466 +            mod_time = time.time()
  64.467 +
  64.468 +        for k, v in file_items:
  64.469 +            _addMember(k, v, mod_time)
  64.470 +
  64.471 +        archive.close()
  64.472 +        bits = archive_stream.getvalue()
  64.473 +
  64.474 +        site = DummySite( 'site' ).__of__( self.root )
  64.475 +        site._setObject( 'setup_tool', Folder( 'setup_tool' ) )
  64.476 +        tool = site._getOb( 'setup_tool' )
  64.477 +
  64.478 +        ctx = self._getTargetClass()( tool, bits, *args, **kw )
  64.479 +
  64.480 +        return site, tool, ctx.__of__( tool )
  64.481 +
  64.482 +    def test_getLogger( self ):
  64.483 +
  64.484 +        site, tool, ctx = self._makeOne()
  64.485 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  64.486 +
  64.487 +        logger = ctx.getLogger('foo')
  64.488 +        logger.info('bar')
  64.489 +
  64.490 +        self.assertEqual( len( ctx.listNotes() ), 1 )
  64.491 +        level, component, message = ctx.listNotes()[0]
  64.492 +        self.assertEqual( level, logging.INFO )
  64.493 +        self.assertEqual( component, 'foo' )
  64.494 +        self.assertEqual( message, 'bar' )
  64.495 +
  64.496 +        ctx.clearNotes()
  64.497 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  64.498 +
  64.499 +    def test_ctorparms( self ):
  64.500 +
  64.501 +        ENCODING = 'latin-1'
  64.502 +        site, tool, ctx = self._makeOne( encoding=ENCODING
  64.503 +                                       , should_purge=True
  64.504 +                                       )
  64.505 +
  64.506 +        self.assertEqual( ctx.getEncoding(), ENCODING )
  64.507 +        self.assertEqual( ctx.shouldPurge(), True )
  64.508 +
  64.509 +    def test_empty( self ):
  64.510 +
  64.511 +        site, tool, ctx = self._makeOne()
  64.512 +
  64.513 +        self.assertEqual( ctx.getSite(), site )
  64.514 +        self.assertEqual( ctx.getEncoding(), None )
  64.515 +        self.assertEqual( ctx.shouldPurge(), False )
  64.516 +
  64.517 +        # These methods are all specified to return 'None' for non-existing
  64.518 +        # paths / entities
  64.519 +        self.assertEqual( ctx.isDirectory( 'nonesuch/path' ), None )
  64.520 +        self.assertEqual( ctx.listDirectory( 'nonesuch/path' ), None )
  64.521 +
  64.522 +    def test_readDataFile_nonesuch( self ):
  64.523 +
  64.524 +        FILENAME = 'nonesuch.txt'
  64.525 +
  64.526 +        site, tool, ctx = self._makeOne()
  64.527 +
  64.528 +        self.assertEqual( ctx.readDataFile( FILENAME ), None )
  64.529 +        self.assertEqual( ctx.readDataFile( FILENAME, 'subdir' ), None )
  64.530 +
  64.531 +    def test_readDataFile_simple( self ):
  64.532 +
  64.533 +        from string import printable
  64.534 +
  64.535 +        FILENAME = 'simple.txt'
  64.536 +
  64.537 +        site, tool, ctx = self._makeOne( { FILENAME: printable } )
  64.538 +
  64.539 +        self.assertEqual( ctx.readDataFile( FILENAME ), printable )
  64.540 +
  64.541 +    def test_readDataFile_subdir( self ):
  64.542 +
  64.543 +        from string import printable
  64.544 +
  64.545 +        FILENAME = 'subdir.txt'
  64.546 +        SUBDIR = 'subdir'
  64.547 +
  64.548 +        site, tool, ctx = self._makeOne( { '%s/%s' % (SUBDIR, FILENAME):
  64.549 +                                            printable } )
  64.550 +
  64.551 +        self.assertEqual( ctx.readDataFile( FILENAME, SUBDIR ), printable )
  64.552 +
  64.553 +    def test_getLastModified_nonesuch( self ):
  64.554 +
  64.555 +        FILENAME = 'nonesuch.txt'
  64.556 +
  64.557 +        site, tool, ctx = self._makeOne()
  64.558 +
  64.559 +        self.assertEqual( ctx.getLastModified( FILENAME ), None )
  64.560 +
  64.561 +    def test_getLastModified_simple( self ):
  64.562 +
  64.563 +        from string import printable
  64.564 +
  64.565 +        FILENAME = 'simple.txt'
  64.566 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
  64.567 +
  64.568 +        site, tool, ctx = self._makeOne( { FILENAME : printable }
  64.569 +                                       , mod_time=WHEN )
  64.570 +
  64.571 +        self.assertEqual( ctx.getLastModified( FILENAME ), WHEN )
  64.572 +
  64.573 +    def test_getLastModified_subdir( self ):
  64.574 +
  64.575 +        from string import printable
  64.576 +
  64.577 +        FILENAME = 'subdir.txt'
  64.578 +        SUBDIR = 'subdir'
  64.579 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  64.580 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
  64.581 +
  64.582 +        site, tool, ctx = self._makeOne( { PATH: printable }
  64.583 +                                       , mod_time=WHEN )
  64.584 +
  64.585 +        self.assertEqual( ctx.getLastModified( PATH ), WHEN )
  64.586 +
  64.587 +    def test_getLastModified_directory( self ):
  64.588 +
  64.589 +        from string import printable
  64.590 +
  64.591 +        FILENAME = 'subdir.txt'
  64.592 +        SUBDIR = 'subdir'
  64.593 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  64.594 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
  64.595 +
  64.596 +        site, tool, ctx = self._makeOne( { PATH: printable }
  64.597 +                                       , mod_time=WHEN
  64.598 +                                       )
  64.599 +
  64.600 +        self.assertEqual( ctx.getLastModified( SUBDIR ), WHEN )
  64.601 +
  64.602 +    def test_isDirectory_nonesuch( self ):
  64.603 +
  64.604 +        FILENAME = 'nonesuch.txt'
  64.605 +
  64.606 +        site, tool, ctx = self._makeOne()
  64.607 +
  64.608 +        self.assertEqual( ctx.isDirectory( FILENAME ), None )
  64.609 +
  64.610 +    def test_isDirectory_simple( self ):
  64.611 +
  64.612 +        from string import printable
  64.613 +
  64.614 +        FILENAME = 'simple.txt'
  64.615 +
  64.616 +        site, tool, ctx = self._makeOne( { FILENAME: printable } )
  64.617 +
  64.618 +        self.assertEqual( ctx.isDirectory( FILENAME ), False )
  64.619 +
  64.620 +    def test_isDirectory_nested( self ):
  64.621 +
  64.622 +        from string import printable
  64.623 +
  64.624 +        SUBDIR = 'subdir'
  64.625 +        FILENAME = 'nested.txt'
  64.626 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  64.627 +
  64.628 +        site, tool, ctx = self._makeOne( { PATH: printable } )
  64.629 +
  64.630 +        self.assertEqual( ctx.isDirectory( PATH ), False )
  64.631 +
  64.632 +    def test_isDirectory_subdir( self ):
  64.633 +
  64.634 +        from string import printable
  64.635 +
  64.636 +        SUBDIR = 'subdir'
  64.637 +        FILENAME = 'nested.txt'
  64.638 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  64.639 +
  64.640 +        site, tool, ctx = self._makeOne( { PATH: printable } )
  64.641 +
  64.642 +        self.assertEqual( ctx.isDirectory( SUBDIR ), True )
  64.643 +
  64.644 +    def test_listDirectory_nonesuch( self ):
  64.645 +
  64.646 +        SUBDIR = 'nonesuch/path'
  64.647 +
  64.648 +        site, tool, ctx = self._makeOne()
  64.649 +
  64.650 +        self.assertEqual( ctx.listDirectory( SUBDIR ), None )
  64.651 +
  64.652 +    def test_listDirectory_root( self ):
  64.653 +
  64.654 +        from string import printable
  64.655 +
  64.656 +        FILENAME = 'simple.txt'
  64.657 +
  64.658 +        site, tool, ctx = self._makeOne( { FILENAME: printable } )
  64.659 +
  64.660 +        self.assertEqual( len( ctx.listDirectory( None ) ), 1 )
  64.661 +        self.failUnless( FILENAME in ctx.listDirectory( None ) )
  64.662 +
  64.663 +    def test_listDirectory_simple( self ):
  64.664 +
  64.665 +        from string import printable
  64.666 +
  64.667 +        FILENAME = 'simple.txt'
  64.668 +
  64.669 +        site, tool, ctx = self._makeOne( { FILENAME: printable } )
  64.670 +
  64.671 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
  64.672 +
  64.673 +    def test_listDirectory_nested( self ):
  64.674 +
  64.675 +        from string import printable
  64.676 +
  64.677 +        SUBDIR = 'subdir'
  64.678 +        FILENAME = 'nested.txt'
  64.679 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  64.680 +
  64.681 +        site, tool, ctx = self._makeOne( { PATH: printable } )
  64.682 +
  64.683 +        self.assertEqual( ctx.listDirectory( PATH ), None )
  64.684 +
  64.685 +    def test_listDirectory_single( self ):
  64.686 +
  64.687 +        from string import printable
  64.688 +
  64.689 +        SUBDIR = 'subdir'
  64.690 +        FILENAME = 'nested.txt'
  64.691 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  64.692 +
  64.693 +        site, tool, ctx = self._makeOne( { PATH: printable } )
  64.694 +
  64.695 +        names = ctx.listDirectory( SUBDIR )
  64.696 +        self.assertEqual( len( names ), 1 )
  64.697 +        self.failUnless( FILENAME in names )
  64.698 +
  64.699 +    def test_listDirectory_multiple( self ):
  64.700 +
  64.701 +        from string import printable, uppercase
  64.702 +
  64.703 +        SUBDIR = 'subdir'
  64.704 +        FILENAME1 = 'nested.txt'
  64.705 +        PATH1 = '%s/%s' % ( SUBDIR, FILENAME1 )
  64.706 +        FILENAME2 = 'another.txt'
  64.707 +        PATH2 = '%s/%s' % ( SUBDIR, FILENAME2 )
  64.708 +
  64.709 +        site, tool, ctx = self._makeOne( { PATH1: printable
  64.710 +                                         , PATH2: uppercase
  64.711 +                                         } )
  64.712 +                                             
  64.713 +        names = ctx.listDirectory( SUBDIR )
  64.714 +        self.assertEqual( len( names ), 2 )
  64.715 +        self.failUnless( FILENAME1 in names )
  64.716 +        self.failUnless( FILENAME2 in names )
  64.717 +
  64.718 +    def test_listDirectory_skip( self ):
  64.719 +
  64.720 +        from string import printable, uppercase
  64.721 +
  64.722 +        SUBDIR = 'subdir'
  64.723 +        FILENAME1 = 'nested.txt'
  64.724 +        PATH1 = '%s/%s' % ( SUBDIR, FILENAME1 )
  64.725 +        FILENAME2 = 'another.txt'
  64.726 +        PATH2 = '%s/%s' % ( SUBDIR, FILENAME2 )
  64.727 +
  64.728 +        site, tool, ctx = self._makeOne( { PATH1: printable
  64.729 +                                         , PATH2: uppercase
  64.730 +                                         } )
  64.731 +
  64.732 +        names = ctx.listDirectory( SUBDIR, skip=( FILENAME1, ) )
  64.733 +        self.assertEqual( len( names ), 1 )
  64.734 +        self.failIf( FILENAME1 in names )
  64.735 +        self.failUnless( FILENAME2 in names )
  64.736 +
  64.737 +
  64.738 +class TarballExportContextTests( FilesystemTestBase
  64.739 +                               , TarballTester
  64.740 +                               , ConformsToISetupContext
  64.741 +                               , ConformsToIExportContext
  64.742 +                               ):
  64.743 +
  64.744 +    _PROFILE_PATH = '/tmp/TECT_tests'
  64.745 +
  64.746 +    def _getTargetClass( self ):
  64.747 +
  64.748 +        from Products.GenericSetup.context import TarballExportContext
  64.749 +        return TarballExportContext
  64.750 +
  64.751 +    def test_getLogger( self ):
  64.752 +
  64.753 +        site = DummySite( 'site' ).__of__( self.root )
  64.754 +        ctx = self._getTargetClass()( site )
  64.755 +
  64.756 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  64.757 +
  64.758 +        logger = ctx.getLogger('foo')
  64.759 +        logger.info('bar')
  64.760 +
  64.761 +        self.assertEqual( len( ctx.listNotes() ), 1 )
  64.762 +        level, component, message = ctx.listNotes()[0]
  64.763 +        self.assertEqual( level, logging.INFO )
  64.764 +        self.assertEqual( component, 'foo' )
  64.765 +        self.assertEqual( message, 'bar' )
  64.766 +
  64.767 +        ctx.clearNotes()
  64.768 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  64.769 +
  64.770 +    def test_writeDataFile_simple( self ):
  64.771 +
  64.772 +        from string import printable
  64.773 +        now = long( time.time() )
  64.774 +
  64.775 +        site = DummySite( 'site' ).__of__( self.root )
  64.776 +        ctx = self._getTargetClass()( site )
  64.777 +
  64.778 +        ctx.writeDataFile( 'foo.txt', printable, 'text/plain' )
  64.779 +
  64.780 +        fileish = StringIO( ctx.getArchive() )
  64.781 +
  64.782 +        self._verifyTarballContents( fileish, [ 'foo.txt' ], now )
  64.783 +        self._verifyTarballEntry( fileish, 'foo.txt', printable )
  64.784 +
  64.785 +    def test_writeDataFile_multiple( self ):
  64.786 +
  64.787 +        from string import printable
  64.788 +        from string import digits
  64.789 +
  64.790 +        site = DummySite( 'site' ).__of__( self.root )
  64.791 +        ctx = self._getTargetClass()( site )
  64.792 +
  64.793 +        ctx.writeDataFile( 'foo.txt', printable, 'text/plain' )
  64.794 +        ctx.writeDataFile( 'bar.txt', digits, 'text/plain' )
  64.795 +
  64.796 +        fileish = StringIO( ctx.getArchive() )
  64.797 +
  64.798 +        self._verifyTarballContents( fileish, [ 'foo.txt', 'bar.txt' ] )
  64.799 +        self._verifyTarballEntry( fileish, 'foo.txt', printable )
  64.800 +        self._verifyTarballEntry( fileish, 'bar.txt', digits )
  64.801 +
  64.802 +    def test_writeDataFile_subdir( self ):
  64.803 +
  64.804 +        from string import printable
  64.805 +        from string import digits
  64.806 +
  64.807 +        site = DummySite( 'site' ).__of__( self.root )
  64.808 +        ctx = self._getTargetClass()( site )
  64.809 +
  64.810 +        ctx.writeDataFile( 'foo.txt', printable, 'text/plain' )
  64.811 +        ctx.writeDataFile( 'bar/baz.txt', digits, 'text/plain' )
  64.812 +
  64.813 +        fileish = StringIO( ctx.getArchive() )
  64.814 +
  64.815 +        self._verifyTarballContents( fileish, [ 'foo.txt', 'bar/baz.txt' ] )
  64.816 +        self._verifyTarballEntry( fileish, 'foo.txt', printable )
  64.817 +        self._verifyTarballEntry( fileish, 'bar/baz.txt', digits )
  64.818 +
  64.819 +
  64.820 +class SnapshotExportContextTests( SecurityRequestTest
  64.821 +                                , ConformsToISetupContext
  64.822 +                                , ConformsToIExportContext
  64.823 +                                ):
  64.824 +
  64.825 +    def _getTargetClass( self ):
  64.826 +
  64.827 +        from Products.GenericSetup.context import SnapshotExportContext
  64.828 +        return SnapshotExportContext
  64.829 +
  64.830 +    def _makeOne( self, *args, **kw ):
  64.831 +
  64.832 +        return self._getTargetClass()( *args, **kw )
  64.833 +
  64.834 +    def test_getLogger( self ):
  64.835 +
  64.836 +        site = DummySite( 'site' ).__of__( self.root )
  64.837 +        site.setup_tool = DummyTool( 'setup_tool' )
  64.838 +        tool = site.setup_tool
  64.839 +        ctx = self._makeOne( tool, 'simple' )
  64.840 +
  64.841 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  64.842 +
  64.843 +        logger = ctx.getLogger('foo')
  64.844 +        logger.info('bar')
  64.845 +
  64.846 +        self.assertEqual( len( ctx.listNotes() ), 1 )
  64.847 +        level, component, message = ctx.listNotes()[0]
  64.848 +        self.assertEqual( level, logging.INFO )
  64.849 +        self.assertEqual( component, 'foo' )
  64.850 +        self.assertEqual( message, 'bar' )
  64.851 +
  64.852 +        ctx.clearNotes()
  64.853 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  64.854 +
  64.855 +    def test_writeDataFile_simple_image( self ):
  64.856 +
  64.857 +        from OFS.Image import Image
  64.858 +        FILENAME = 'simple.txt'
  64.859 +        CONTENT_TYPE = 'image/png'
  64.860 +        png_filename = os.path.join( os.path.split( __file__ )[0]
  64.861 +                                   , 'simple.png' )
  64.862 +        png_file = open( png_filename, 'rb' )
  64.863 +        png_data = png_file.read()
  64.864 +        png_file.close()
  64.865 +
  64.866 +        site = DummySite( 'site' ).__of__( self.root )
  64.867 +        site.setup_tool = DummyTool( 'setup_tool' )
  64.868 +        tool = site.setup_tool
  64.869 +        ctx = self._makeOne( tool, 'simple' )
  64.870 +
  64.871 +        ctx.writeDataFile( FILENAME, png_data, CONTENT_TYPE )
  64.872 +
  64.873 +        snapshot = tool.snapshots._getOb( 'simple' )
  64.874 +
  64.875 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  64.876 +        self.failUnless( FILENAME in snapshot.objectIds() )
  64.877 +
  64.878 +        fileobj = snapshot._getOb( FILENAME )
  64.879 +
  64.880 +        self.assertEqual( fileobj.getId(), FILENAME )
  64.881 +        self.assertEqual( fileobj.meta_type, Image.meta_type )
  64.882 +        self.assertEqual( fileobj.getContentType(), CONTENT_TYPE )
  64.883 +        self.assertEqual( fileobj.data, png_data )
  64.884 +
  64.885 +    def test_writeDataFile_simple_plain_text( self ):
  64.886 +
  64.887 +        from string import digits
  64.888 +        from OFS.Image import File
  64.889 +        FILENAME = 'simple.txt'
  64.890 +        CONTENT_TYPE = 'text/plain'
  64.891 +
  64.892 +        site = DummySite( 'site' ).__of__( self.root )
  64.893 +        site.setup_tool = DummyTool( 'setup_tool' )
  64.894 +        tool = site.setup_tool
  64.895 +        ctx = self._makeOne( tool, 'simple' )
  64.896 +
  64.897 +        ctx.writeDataFile( FILENAME, digits, CONTENT_TYPE )
  64.898 +
  64.899 +        snapshot = tool.snapshots._getOb( 'simple' )
  64.900 +
  64.901 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  64.902 +        self.failUnless( FILENAME in snapshot.objectIds() )
  64.903 +
  64.904 +        fileobj = snapshot._getOb( FILENAME )
  64.905 +
  64.906 +        self.assertEqual( fileobj.getId(), FILENAME )
  64.907 +        self.assertEqual( fileobj.meta_type, File.meta_type )
  64.908 +        self.assertEqual( fileobj.getContentType(), CONTENT_TYPE )
  64.909 +        self.assertEqual( str( fileobj ), digits )
  64.910 +
  64.911 +    def test_writeDataFile_simple_plain_text_unicode( self ):
  64.912 +
  64.913 +        from string import digits
  64.914 +        from OFS.Image import File
  64.915 +        FILENAME = 'simple.txt'
  64.916 +        CONTENT_TYPE = 'text/plain'
  64.917 +        CONTENT = u'Unicode, with non-ASCII: %s.' % unichr(150)
  64.918 +
  64.919 +        site = DummySite( 'site' ).__of__( self.root )
  64.920 +        site.setup_tool = DummyTool( 'setup_tool' )
  64.921 +        tool = site.setup_tool
  64.922 +        ctx = self._makeOne( tool, 'simple', 'latin_1' )
  64.923 +
  64.924 +        ctx.writeDataFile( FILENAME, CONTENT, CONTENT_TYPE )
  64.925 +
  64.926 +        snapshot = tool.snapshots._getOb( 'simple' )
  64.927 +
  64.928 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  64.929 +        self.failUnless( FILENAME in snapshot.objectIds() )
  64.930 +
  64.931 +        fileobj = snapshot._getOb( FILENAME )
  64.932 +
  64.933 +        self.assertEqual( fileobj.getId(), FILENAME )
  64.934 +        self.assertEqual( fileobj.meta_type, File.meta_type )
  64.935 +        self.assertEqual( fileobj.getContentType(), CONTENT_TYPE )
  64.936 +        saved = fileobj.manage_FTPget().decode('latin_1')
  64.937 +        self.assertEqual( saved, CONTENT )
  64.938 +
  64.939 +    def test_writeDataFile_simple_xml( self ):
  64.940 +
  64.941 +        from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
  64.942 +        FILENAME = 'simple.xml'
  64.943 +        CONTENT_TYPE = 'text/xml'
  64.944 +        _XML = """<?xml version="1.0"?><simple />"""
  64.945 +
  64.946 +        site = DummySite( 'site' ).__of__( self.root )
  64.947 +        site.setup_tool = DummyTool( 'setup_tool' )
  64.948 +        tool = site.setup_tool
  64.949 +        ctx = self._makeOne( tool, 'simple' )
  64.950 +
  64.951 +        ctx.writeDataFile( FILENAME, _XML, CONTENT_TYPE )
  64.952 +
  64.953 +        snapshot = tool.snapshots._getOb( 'simple' )
  64.954 +
  64.955 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  64.956 +        self.failUnless( FILENAME in snapshot.objectIds() )
  64.957 +
  64.958 +        template = snapshot._getOb( FILENAME )
  64.959 +
  64.960 +        self.assertEqual( template.getId(), FILENAME )
  64.961 +        self.assertEqual( template.meta_type, ZopePageTemplate.meta_type )
  64.962 +        self.assertEqual( template.read(), _XML )
  64.963 +        self.failIf( template.html() )
  64.964 +
  64.965 +    def test_writeDataFile_unicode_xml( self ):
  64.966 +
  64.967 +        from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
  64.968 +        FILENAME = 'simple.xml'
  64.969 +        CONTENT_TYPE = 'text/xml'
  64.970 +        _XML = u"""<?xml version="1.0"?><simple />"""
  64.971 +
  64.972 +        site = DummySite( 'site' ).__of__( self.root )
  64.973 +        site.setup_tool = DummyTool( 'setup_tool' )
  64.974 +        tool = site.setup_tool
  64.975 +        ctx = self._makeOne( tool, 'simple' )
  64.976 +
  64.977 +        ctx.writeDataFile( FILENAME, _XML, CONTENT_TYPE )
  64.978 +
  64.979 +        snapshot = tool.snapshots._getOb( 'simple' )
  64.980 +
  64.981 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  64.982 +        self.failUnless( FILENAME in snapshot.objectIds() )
  64.983 +
  64.984 +        template = snapshot._getOb( FILENAME )
  64.985 +
  64.986 +        self.assertEqual( template.getId(), FILENAME )
  64.987 +        self.assertEqual( template.meta_type, ZopePageTemplate.meta_type )
  64.988 +        self.assertEqual( template.read(), _XML )
  64.989 +        self.failIf( template.html() )
  64.990 +
  64.991 +    def test_writeDataFile_subdir_dtml( self ):
  64.992 +
  64.993 +        from OFS.DTMLDocument import DTMLDocument
  64.994 +        FILENAME = 'simple.dtml'
  64.995 +        CONTENT_TYPE = 'text/html'
  64.996 +        _HTML = """<html><body><h1>HTML</h1></body></html>"""
  64.997 +
  64.998 +        site = DummySite( 'site' ).__of__( self.root )
  64.999 +        site.setup_tool = DummyTool( 'setup_tool' )
 64.1000 +        tool = site.setup_tool
 64.1001 +        ctx = self._makeOne( tool, 'simple' )
 64.1002 +
 64.1003 +        ctx.writeDataFile( FILENAME, _HTML, CONTENT_TYPE, 'sub1' )
 64.1004 +
 64.1005 +        snapshot = tool.snapshots._getOb( 'simple' )
 64.1006 +        sub1 = snapshot._getOb( 'sub1' )
 64.1007 +
 64.1008 +        self.assertEqual( len( sub1.objectIds() ), 1 )
 64.1009 +        self.failUnless( FILENAME in sub1.objectIds() )
 64.1010 +
 64.1011 +        template = sub1._getOb( FILENAME )
 64.1012 +
 64.1013 +        self.assertEqual( template.getId(), FILENAME )
 64.1014 +        self.assertEqual( template.meta_type, DTMLDocument.meta_type )
 64.1015 +        self.assertEqual( template.read(), _HTML )
 64.1016 +
 64.1017 +        ctx.writeDataFile( 'sub1/%s2' % FILENAME, _HTML, CONTENT_TYPE)
 64.1018 +        self.assertEqual( len( sub1.objectIds() ), 2 )
 64.1019 +
 64.1020 +    def test_writeDataFile_nested_subdirs_html( self ):
 64.1021 +
 64.1022 +        from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
 64.1023 +        FILENAME = 'simple.html'
 64.1024 +        CONTENT_TYPE = 'text/html'
 64.1025 +        _HTML = """<html><body><h1>HTML</h1></body></html>"""
 64.1026 +
 64.1027 +        site = DummySite( 'site' ).__of__( self.root )
 64.1028 +        site.setup_tool = DummyTool( 'setup_tool' )
 64.1029 +        tool = site.setup_tool
 64.1030 +        ctx = self._makeOne( tool, 'simple' )
 64.1031 +
 64.1032 +        ctx.writeDataFile( FILENAME, _HTML, CONTENT_TYPE, 'sub1/sub2' )
 64.1033 +
 64.1034 +        snapshot = tool.snapshots._getOb( 'simple' )
 64.1035 +        sub1 = snapshot._getOb( 'sub1' )
 64.1036 +        sub2 = sub1._getOb( 'sub2' )
 64.1037 +
 64.1038 +        self.assertEqual( len( sub2.objectIds() ), 1 )
 64.1039 +        self.failUnless( FILENAME in sub2.objectIds() )
 64.1040 +
 64.1041 +        template = sub2._getOb( FILENAME )
 64.1042 +
 64.1043 +        self.assertEqual( template.getId(), FILENAME )
 64.1044 +        self.assertEqual( template.meta_type, ZopePageTemplate.meta_type )
 64.1045 +        self.assertEqual( template.read(), _HTML )
 64.1046 +        self.failUnless( template.html() )
 64.1047 +
 64.1048 +    def test_writeDataFile_multiple( self ):
 64.1049 +
 64.1050 +        from string import printable
 64.1051 +        from string import digits
 64.1052 +
 64.1053 +        site = DummySite( 'site' ).__of__( self.root )
 64.1054 +        site.setup_tool = DummyTool( 'setup_tool' )
 64.1055 +        tool = site.setup_tool
 64.1056 +        ctx = self._makeOne( tool, 'multiple' )
 64.1057 +
 64.1058 +        ctx.writeDataFile( 'foo.txt', printable, 'text/plain' )
 64.1059 +        ctx.writeDataFile( 'bar.txt', digits, 'text/plain' )
 64.1060 +
 64.1061 +        snapshot = tool.snapshots._getOb( 'multiple' )
 64.1062 +
 64.1063 +        self.assertEqual( len( snapshot.objectIds() ), 2 )
 64.1064 +
 64.1065 +        for id in [ 'foo.txt', 'bar.txt' ]:
 64.1066 +            self.failUnless( id in snapshot.objectIds() )
 64.1067 +
 64.1068 +
 64.1069 +class SnapshotImportContextTests( SecurityRequestTest
 64.1070 +                                , ConformsToISetupContext
 64.1071 +                                , ConformsToIImportContext
 64.1072 +                                ):
 64.1073 +
 64.1074 +    def _getTargetClass( self ):
 64.1075 +
 64.1076 +        from Products.GenericSetup.context import SnapshotImportContext
 64.1077 +        return SnapshotImportContext
 64.1078 +
 64.1079 +    def _makeOne( self, context_id, *args, **kw ):
 64.1080 +
 64.1081 +        site = DummySite( 'site' ).__of__( self.root )
 64.1082 +        site._setObject( 'setup_tool', Folder( 'setup_tool' ) )
 64.1083 +        tool = site._getOb( 'setup_tool' )
 64.1084 +
 64.1085 +        tool._setObject( 'snapshots', Folder( 'snapshots' ) )
 64.1086 +        tool.snapshots._setObject( context_id, Folder( context_id ) )
 64.1087 +
 64.1088 +        ctx = self._getTargetClass()( tool, context_id, *args, **kw )
 64.1089 +
 64.1090 +        return site, tool, ctx.__of__( tool )
 64.1091 +
 64.1092 +    def _makeFile( self
 64.1093 +                 , tool
 64.1094 +                 , snapshot_id
 64.1095 +                 , filename
 64.1096 +                 , contents
 64.1097 +                 , content_type='text/plain'
 64.1098 +                 , mod_time=None
 64.1099 +                 , subdir=None
 64.1100 +                 ):
 64.1101 +
 64.1102 +        snapshots = tool._getOb( 'snapshots' )
 64.1103 +        folder = snapshot = snapshots._getOb( snapshot_id )
 64.1104 +
 64.1105 +        if subdir is not None:
 64.1106 +
 64.1107 +            for element in subdir.split( '/' ):
 64.1108 +
 64.1109 +                try:
 64.1110 +                    folder = folder._getOb( element )
 64.1111 +                except AttributeError:
 64.1112 +                    folder._setObject( element, Folder( element ) )
 64.1113 +                    folder = folder._getOb( element )
 64.1114 +
 64.1115 +        file = File( filename, '', contents, content_type )
 64.1116 +        folder._setObject( filename, file )
 64.1117 +
 64.1118 +        if mod_time is not None:
 64.1119 +
 64.1120 +            def __faux_mod_time():
 64.1121 +                return mod_time
 64.1122 +
 64.1123 +            folder.bobobase_modification_time = \
 64.1124 +            file.bobobase_modification_time = __faux_mod_time
 64.1125 +
 64.1126 +        return folder._getOb( filename )
 64.1127 +
 64.1128 +    def test_getLogger( self ):
 64.1129 +
 64.1130 +        SNAPSHOT_ID = 'note'
 64.1131 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1132 +
 64.1133 +        self.assertEqual( len( ctx.listNotes() ), 0 )
 64.1134 +
 64.1135 +        logger = ctx.getLogger('foo')
 64.1136 +        logger.info('bar')
 64.1137 +
 64.1138 +        self.assertEqual( len( ctx.listNotes() ), 1 )
 64.1139 +        level, component, message = ctx.listNotes()[0]
 64.1140 +        self.assertEqual( level, logging.INFO )
 64.1141 +        self.assertEqual( component, 'foo' )
 64.1142 +        self.assertEqual( message, 'bar' )
 64.1143 +
 64.1144 +        ctx.clearNotes()
 64.1145 +        self.assertEqual( len( ctx.listNotes() ), 0 )
 64.1146 +
 64.1147 +    def test_ctorparms( self ):
 64.1148 +
 64.1149 +        SNAPSHOT_ID = 'ctorparms'
 64.1150 +        ENCODING = 'latin-1'
 64.1151 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID
 64.1152 +                                       , encoding=ENCODING
 64.1153 +                                       , should_purge=True
 64.1154 +                                       )
 64.1155 +
 64.1156 +        self.assertEqual( ctx.getEncoding(), ENCODING )
 64.1157 +        self.assertEqual( ctx.shouldPurge(), True )
 64.1158 +
 64.1159 +    def test_empty( self ):
 64.1160 +
 64.1161 +        SNAPSHOT_ID = 'empty'
 64.1162 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1163 +
 64.1164 +        self.assertEqual( ctx.getSite(), site )
 64.1165 +        self.assertEqual( ctx.getEncoding(), None )
 64.1166 +        self.assertEqual( ctx.shouldPurge(), False )
 64.1167 +
 64.1168 +        # These methods are all specified to return 'None' for non-existing
 64.1169 +        # paths / entities
 64.1170 +        self.assertEqual( ctx.isDirectory( 'nonesuch/path' ), None )
 64.1171 +        self.assertEqual( ctx.listDirectory( 'nonesuch/path' ), None )
 64.1172 +
 64.1173 +    def test_readDataFile_nonesuch( self ):
 64.1174 +
 64.1175 +        SNAPSHOT_ID = 'readDataFile_nonesuch'
 64.1176 +        FILENAME = 'nonesuch.txt'
 64.1177 +
 64.1178 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1179 +
 64.1180 +        self.assertEqual( ctx.readDataFile( FILENAME ), None )
 64.1181 +        self.assertEqual( ctx.readDataFile( FILENAME, 'subdir' ), None )
 64.1182 +
 64.1183 +    def test_readDataFile_simple( self ):
 64.1184 +
 64.1185 +        from string import printable
 64.1186 +
 64.1187 +        SNAPSHOT_ID = 'readDataFile_simple'
 64.1188 +        FILENAME = 'simple.txt'
 64.1189 +
 64.1190 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1191 +        self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable )
 64.1192 +
 64.1193 +        self.assertEqual( ctx.readDataFile( FILENAME ), printable )
 64.1194 +
 64.1195 +    def test_readDataFile_subdir( self ):
 64.1196 +
 64.1197 +        from string import printable
 64.1198 +
 64.1199 +        SNAPSHOT_ID = 'readDataFile_subdir'
 64.1200 +        FILENAME = 'subdir.txt'
 64.1201 +        SUBDIR = 'subdir'
 64.1202 +
 64.1203 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1204 +        self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 64.1205 +                      , subdir=SUBDIR )
 64.1206 +
 64.1207 +        self.assertEqual( ctx.readDataFile( FILENAME, SUBDIR ), printable )
 64.1208 +        self.assertEqual( ctx.readDataFile( '%s/%s' % (SUBDIR, FILENAME) ),
 64.1209 +                                            printable )
 64.1210 +
 64.1211 +    def test_getLastModified_nonesuch( self ):
 64.1212 +
 64.1213 +        SNAPSHOT_ID = 'getLastModified_nonesuch'
 64.1214 +        FILENAME = 'nonesuch.txt'
 64.1215 +
 64.1216 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1217 +
 64.1218 +        self.assertEqual( ctx.getLastModified( FILENAME ), None )
 64.1219 +
 64.1220 +    def test_getLastModified_simple( self ):
 64.1221 +
 64.1222 +        from string import printable
 64.1223 +
 64.1224 +        SNAPSHOT_ID = 'getLastModified_simple'
 64.1225 +        FILENAME = 'simple.txt'
 64.1226 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
 64.1227 +
 64.1228 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1229 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 64.1230 +                             , mod_time=WHEN )
 64.1231 +
 64.1232 +        self.assertEqual( ctx.getLastModified( FILENAME ), WHEN )
 64.1233 +
 64.1234 +    def test_getLastModified_subdir( self ):
 64.1235 +
 64.1236 +        from string import printable
 64.1237 +
 64.1238 +        SNAPSHOT_ID = 'getLastModified_subdir'
 64.1239 +        FILENAME = 'subdir.txt'
 64.1240 +        SUBDIR = 'subdir'
 64.1241 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
 64.1242 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
 64.1243 +
 64.1244 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1245 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 64.1246 +                             , mod_time=WHEN, subdir=SUBDIR )
 64.1247 +
 64.1248 +        self.assertEqual( ctx.getLastModified( PATH ), WHEN )
 64.1249 +
 64.1250 +    def test_getLastModified_directory( self ):
 64.1251 +
 64.1252 +        from string import printable
 64.1253 +
 64.1254 +        SNAPSHOT_ID = 'readDataFile_subdir'
 64.1255 +        FILENAME = 'subdir.txt'
 64.1256 +        SUBDIR = 'subdir'
 64.1257 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
 64.1258 +
 64.1259 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1260 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 64.1261 +                             , mod_time=WHEN, subdir=SUBDIR )
 64.1262 +
 64.1263 +        self.assertEqual( ctx.getLastModified( SUBDIR ), WHEN )
 64.1264 +
 64.1265 +    def test_isDirectory_nonesuch( self ):
 64.1266 +
 64.1267 +        SNAPSHOT_ID = 'isDirectory_nonesuch'
 64.1268 +        FILENAME = 'nonesuch.txt'
 64.1269 +
 64.1270 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1271 +
 64.1272 +        self.assertEqual( ctx.isDirectory( FILENAME ), None )
 64.1273 +
 64.1274 +    def test_isDirectory_simple( self ):
 64.1275 +
 64.1276 +        from string import printable
 64.1277 +
 64.1278 +        SNAPSHOT_ID = 'isDirectory_simple'
 64.1279 +        FILENAME = 'simple.txt'
 64.1280 +
 64.1281 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1282 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable )
 64.1283 +
 64.1284 +        self.assertEqual( ctx.isDirectory( FILENAME ), False )
 64.1285 +
 64.1286 +    def test_isDirectory_nested( self ):
 64.1287 +
 64.1288 +        from string import printable
 64.1289 +
 64.1290 +        SNAPSHOT_ID = 'isDirectory_nested'
 64.1291 +        SUBDIR = 'subdir'
 64.1292 +        FILENAME = 'nested.txt'
 64.1293 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
 64.1294 +
 64.1295 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1296 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 64.1297 +                             , subdir=SUBDIR )
 64.1298 +
 64.1299 +        self.assertEqual( ctx.isDirectory( PATH ), False )
 64.1300 +
 64.1301 +    def test_isDirectory_subdir( self ):
 64.1302 +
 64.1303 +        from string import printable
 64.1304 +
 64.1305 +        SNAPSHOT_ID = 'isDirectory_subdir'
 64.1306 +        SUBDIR = 'subdir'
 64.1307 +        FILENAME = 'nested.txt'
 64.1308 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
 64.1309 +
 64.1310 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1311 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 64.1312 +                             , subdir=SUBDIR )
 64.1313 +
 64.1314 +        self.assertEqual( ctx.isDirectory( SUBDIR ), True )
 64.1315 +
 64.1316 +    def test_listDirectory_nonesuch( self ):
 64.1317 +
 64.1318 +        SNAPSHOT_ID = 'listDirectory_nonesuch'
 64.1319 +        SUBDIR = 'nonesuch/path'
 64.1320 +
 64.1321 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1322 +
 64.1323 +        self.assertEqual( ctx.listDirectory( SUBDIR ), None )
 64.1324 +
 64.1325 +    def test_listDirectory_root( self ):
 64.1326 +
 64.1327 +        from string import printable
 64.1328 +
 64.1329 +        SNAPSHOT_ID = 'listDirectory_root'
 64.1330 +        FILENAME = 'simple.txt'
 64.1331 +
 64.1332 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1333 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable )
 64.1334 +
 64.1335 +        self.assertEqual( len( ctx.listDirectory( None ) ), 1 )
 64.1336 +        self.failUnless( FILENAME in ctx.listDirectory( None ) )
 64.1337 +
 64.1338 +    def test_listDirectory_simple( self ):
 64.1339 +
 64.1340 +        from string import printable
 64.1341 +
 64.1342 +        SNAPSHOT_ID = 'listDirectory_simple'
 64.1343 +        FILENAME = 'simple.txt'
 64.1344 +
 64.1345 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1346 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable )
 64.1347 +
 64.1348 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
 64.1349 +
 64.1350 +    def test_listDirectory_nested( self ):
 64.1351 +
 64.1352 +        from string import printable
 64.1353 +
 64.1354 +        SNAPSHOT_ID = 'listDirectory_nested'
 64.1355 +        SUBDIR = 'subdir'
 64.1356 +        FILENAME = 'nested.txt'
 64.1357 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
 64.1358 +
 64.1359 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1360 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 64.1361 +                             , subdir=SUBDIR )
 64.1362 +
 64.1363 +        self.assertEqual( ctx.listDirectory( PATH ), None )
 64.1364 +
 64.1365 +    def test_listDirectory_single( self ):
 64.1366 +
 64.1367 +        from string import printable
 64.1368 +
 64.1369 +        SNAPSHOT_ID = 'listDirectory_nested'
 64.1370 +        SUBDIR = 'subdir'
 64.1371 +        FILENAME = 'nested.txt'
 64.1372 +
 64.1373 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1374 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 64.1375 +                             , subdir=SUBDIR )
 64.1376 +
 64.1377 +        names = ctx.listDirectory( SUBDIR )
 64.1378 +        self.assertEqual( len( names ), 1 )
 64.1379 +        self.failUnless( FILENAME in names )
 64.1380 +
 64.1381 +    def test_listDirectory_multiple( self ):
 64.1382 +
 64.1383 +        from string import printable, uppercase
 64.1384 +
 64.1385 +        SNAPSHOT_ID = 'listDirectory_nested'
 64.1386 +        SUBDIR = 'subdir'
 64.1387 +        FILENAME1 = 'nested.txt'
 64.1388 +        FILENAME2 = 'another.txt'
 64.1389 +
 64.1390 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1391 +        file1 = self._makeFile( tool, SNAPSHOT_ID, FILENAME1, printable
 64.1392 +                              , subdir=SUBDIR )
 64.1393 +        file2 = self._makeFile( tool, SNAPSHOT_ID, FILENAME2, uppercase
 64.1394 +                              , subdir=SUBDIR )
 64.1395 +
 64.1396 +        names = ctx.listDirectory( SUBDIR )
 64.1397 +        self.assertEqual( len( names ), 2 )
 64.1398 +        self.failUnless( FILENAME1 in names )
 64.1399 +        self.failUnless( FILENAME2 in names )
 64.1400 +
 64.1401 +    def test_listDirectory_skip( self ):
 64.1402 +
 64.1403 +        from string import printable, uppercase
 64.1404 +
 64.1405 +        SNAPSHOT_ID = 'listDirectory_nested'
 64.1406 +        SUBDIR = 'subdir'
 64.1407 +        FILENAME1 = 'nested.txt'
 64.1408 +        FILENAME2 = 'another.txt'
 64.1409 +
 64.1410 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 64.1411 +        file1 = self._makeFile( tool, SNAPSHOT_ID, FILENAME1, printable
 64.1412 +                              , subdir=SUBDIR )
 64.1413 +        file2 = self._makeFile( tool, SNAPSHOT_ID, FILENAME2, uppercase
 64.1414 +                              , subdir=SUBDIR )
 64.1415 +
 64.1416 +        names = ctx.listDirectory( SUBDIR, skip=( FILENAME1, ) )
 64.1417 +        self.assertEqual( len( names ), 1 )
 64.1418 +        self.failIf( FILENAME1 in names )
 64.1419 +        self.failUnless( FILENAME2 in names )
 64.1420 +
 64.1421 +
 64.1422 +def test_suite():
 64.1423 +    return unittest.TestSuite((
 64.1424 +        unittest.makeSuite( DirectoryImportContextTests ),
 64.1425 +        unittest.makeSuite( DirectoryExportContextTests ),
 64.1426 +        unittest.makeSuite( TarballImportContextTests ),
 64.1427 +        unittest.makeSuite( TarballExportContextTests ),
 64.1428 +        unittest.makeSuite( SnapshotExportContextTests ),
 64.1429 +        unittest.makeSuite( SnapshotImportContextTests ),
 64.1430 +        ))
 64.1431 +
 64.1432 +if __name__ == '__main__':
 64.1433 +    unittest.main(defaultTest='test_suite')
    65.1 new file mode 100644
    65.2 --- /dev/null
    65.3 +++ b/tests/test_differ.py
    65.4 @@ -0,0 +1,406 @@
    65.5 +##############################################################################
    65.6 +#
    65.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    65.8 +#
    65.9 +# This software is subject to the provisions of the Zope Public License,
   65.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   65.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   65.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   65.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   65.14 +# FOR A PARTICULAR PURPOSE.
   65.15 +#
   65.16 +##############################################################################
   65.17 +""" Unit tests for differ module.
   65.18 +
   65.19 +$Id: test_differ.py 38578 2005-09-24 09:34:34Z tseaver $
   65.20 +"""
   65.21 +
   65.22 +import unittest
   65.23 +import Testing
   65.24 +import Zope2
   65.25 +Zope2.startup()
   65.26 +
   65.27 +from OFS.Folder import Folder
   65.28 +from OFS.Image import File
   65.29 +
   65.30 +from DateTime.DateTime import DateTime
   65.31 +
   65.32 +from common import SecurityRequestTest
   65.33 +
   65.34 +class DummySite( Folder ):
   65.35 +
   65.36 +    pass
   65.37 +
   65.38 +
   65.39 +class Test_unidiff( unittest.TestCase ):
   65.40 +
   65.41 +    def test_unidiff_both_text( self ):
   65.42 +
   65.43 +        from Products.GenericSetup.differ import unidiff
   65.44 +
   65.45 +        diff_lines = unidiff( ONE_FOUR, ZERO_FOUR )
   65.46 +        diff_text = '\n'.join( diff_lines )
   65.47 +        self.assertEqual( diff_text, DIFF_TEXT )
   65.48 +
   65.49 +    def test_unidiff_both_lines( self ):
   65.50 +
   65.51 +        from Products.GenericSetup.differ import unidiff
   65.52 +
   65.53 +        diff_lines = unidiff( ONE_FOUR.splitlines(), ZERO_FOUR.splitlines() )
   65.54 +        diff_text = '\n'.join( diff_lines )
   65.55 +        self.assertEqual( diff_text, DIFF_TEXT )
   65.56 +
   65.57 +    def test_unidiff_mixed( self ):
   65.58 +
   65.59 +        from Products.GenericSetup.differ import unidiff
   65.60 +
   65.61 +        diff_lines = unidiff( ONE_FOUR, ZERO_FOUR.splitlines() )
   65.62 +        diff_text = '\n'.join( diff_lines )
   65.63 +        self.assertEqual( diff_text, DIFF_TEXT )
   65.64 +
   65.65 +    def test_unidiff_ignore_blanks( self ):
   65.66 +
   65.67 +        from Products.GenericSetup.differ import unidiff
   65.68 +
   65.69 +        double_spaced = ONE_FOUR.replace( '\n', '\n\n' )
   65.70 +        diff_lines = unidiff( double_spaced
   65.71 +                            , ZERO_FOUR.splitlines()
   65.72 +                            , ignore_blanks=True
   65.73 +                            )
   65.74 +
   65.75 +        diff_text = '\n'.join( diff_lines )
   65.76 +        self.assertEqual( diff_text, DIFF_TEXT )
   65.77 +
   65.78 +ZERO_FOUR = """\
   65.79 +zero
   65.80 +one
   65.81 +tree
   65.82 +four
   65.83 +"""
   65.84 +
   65.85 +ONE_FOUR = """\
   65.86 +one
   65.87 +two
   65.88 +three
   65.89 +four
   65.90 +"""
   65.91 +
   65.92 +DIFF_TEXT = """\
   65.93 +--- original None
   65.94 ++++ modified None
   65.95 +@@ -1,4 +1,4 @@
   65.96 ++zero
   65.97 + one
   65.98 +-two
   65.99 +-three
  65.100 ++tree
  65.101 + four\
  65.102 +"""
  65.103 +
  65.104 +class ConfigDiffTests( SecurityRequestTest ):
  65.105 +
  65.106 +    site = None
  65.107 +    tool = None
  65.108 +
  65.109 +    def _getTargetClass( self ):
  65.110 +
  65.111 +        from Products.GenericSetup.differ import ConfigDiff
  65.112 +        return ConfigDiff
  65.113 +
  65.114 +    def _makeOne( self, lhs, rhs, *args, **kw ):
  65.115 +
  65.116 +        return self._getTargetClass()( lhs, rhs, *args, **kw )
  65.117 +
  65.118 +    def _makeSite( self ):
  65.119 +
  65.120 +        if self.site is not None:
  65.121 +            return
  65.122 +
  65.123 +        site = self.site = DummySite( 'site' ).__of__( self.root )
  65.124 +        site._setObject( 'setup_tool', Folder( 'setup_tool' ) )
  65.125 +        self.tool = tool = site._getOb( 'setup_tool' )
  65.126 +
  65.127 +        tool._setObject( 'snapshots', Folder( 'snapshots' ) )
  65.128 +
  65.129 +    def _makeContext( self, context_id ):
  65.130 +
  65.131 +        from Products.GenericSetup.context import SnapshotImportContext
  65.132 +
  65.133 +        self._makeSite()
  65.134 +
  65.135 +        if context_id not in self.tool.snapshots.objectIds():
  65.136 +            self.tool.snapshots._setObject( context_id, Folder( context_id ) )
  65.137 +
  65.138 +        ctx = SnapshotImportContext( self.tool, context_id )
  65.139 +
  65.140 +        return ctx.__of__( self.tool )
  65.141 +
  65.142 +    def _makeDirectory( self, snapshot_id, subdir ):
  65.143 +
  65.144 +        self._makeSite()
  65.145 +        folder = self.tool.snapshots._getOb( snapshot_id )
  65.146 +
  65.147 +        for element in subdir.split( '/' ):
  65.148 +
  65.149 +            try:
  65.150 +                folder = folder._getOb( element )
  65.151 +            except AttributeError:
  65.152 +                folder._setObject( element, Folder( element ) )
  65.153 +                folder = folder._getOb( element )
  65.154 +
  65.155 +        return folder
  65.156 +
  65.157 +    def _makeFile( self
  65.158 +                 , snapshot_id
  65.159 +                 , filename
  65.160 +                 , contents
  65.161 +                 , content_type='text/plain'
  65.162 +                 , mod_time=None
  65.163 +                 , subdir=None
  65.164 +                 ):
  65.165 +
  65.166 +        self._makeSite()
  65.167 +        snapshots = self.tool.snapshots
  65.168 +        snapshot = snapshots._getOb( snapshot_id )
  65.169 +
  65.170 +        if subdir is not None:
  65.171 +            folder = self._makeDirectory( snapshot_id, subdir )
  65.172 +        else:
  65.173 +            folder = snapshot
  65.174 +
  65.175 +        file = File( filename, '', contents, content_type )
  65.176 +        folder._setObject( filename, file )
  65.177 +
  65.178 +        if mod_time is not None:
  65.179 +
  65.180 +            def __faux_mod_time():
  65.181 +                return mod_time
  65.182 +
  65.183 +            folder.bobobase_modification_time = \
  65.184 +            file.bobobase_modification_time = __faux_mod_time
  65.185 +
  65.186 +        return folder._getOb( filename )
  65.187 +
  65.188 +    def test_compare_empties( self ):
  65.189 +
  65.190 +        lhs = self._makeContext( 'lhs' )
  65.191 +        rhs = self._makeContext( 'rhs' )
  65.192 +
  65.193 +        cd = self._makeOne( lhs, rhs )
  65.194 +
  65.195 +        diffs = cd.compare()
  65.196 +
  65.197 +        self.assertEqual( diffs, '' )
  65.198 +
  65.199 +    def test_compare_identical( self ):
  65.200 +
  65.201 +        lhs = self._makeContext( 'lhs' )
  65.202 +        rhs = self._makeContext( 'rhs' )
  65.203 +
  65.204 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF' )
  65.205 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub' )
  65.206 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF' )
  65.207 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub' )
  65.208 +
  65.209 +        cd = self._makeOne( lhs, rhs )
  65.210 +
  65.211 +        diffs = cd.compare()
  65.212 +
  65.213 +        self.assertEqual( diffs, '' )
  65.214 +
  65.215 +    def test_compare_changed_file( self ):
  65.216 +
  65.217 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  65.218 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  65.219 +
  65.220 +        lhs = self._makeContext( 'lhs' )
  65.221 +        rhs = self._makeContext( 'rhs' )
  65.222 +
  65.223 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ', mod_time=BEFORE )
  65.224 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub' )
  65.225 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nQRST', mod_time=AFTER )
  65.226 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub' )
  65.227 +
  65.228 +        cd = self._makeOne( lhs, rhs )
  65.229 +
  65.230 +        diffs = cd.compare()
  65.231 +
  65.232 +        self.assertEqual( diffs, TEST_TXT_DIFFS % ( BEFORE, AFTER ) )
  65.233 +
  65.234 +    def test_compare_changed_file_ignore_blanks( self ):
  65.235 +
  65.236 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  65.237 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  65.238 +
  65.239 +        lhs = self._makeContext( 'lhs' )
  65.240 +        rhs = self._makeContext( 'rhs' )
  65.241 +
  65.242 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ', mod_time=BEFORE )
  65.243 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\n\n\nWXYZ', mod_time=AFTER )
  65.244 +
  65.245 +        cd = self._makeOne( lhs, rhs, ignore_blanks=True )
  65.246 +
  65.247 +        diffs = cd.compare()
  65.248 +
  65.249 +        self.assertEqual( diffs, '' )
  65.250 +
  65.251 +    def test_compare_changed_file_explicit_skip( self ):
  65.252 +
  65.253 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  65.254 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  65.255 +
  65.256 +        lhs = self._makeContext( 'lhs' )
  65.257 +        rhs = self._makeContext( 'rhs' )
  65.258 +
  65.259 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ', subdir='skipme'
  65.260 +                      , mod_time=BEFORE )
  65.261 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub' )
  65.262 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nQRST', subdir='skipme'
  65.263 +                      , mod_time=AFTER )
  65.264 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub' )
  65.265 +
  65.266 +        cd = self._makeOne( lhs, rhs, skip=[ 'skipme' ] )
  65.267 +
  65.268 +        diffs = cd.compare()
  65.269 +
  65.270 +        self.assertEqual( diffs, '' )
  65.271 +
  65.272 +    def test_compare_changed_file_implicit_skip( self ):
  65.273 +
  65.274 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  65.275 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  65.276 +
  65.277 +        lhs = self._makeContext( 'lhs' )
  65.278 +        rhs = self._makeContext( 'rhs' )
  65.279 +
  65.280 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ', subdir='CVS'
  65.281 +                      , mod_time=BEFORE )
  65.282 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='.svn'
  65.283 +                      , mod_time=BEFORE )
  65.284 +
  65.285 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nQRST', subdir='CVS'
  65.286 +                      , mod_time=AFTER )
  65.287 +        self._makeFile( 'rhs', 'again.txt', 'MNOPQR', subdir='.svn'
  65.288 +                      , mod_time=AFTER )
  65.289 +
  65.290 +        cd = self._makeOne( lhs, rhs )
  65.291 +
  65.292 +        diffs = cd.compare()
  65.293 +
  65.294 +        self.assertEqual( diffs, '' )
  65.295 +
  65.296 +    def test_compare_added_file_no_missing_as_empty( self ):
  65.297 +
  65.298 +        lhs = self._makeContext( 'lhs' )
  65.299 +        rhs = self._makeContext( 'rhs' )
  65.300 +
  65.301 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ' )
  65.302 +        self._makeDirectory( 'lhs', subdir='sub' )
  65.303 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nWXYZ' )
  65.304 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub' )
  65.305 +
  65.306 +        cd = self._makeOne( lhs, rhs )
  65.307 +
  65.308 +        diffs = cd.compare()
  65.309 +
  65.310 +        self.assertEqual( diffs, ADDED_FILE_DIFFS_NO_MAE )
  65.311 +
  65.312 +    def test_compare_added_file_missing_as_empty( self ):
  65.313 +
  65.314 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  65.315 +        lhs = self._makeContext( 'lhs' )
  65.316 +        rhs = self._makeContext( 'rhs' )
  65.317 +
  65.318 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ' )
  65.319 +        self._makeDirectory( 'lhs', subdir='sub' )
  65.320 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nWXYZ' )
  65.321 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub'
  65.322 +                      , mod_time=AFTER )
  65.323 +
  65.324 +        cd = self._makeOne( lhs, rhs, missing_as_empty=True )
  65.325 +
  65.326 +        diffs = cd.compare()
  65.327 +
  65.328 +        self.assertEqual( diffs, ADDED_FILE_DIFFS_MAE % AFTER )
  65.329 +
  65.330 +    def test_compare_removed_file_no_missing_as_empty( self ):
  65.331 +
  65.332 +        lhs = self._makeContext( 'lhs' )
  65.333 +        rhs = self._makeContext( 'rhs' )
  65.334 +
  65.335 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ' )
  65.336 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub' )
  65.337 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nWXYZ' )
  65.338 +        self._makeDirectory( 'rhs', subdir='sub' )
  65.339 +
  65.340 +        cd = self._makeOne( lhs, rhs )
  65.341 +
  65.342 +        diffs = cd.compare()
  65.343 +
  65.344 +        self.assertEqual( diffs, REMOVED_FILE_DIFFS_NO_MAE )
  65.345 +
  65.346 +    def test_compare_removed_file_missing_as_empty( self ):
  65.347 +
  65.348 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  65.349 +        lhs = self._makeContext( 'lhs' )
  65.350 +        rhs = self._makeContext( 'rhs' )
  65.351 +
  65.352 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ' )
  65.353 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub'
  65.354 +                      , mod_time=BEFORE )
  65.355 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nWXYZ' )
  65.356 +        self._makeDirectory( 'rhs', subdir='sub' )
  65.357 +
  65.358 +        cd = self._makeOne( lhs, rhs, missing_as_empty=True )
  65.359 +
  65.360 +        diffs = cd.compare()
  65.361 +
  65.362 +        self.assertEqual( diffs, REMOVED_FILE_DIFFS_MAE % BEFORE )
  65.363 +
  65.364 +
  65.365 +TEST_TXT_DIFFS = """\
  65.366 +Index: test.txt
  65.367 +===================================================================
  65.368 +--- test.txt %s
  65.369 ++++ test.txt %s
  65.370 +@@ -1,2 +1,2 @@
  65.371 + ABCDEF
  65.372 +-WXYZ
  65.373 ++QRST\
  65.374 +"""
  65.375 +
  65.376 +ADDED_FILE_DIFFS_NO_MAE = """\
  65.377 +** File sub/again.txt added
  65.378 +"""
  65.379 +
  65.380 +ADDED_FILE_DIFFS_MAE = """\
  65.381 +Index: sub/again.txt
  65.382 +===================================================================
  65.383 +--- sub/again.txt 0
  65.384 ++++ sub/again.txt %s
  65.385 +@@ -1,0 +1,1 @@
  65.386 ++GHIJKL\
  65.387 +"""
  65.388 +
  65.389 +REMOVED_FILE_DIFFS_NO_MAE = """\
  65.390 +** File sub/again.txt removed
  65.391 +"""
  65.392 +
  65.393 +REMOVED_FILE_DIFFS_MAE = """\
  65.394 +Index: sub/again.txt
  65.395 +===================================================================
  65.396 +--- sub/again.txt %s
  65.397 ++++ sub/again.txt 0
  65.398 +@@ -1,1 +1,0 @@
  65.399 +-GHIJKL\
  65.400 +"""
  65.401 +
  65.402 +
  65.403 +def test_suite():
  65.404 +    return unittest.TestSuite((
  65.405 +        unittest.makeSuite( Test_unidiff ),
  65.406 +        unittest.makeSuite( ConfigDiffTests ),
  65.407 +        ))
  65.408 +
  65.409 +if __name__ == '__main__':
  65.410 +    unittest.main(defaultTest='test_suite')
    66.1 new file mode 100644
    66.2 --- /dev/null
    66.3 +++ b/tests/test_registry.py
    66.4 @@ -0,0 +1,1174 @@
    66.5 +##############################################################################
    66.6 +#
    66.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    66.8 +#
    66.9 +# This software is subject to the provisions of the Zope Public License,
   66.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   66.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   66.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   66.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   66.14 +# FOR A PARTICULAR PURPOSE.
   66.15 +#
   66.16 +##############################################################################
   66.17 +""" Registry unit tests.
   66.18 +
   66.19 +$Id: test_registry.py 40299 2005-11-21 16:49:38Z tseaver $
   66.20 +"""
   66.21 +
   66.22 +import unittest
   66.23 +import Testing
   66.24 +import Zope2
   66.25 +Zope2.startup()
   66.26 +
   66.27 +from OFS.Folder import Folder
   66.28 +from Products.GenericSetup.tests.common import BaseRegistryTests
   66.29 +from Products.GenericSetup import EXTENSION
   66.30 +from zope.interface import Interface
   66.31 +
   66.32 +from conformance import ConformsToIStepRegistry
   66.33 +from conformance import ConformsToIImportStepRegistry
   66.34 +from conformance import ConformsToIExportStepRegistry
   66.35 +from conformance import ConformsToIToolsetRegistry
   66.36 +from conformance import ConformsToIProfileRegistry
   66.37 +
   66.38 +
   66.39 +#==============================================================================
   66.40 +#   Dummy handlers
   66.41 +#==============================================================================
   66.42 +def ONE_FUNC( context ): pass
   66.43 +def TWO_FUNC( context ): pass
   66.44 +def THREE_FUNC( context ): pass
   66.45 +def FOUR_FUNC( context ): pass
   66.46 +
   66.47 +ONE_FUNC_NAME = '%s.%s' % ( __name__, ONE_FUNC.__name__ )
   66.48 +TWO_FUNC_NAME = '%s.%s' % ( __name__, TWO_FUNC.__name__ )
   66.49 +THREE_FUNC_NAME = '%s.%s' % ( __name__, THREE_FUNC.__name__ )
   66.50 +FOUR_FUNC_NAME = '%s.%s' % ( __name__, FOUR_FUNC.__name__ )
   66.51 +
   66.52 +
   66.53 +#==============================================================================
   66.54 +#   SSR tests
   66.55 +#==============================================================================
   66.56 +class ImportStepRegistryTests( BaseRegistryTests
   66.57 +                             , ConformsToIStepRegistry
   66.58 +                             , ConformsToIImportStepRegistry
   66.59 +                             ):
   66.60 +
   66.61 +    def _getTargetClass( self ):
   66.62 +
   66.63 +        from Products.GenericSetup.registry import ImportStepRegistry
   66.64 +        return ImportStepRegistry
   66.65 +
   66.66 +    def test_empty( self ):
   66.67 +
   66.68 +        registry = self._makeOne()
   66.69 +
   66.70 +        self.assertEqual( len( registry.listSteps() ), 0 )
   66.71 +        self.assertEqual( len( registry.listStepMetadata() ), 0 )
   66.72 +        self.assertEqual( len( registry.sortSteps() ), 0 )
   66.73 +
   66.74 +    def test_getStep_nonesuch( self ):
   66.75 +
   66.76 +        registry = self._makeOne()
   66.77 +
   66.78 +        self.assertEqual( registry.getStep( 'nonesuch' ), None )
   66.79 +        self.assertEqual( registry.getStep( 'nonesuch' ), None )
   66.80 +        default = object()
   66.81 +        self.failUnless( registry.getStepMetadata( 'nonesuch'
   66.82 +                                                 , default ) is default )
   66.83 +        self.failUnless( registry.getStep( 'nonesuch', default ) is default )
   66.84 +        self.failUnless( registry.getStepMetadata( 'nonesuch'
   66.85 +                                                 , default ) is default )
   66.86 +
   66.87 +    def test_getStep_defaulted( self ):
   66.88 +
   66.89 +        registry = self._makeOne()
   66.90 +        default = object()
   66.91 +
   66.92 +        self.failUnless( registry.getStep( 'nonesuch', default ) is default )
   66.93 +        self.assertEqual( registry.getStepMetadata( 'nonesuch', {} ), {} )
   66.94 +
   66.95 +    def test_registerStep_docstring( self ):
   66.96 +
   66.97 +        def func_with_doc( site ):
   66.98 +            """This is the first line.
   66.99 +
  66.100 +            This is the second line.
  66.101 +            """
  66.102 +        FUNC_NAME = '%s.%s' % ( __name__, func_with_doc.__name__ )
  66.103 +
  66.104 +        registry = self._makeOne()
  66.105 +
  66.106 +        registry.registerStep( id='docstring'
  66.107 +                             , version='1'
  66.108 +                             , handler=func_with_doc
  66.109 +                             , dependencies=()
  66.110 +                             )
  66.111 +
  66.112 +        info = registry.getStepMetadata( 'docstring' )
  66.113 +        self.assertEqual( info[ 'id' ], 'docstring' )
  66.114 +        self.assertEqual( info[ 'handler' ], FUNC_NAME )
  66.115 +        self.assertEqual( info[ 'dependencies' ], () )
  66.116 +        self.assertEqual( info[ 'title' ], 'This is the first line.' )
  66.117 +        self.assertEqual( info[ 'description' ] , 'This is the second line.' )
  66.118 +
  66.119 +    def test_registerStep_docstring_override( self ):
  66.120 +
  66.121 +        def func_with_doc( site ):
  66.122 +            """This is the first line.
  66.123 +
  66.124 +            This is the second line.
  66.125 +            """
  66.126 +        FUNC_NAME = '%s.%s' % ( __name__, func_with_doc.__name__ )
  66.127 +
  66.128 +        registry = self._makeOne()
  66.129 +
  66.130 +        registry.registerStep( id='docstring'
  66.131 +                             , version='1'
  66.132 +                             , handler=func_with_doc
  66.133 +                             , dependencies=()
  66.134 +                             , title='Title'
  66.135 +                             )
  66.136 +
  66.137 +        info = registry.getStepMetadata( 'docstring' )
  66.138 +        self.assertEqual( info[ 'id' ], 'docstring' )
  66.139 +        self.assertEqual( info[ 'handler' ], FUNC_NAME )
  66.140 +        self.assertEqual( info[ 'dependencies' ], () )
  66.141 +        self.assertEqual( info[ 'title' ], 'Title' )
  66.142 +        self.assertEqual( info[ 'description' ] , 'This is the second line.' )
  66.143 +
  66.144 +    def test_registerStep_single( self ):
  66.145 +
  66.146 +        registry = self._makeOne()
  66.147 +
  66.148 +        registry.registerStep( id='one'
  66.149 +                             , version='1'
  66.150 +                             , handler=ONE_FUNC
  66.151 +                             , dependencies=( 'two', 'three' )
  66.152 +                             , title='One Step'
  66.153 +                             , description='One small step'
  66.154 +                             )
  66.155 +
  66.156 +        steps = registry.listSteps()
  66.157 +        self.assertEqual( len( steps ), 1 )
  66.158 +        self.failUnless( 'one' in steps )
  66.159 +
  66.160 +        sorted = registry.sortSteps()
  66.161 +        self.assertEqual( len( sorted ), 1 )
  66.162 +        self.assertEqual( sorted[ 0 ], 'one' )
  66.163 +
  66.164 +        self.assertEqual( registry.getStep( 'one' ), ONE_FUNC )
  66.165 +
  66.166 +        info = registry.getStepMetadata( 'one' )
  66.167 +        self.assertEqual( info[ 'id' ], 'one' )
  66.168 +        self.assertEqual( info[ 'version' ], '1' )
  66.169 +        self.assertEqual( info[ 'handler' ], ONE_FUNC_NAME )
  66.170 +        self.assertEqual( info[ 'dependencies' ], ( 'two', 'three' ) )
  66.171 +        self.assertEqual( info[ 'title' ], 'One Step' )
  66.172 +        self.assertEqual( info[ 'description' ], 'One small step' )
  66.173 +
  66.174 +        info_list = registry.listStepMetadata()
  66.175 +        self.assertEqual( len( info_list ), 1 )
  66.176 +        self.assertEqual( info, info_list[ 0 ] )
  66.177 +
  66.178 +    def test_registerStep_conflict( self ):
  66.179 +
  66.180 +        registry = self._makeOne()
  66.181 +
  66.182 +        registry.registerStep( id='one', version='1', handler=ONE_FUNC )
  66.183 +
  66.184 +        self.assertRaises( KeyError
  66.185 +                         , registry.registerStep
  66.186 +                         , id='one'
  66.187 +                         , version='0'
  66.188 +                         , handler=ONE_FUNC
  66.189 +                         )
  66.190 +
  66.191 +        registry.registerStep( id='one', version='1', handler=ONE_FUNC )
  66.192 +
  66.193 +        info_list = registry.listStepMetadata()
  66.194 +        self.assertEqual( len( info_list ), 1 )
  66.195 +
  66.196 +    def test_registerStep_replacement( self ):
  66.197 +
  66.198 +        registry = self._makeOne()
  66.199 +
  66.200 +        registry.registerStep( id='one'
  66.201 +                             , version='1'
  66.202 +                             , handler=ONE_FUNC
  66.203 +                             , dependencies=( 'two', 'three' )
  66.204 +                             , title='One Step'
  66.205 +                             , description='One small step'
  66.206 +                             )
  66.207 +
  66.208 +        registry.registerStep( id='one'
  66.209 +                             , version='1.1'
  66.210 +                             , handler=ONE_FUNC
  66.211 +                             , dependencies=()
  66.212 +                             , title='Leads to Another'
  66.213 +                             , description='Another small step'
  66.214 +                             )
  66.215 +
  66.216 +        info = registry.getStepMetadata( 'one' )
  66.217 +        self.assertEqual( info[ 'id' ], 'one' )
  66.218 +        self.assertEqual( info[ 'version' ], '1.1' )
  66.219 +        self.assertEqual( info[ 'dependencies' ], () )
  66.220 +        self.assertEqual( info[ 'title' ], 'Leads to Another' )
  66.221 +        self.assertEqual( info[ 'description' ], 'Another small step' )
  66.222 +
  66.223 +    def test_registerStep_multiple( self ):
  66.224 +
  66.225 +        registry = self._makeOne()
  66.226 +
  66.227 +        registry.registerStep( id='one'
  66.228 +                             , version='1'
  66.229 +                             , handler=ONE_FUNC
  66.230 +                             , dependencies=()
  66.231 +                             )
  66.232 +
  66.233 +        registry.registerStep( id='two'
  66.234 +                             , version='2'
  66.235 +                             , handler=TWO_FUNC
  66.236 +                             , dependencies=()
  66.237 +                             )
  66.238 +
  66.239 +        registry.registerStep( id='three'
  66.240 +                             , version='3'
  66.241 +                             , handler=THREE_FUNC
  66.242 +                             , dependencies=()
  66.243 +                             )
  66.244 +
  66.245 +        steps = registry.listSteps()
  66.246 +        self.assertEqual( len( steps ), 3 )
  66.247 +        self.failUnless( 'one' in steps )
  66.248 +        self.failUnless( 'two' in steps )
  66.249 +        self.failUnless( 'three' in steps )
  66.250 +
  66.251 +    def test_sortStep_simple( self ):
  66.252 +
  66.253 +        registry = self._makeOne()
  66.254 +
  66.255 +        registry.registerStep( id='one'
  66.256 +                             , version='1'
  66.257 +                             , handler=ONE_FUNC
  66.258 +                             , dependencies=( 'two', )
  66.259 +                             )
  66.260 +
  66.261 +        registry.registerStep( id='two'
  66.262 +                             , version='2'
  66.263 +                             , handler=TWO_FUNC
  66.264 +                             , dependencies=()
  66.265 +                             )
  66.266 +
  66.267 +        steps = registry.sortSteps()
  66.268 +        self.assertEqual( len( steps ), 2 )
  66.269 +        one = steps.index( 'one' )
  66.270 +        two = steps.index( 'two' )
  66.271 +
  66.272 +        self.failUnless( 0 <= two < one )
  66.273 +
  66.274 +    def test_sortStep_chained( self ):
  66.275 +
  66.276 +        registry = self._makeOne()
  66.277 +
  66.278 +        registry.registerStep( id='one'
  66.279 +                             , version='1'
  66.280 +                             , handler=ONE_FUNC
  66.281 +                             , dependencies=( 'two', )
  66.282 +                             , title='One small step'
  66.283 +                             )
  66.284 +
  66.285 +        registry.registerStep( id='two'
  66.286 +                             , version='2'
  66.287 +                             , handler=TWO_FUNC
  66.288 +                             , dependencies=( 'three', )
  66.289 +                             , title='Texas two step'
  66.290 +                             )
  66.291 +
  66.292 +        registry.registerStep( id='three'
  66.293 +                             , version='3'
  66.294 +                             , handler=THREE_FUNC
  66.295 +                             , dependencies=()
  66.296 +                             , title='Gimme three steps'
  66.297 +                             )
  66.298 +
  66.299 +        steps = registry.sortSteps()
  66.300 +        self.assertEqual( len( steps ), 3 )
  66.301 +        one = steps.index( 'one' )
  66.302 +        two = steps.index( 'two' )
  66.303 +        three = steps.index( 'three' )
  66.304 +
  66.305 +        self.failUnless( 0 <= three < two < one )
  66.306 +
  66.307 +    def test_sortStep_complex( self ):
  66.308 +
  66.309 +        registry = self._makeOne()
  66.310 +
  66.311 +        registry.registerStep( id='one'
  66.312 +                             , version='1'
  66.313 +                             , handler=ONE_FUNC
  66.314 +                             , dependencies=( 'two', )
  66.315 +                             , title='One small step'
  66.316 +                             )
  66.317 +
  66.318 +        registry.registerStep( id='two'
  66.319 +                             , version='2'
  66.320 +                             , handler=TWO_FUNC
  66.321 +                             , dependencies=( 'four', )
  66.322 +                             , title='Texas two step'
  66.323 +                             )
  66.324 +
  66.325 +        registry.registerStep( id='three'
  66.326 +                             , version='3'
  66.327 +                             , handler=THREE_FUNC
  66.328 +                             , dependencies=( 'four', )
  66.329 +                             , title='Gimme three steps'
  66.330 +                             )
  66.331 +
  66.332 +        registry.registerStep( id='four'
  66.333 +                             , version='4'
  66.334 +                             , handler=FOUR_FUNC
  66.335 +                             , dependencies=()
  66.336 +                             , title='Four step program'
  66.337 +                             )
  66.338 +
  66.339 +        steps = registry.sortSteps()
  66.340 +        self.assertEqual( len( steps ), 4 )
  66.341 +        one = steps.index( 'one' )
  66.342 +        two = steps.index( 'two' )
  66.343 +        three = steps.index( 'three' )
  66.344 +        four = steps.index( 'four' )
  66.345 +
  66.346 +        self.failUnless( 0 <= four < two < one )
  66.347 +        self.failUnless( 0 <= four < three )
  66.348 +
  66.349 +    def test_sortStep_equivalence( self ):
  66.350 +
  66.351 +        registry = self._makeOne()
  66.352 +
  66.353 +        registry.registerStep( id='one'
  66.354 +                             , version='1'
  66.355 +                             , handler=ONE_FUNC
  66.356 +                             , dependencies=( 'two', 'three' )
  66.357 +                             , title='One small step'
  66.358 +                             )
  66.359 +
  66.360 +        registry.registerStep( id='two'
  66.361 +                             , version='2'
  66.362 +                             , handler=TWO_FUNC
  66.363 +                             , dependencies=( 'four', )
  66.364 +                             , title='Texas two step'
  66.365 +                             )
  66.366 +
  66.367 +        registry.registerStep( id='three'
  66.368 +                             , version='3'
  66.369 +                             , handler=THREE_FUNC
  66.370 +                             , dependencies=( 'four', )
  66.371 +                             , title='Gimme three steps'
  66.372 +                             )
  66.373 +
  66.374 +        registry.registerStep( id='four'
  66.375 +                             , version='4'
  66.376 +                             , handler=FOUR_FUNC
  66.377 +                             , dependencies=()
  66.378 +                             , title='Four step program'
  66.379 +                             )
  66.380 +
  66.381 +        steps = registry.sortSteps()
  66.382 +        self.assertEqual( len( steps ), 4 )
  66.383 +        one = steps.index( 'one' )
  66.384 +        two = steps.index( 'two' )
  66.385 +        three = steps.index( 'three' )
  66.386 +        four = steps.index( 'four' )
  66.387 +
  66.388 +        self.failUnless( 0 <= four < two < one )
  66.389 +        self.failUnless( 0 <= four < three < one )
  66.390 +
  66.391 +    def test_checkComplete_simple( self ):
  66.392 +
  66.393 +        registry = self._makeOne()
  66.394 +
  66.395 +        registry.registerStep( id='one'
  66.396 +                             , version='1'
  66.397 +                             , handler=ONE_FUNC
  66.398 +                             , dependencies=( 'two', )
  66.399 +                             )
  66.400 +
  66.401 +        incomplete = registry.checkComplete()
  66.402 +        self.assertEqual( len( incomplete ), 1 )
  66.403 +        self.failUnless( ( 'one', 'two' ) in incomplete )
  66.404 +
  66.405 +        registry.registerStep( id='two'
  66.406 +                             , version='2'
  66.407 +                             , handler=TWO_FUNC
  66.408 +                             , dependencies=()
  66.409 +                             )
  66.410 +
  66.411 +        self.assertEqual( len( registry.checkComplete() ), 0 )
  66.412 +
  66.413 +    def test_checkComplete_double( self ):
  66.414 +
  66.415 +        registry = self._makeOne()
  66.416 +
  66.417 +        registry.registerStep( id='one'
  66.418 +                             , version='1'
  66.419 +                             , handler=ONE_FUNC
  66.420 +                             , dependencies=( 'two', 'three' )
  66.421 +                             )
  66.422 +
  66.423 +        incomplete = registry.checkComplete()
  66.424 +        self.assertEqual( len( incomplete ), 2 )
  66.425 +        self.failUnless( ( 'one', 'two' ) in incomplete )
  66.426 +        self.failUnless( ( 'one', 'three' ) in incomplete )
  66.427 +
  66.428 +        registry.registerStep( id='two'
  66.429 +                             , version='2'
  66.430 +                             , handler=TWO_FUNC
  66.431 +                             , dependencies=()
  66.432 +                             )
  66.433 +
  66.434 +        incomplete = registry.checkComplete()
  66.435 +        self.assertEqual( len( incomplete ), 1 )
  66.436 +        self.failUnless( ( 'one', 'three' ) in incomplete )
  66.437 +
  66.438 +        registry.registerStep( id='three'
  66.439 +                             , version='3'
  66.440 +                             , handler=THREE_FUNC
  66.441 +                             , dependencies=()
  66.442 +                             )
  66.443 +
  66.444 +        self.assertEqual( len( registry.checkComplete() ), 0 )
  66.445 +
  66.446 +        registry.registerStep( id='two'
  66.447 +                             , version='2.1'
  66.448 +                             , handler=TWO_FUNC
  66.449 +                             , dependencies=( 'four', )
  66.450 +                             )
  66.451 +
  66.452 +        incomplete = registry.checkComplete()
  66.453 +        self.assertEqual( len( incomplete ), 1 )
  66.454 +        self.failUnless( ( 'two', 'four' ) in incomplete )
  66.455 +
  66.456 +    def test_generateXML_empty( self ):
  66.457 +
  66.458 +        registry = self._makeOne().__of__( self.root )
  66.459 +
  66.460 +        xml = registry.generateXML()