vendor/CMF/1.6.0/GenericSetup

changeset 0:113a50be4419 GenericSetup

CMF 1.6.0
author fguillaume
date Mon, 27 Feb 2006 13:41:18 +0000
parents
children 66ee37a2b2d7
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 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 doc/handlers.txt doc/profiles.txt 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 84 files changed, 14095 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,70 @@
     1.5 +GenericSetup Product Changelog
     1.6 +
     1.7 +  After GenericSetup 1.0
     1.8 +
     1.9 +    - Allowed subclasses of DAVAwareFileAdapter to override the filename
    1.10 +      in which the file is stored.
    1.11 +
    1.12 +    - Added a doc directory including some basic documentation.
    1.13 +
    1.14 +    - Made GenericSetup a standalone package independent of the CMF
    1.15 +
    1.16 +    - Added 'for_' argument to profile registry operations.
    1.17 +      A profile may be registered and queried as appropriate to a specific
    1.18 +      site interface;  the default value, 'None', indicates that the profile
    1.19 +      is relevant to any site.  Note that this is essentially an adapter
    1.20 +      lookup;  perhaps we should reimplement it so.
    1.21 +
    1.22 +    - Forward ported changes from GenericSetup 0.11 and 0.12 (which were
    1.23 +      created in a separate repository).
    1.24 +
    1.25 +    - A sequence property with the purge="False" attribute will not be
    1.26 +      purged, but merged (the sequences are treated as sets, which means
    1.27 +      that duplicates are removed). This is useful in extension profiles.
    1.28 +
    1.29 +    - Don't export or purge read-only properties. Correctly purge
    1.30 +      non-deletable int/float properties.
    1.31 +
    1.32 +    - Correctly quote XML on export.
    1.33 +
    1.34 +  GenericSetup 1.0 (2005/09/23)
    1.35 +
    1.36 +    - CVS tag:  GenericSetup-1_0
    1.37 +
    1.38 +    - Forward-ported i18n support from CMF 1.5 branch.
    1.39 +
    1.40 +    - Forward ported BBB for old instances that stored properties as
    1.41 +      lists from CMFSetup.
    1.42 +
    1.43 +    - Forward ported fix for tools with non unique IDs from CMFSetup.
    1.44 +
    1.45 +  GenericSetup-0.12 (2005/08/29)
    1.46 +
    1.47 +    - CVS tag:  GenericSetup-0_12
    1.48 +
    1.49 +    - Import requests now create reports (by default) which record any
    1.50 +      status messages generated by the profile's steps.
    1.51 +
    1.52 +  GenericSetup-0.11 (2005/08/23)
    1.53 +
    1.54 +    - CVS tag:  GenericSetup-0_11
    1.55 +
    1.56 +    - Added report of messages generated by import to the "Import" tab.
    1.57 +
    1.58 +    - Consolidated ISetupContext implementation into base class,
    1.59 +      'SetupContextBase'.
    1.60 +
    1.61 +    - Added 'note', 'listNotes', and 'clearNotes'  methods to ISetupContext,
    1.62 +      to allow plugins to record information about the state of the operation.
    1.63 +
    1.64 +  GenericSetup 0.10 (2005/08/11)
    1.65 +
    1.66 +    - CVS tag:  GenericSetup-0_10
    1.67 +
    1.68 +    - Added TarballImportContext, including full test suite.
    1.69 +
    1.70 +  GenericSetup 0.9 (2005/08/08)
    1.71 +
    1.72 +    - CVS tag:  GenericSetup-0_9
    1.73 +
    1.74 +    - 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/PluginIndexes/__init__.py
    14.4 @@ -0,0 +1,16 @@
    14.5 +##############################################################################
    14.6 +#
    14.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    14.8 +#
    14.9 +# This software is subject to the provisions of the Zope Public License,
   14.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   14.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   14.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   14.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   14.14 +# FOR A PARTICULAR PURPOSE.
   14.15 +#
   14.16 +##############################################################################
   14.17 +"""PluginIndexes support.
   14.18 +
   14.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   14.20 +"""
    15.1 new file mode 100644
    15.2 --- /dev/null
    15.3 +++ b/PluginIndexes/configure.zcml
    15.4 @@ -0,0 +1,61 @@
    15.5 +<configure
    15.6 +    xmlns="http://namespaces.zope.org/zope"
    15.7 +    >
    15.8 +
    15.9 +  <adapter
   15.10 +      factory=".exportimport.PluggableIndexNodeAdapter"
   15.11 +      provides="Products.GenericSetup.interfaces.INode"
   15.12 +      for="Products.PluginIndexes.interfaces.IPluggableIndex
   15.13 +           Products.GenericSetup.interfaces.ISetupEnviron"
   15.14 +      />
   15.15 +
   15.16 +  <adapter
   15.17 +      factory=".exportimport.DateIndexNodeAdapter"
   15.18 +      provides="Products.GenericSetup.interfaces.INode"
   15.19 +      for="Products.PluginIndexes.interfaces.IDateIndex
   15.20 +           Products.GenericSetup.interfaces.ISetupEnviron"
   15.21 +      />
   15.22 +
   15.23 +  <adapter
   15.24 +      factory=".exportimport.DateRangeIndexNodeAdapter"
   15.25 +      provides="Products.GenericSetup.interfaces.INode"
   15.26 +      for="Products.PluginIndexes.interfaces.IDateRangeIndex
   15.27 +           Products.GenericSetup.interfaces.ISetupEnviron"
   15.28 +      />
   15.29 +
   15.30 +  <adapter
   15.31 +      factory=".exportimport.PathIndexNodeAdapter"
   15.32 +      provides="Products.GenericSetup.interfaces.INode"
   15.33 +      for="Products.PluginIndexes.interfaces.IPathIndex
   15.34 +           Products.GenericSetup.interfaces.ISetupEnviron"
   15.35 +      />
   15.36 +
   15.37 +  <adapter
   15.38 +      factory=".exportimport.VocabularyNodeAdapter"
   15.39 +      provides="Products.GenericSetup.interfaces.INode"
   15.40 +      for="Products.PluginIndexes.interfaces.IVocabulary
   15.41 +           Products.GenericSetup.interfaces.ISetupEnviron"
   15.42 +      />
   15.43 +
   15.44 +  <adapter
   15.45 +      factory=".exportimport.TextIndexNodeAdapter"
   15.46 +      provides="Products.GenericSetup.interfaces.INode"
   15.47 +      for="Products.PluginIndexes.interfaces.ITextIndex
   15.48 +           Products.GenericSetup.interfaces.ISetupEnviron"
   15.49 +      />
   15.50 +
   15.51 +  <adapter
   15.52 +      factory=".exportimport.FilteredSetNodeAdapter"
   15.53 +      provides="Products.GenericSetup.interfaces.INode"
   15.54 +      for="Products.PluginIndexes.interfaces.IFilteredSet
   15.55 +           Products.GenericSetup.interfaces.ISetupEnviron"
   15.56 +      />
   15.57 +
   15.58 +  <adapter
   15.59 +      factory=".exportimport.TopicIndexNodeAdapter"
   15.60 +      provides="Products.GenericSetup.interfaces.INode"
   15.61 +      for="Products.PluginIndexes.interfaces.ITopicIndex
   15.62 +           Products.GenericSetup.interfaces.ISetupEnviron"
   15.63 +      />
   15.64 +
   15.65 +</configure>
    16.1 new file mode 100644
    16.2 --- /dev/null
    16.3 +++ b/PluginIndexes/exportimport.py
    16.4 @@ -0,0 +1,219 @@
    16.5 +##############################################################################
    16.6 +#
    16.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    16.8 +#
    16.9 +# This software is subject to the provisions of the Zope Public License,
   16.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   16.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   16.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   16.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   16.14 +# FOR A PARTICULAR PURPOSE.
   16.15 +#
   16.16 +##############################################################################
   16.17 +"""PluginIndexes export / import support.
   16.18 +
   16.19 +$Id: exportimport.py 41445 2006-01-25 18:32:47Z yuppie $
   16.20 +"""
   16.21 +
   16.22 +from zope.app import zapi
   16.23 +
   16.24 +from Products.GenericSetup.interfaces import INode
   16.25 +from Products.GenericSetup.utils import NodeAdapterBase
   16.26 +from Products.GenericSetup.utils import PropertyManagerHelpers
   16.27 +
   16.28 +from Products.PluginIndexes.interfaces import IDateIndex
   16.29 +from Products.PluginIndexes.interfaces import IDateRangeIndex
   16.30 +from Products.PluginIndexes.interfaces import IFilteredSet
   16.31 +from Products.PluginIndexes.interfaces import IPathIndex
   16.32 +from Products.PluginIndexes.interfaces import IPluggableIndex
   16.33 +from Products.PluginIndexes.interfaces import ITextIndex
   16.34 +from Products.PluginIndexes.interfaces import ITopicIndex
   16.35 +from Products.PluginIndexes.interfaces import IVocabulary
   16.36 +
   16.37 +
   16.38 +class PluggableIndexNodeAdapter(NodeAdapterBase):
   16.39 +
   16.40 +    """Node im- and exporter for FieldIndex, KeywordIndex.
   16.41 +    """
   16.42 +
   16.43 +    __used_for__ = IPluggableIndex
   16.44 +
   16.45 +    def _exportNode(self):
   16.46 +        """Export the object as a DOM node.
   16.47 +        """
   16.48 +        node = self._getObjectNode('index')
   16.49 +        for value in self.context.getIndexSourceNames():
   16.50 +            child = self._doc.createElement('indexed_attr')
   16.51 +            child.setAttribute('value', value)
   16.52 +            node.appendChild(child)
   16.53 +        return node
   16.54 +
   16.55 +    def _importNode(self, node):
   16.56 +        """Import the object from the DOM node.
   16.57 +        """
   16.58 +        indexed_attrs = []
   16.59 +        for child in node.childNodes:
   16.60 +            if child.nodeName == 'indexed_attr':
   16.61 +                indexed_attrs.append(
   16.62 +                                  child.getAttribute('value').encode('utf-8'))
   16.63 +        self.context.indexed_attrs = indexed_attrs
   16.64 +        self.context.clear()
   16.65 +
   16.66 +    node = property(_exportNode, _importNode)
   16.67 +
   16.68 +
   16.69 +class DateIndexNodeAdapter(NodeAdapterBase, PropertyManagerHelpers):
   16.70 +
   16.71 +    """Node im- and exporter for DateIndex.
   16.72 +    """
   16.73 +
   16.74 +    __used_for__ = IDateIndex
   16.75 +
   16.76 +    def _exportNode(self):
   16.77 +        """Export the object as a DOM node.
   16.78 +        """
   16.79 +        node = self._getObjectNode('index')
   16.80 +        node.appendChild(self._extractProperties())
   16.81 +        return node
   16.82 +
   16.83 +    def _importNode(self, node):
   16.84 +        """Import the object from the DOM node.
   16.85 +        """
   16.86 +        if self.environ.shouldPurge():
   16.87 +            self._purgeProperties()
   16.88 +
   16.89 +        self._initProperties(node)
   16.90 +        self.context.clear()
   16.91 +
   16.92 +    node = property(_exportNode, _importNode)
   16.93 +
   16.94 +
   16.95 +class DateRangeIndexNodeAdapter(NodeAdapterBase):
   16.96 +
   16.97 +    """Node im- and exporter for DateRangeIndex.
   16.98 +    """
   16.99 +
  16.100 +    __used_for__ = IDateRangeIndex
  16.101 +
  16.102 +    def _exportNode(self):
  16.103 +        """Export the object as a DOM node.
  16.104 +        """
  16.105 +        node = self._getObjectNode('index')
  16.106 +        node.setAttribute('since_field', self.context.getSinceField())
  16.107 +        node.setAttribute('until_field', self.context.getUntilField())
  16.108 +        return node
  16.109 +
  16.110 +    def _importNode(self, node):
  16.111 +        """Import the object from the DOM node.
  16.112 +        """
  16.113 +        self.context._edit(node.getAttribute('since_field').encode('utf-8'),
  16.114 +                           node.getAttribute('until_field').encode('utf-8'))
  16.115 +        self.context.clear()
  16.116 +
  16.117 +    node = property(_exportNode, _importNode)
  16.118 +
  16.119 +
  16.120 +class PathIndexNodeAdapter(NodeAdapterBase):
  16.121 +
  16.122 +    """Node im- and exporter for PathIndex.
  16.123 +    """
  16.124 +
  16.125 +    __used_for__ = IPathIndex
  16.126 +
  16.127 +    def _exportNode(self):
  16.128 +        """Export the object as a DOM node.
  16.129 +        """
  16.130 +        return self._getObjectNode('index')
  16.131 +
  16.132 +    node = property(_exportNode, lambda self, val: None)
  16.133 +
  16.134 +
  16.135 +class VocabularyNodeAdapter(NodeAdapterBase):
  16.136 +
  16.137 +    """Node im- and exporter for Vocabulary.
  16.138 +    """
  16.139 +
  16.140 +    __used_for__ = IVocabulary
  16.141 +
  16.142 +    def _exportNode(self):
  16.143 +        """Export the object as a DOM node.
  16.144 +        """
  16.145 +        node = self._getObjectNode('object')
  16.146 +        node.setAttribute('deprecated', 'True')
  16.147 +        return node
  16.148 +
  16.149 +    node = property(_exportNode, lambda self, val: None)
  16.150 +
  16.151 +
  16.152 +class TextIndexNodeAdapter(NodeAdapterBase):
  16.153 +
  16.154 +    """Node im- and exporter for TextIndex.
  16.155 +    """
  16.156 +
  16.157 +    __used_for__ = ITextIndex
  16.158 +
  16.159 +    def _exportNode(self):
  16.160 +        """Export the object as a DOM node.
  16.161 +        """
  16.162 +        node = self._getObjectNode('index')
  16.163 +        node.setAttribute('deprecated', 'True')
  16.164 +        return node
  16.165 +
  16.166 +    node = property(_exportNode, lambda self, val: None)
  16.167 +
  16.168 +
  16.169 +class FilteredSetNodeAdapter(NodeAdapterBase):
  16.170 +
  16.171 +    """Node im- and exporter for FilteredSet.
  16.172 +    """
  16.173 +
  16.174 +    __used_for__ = IFilteredSet
  16.175 +
  16.176 +    def _exportNode(self):
  16.177 +        """Export the object as a DOM node.
  16.178 +        """
  16.179 +        node = self._getObjectNode('filtered_set')
  16.180 +        node.setAttribute('expression', self.context.getExpression())
  16.181 +        return node
  16.182 +
  16.183 +    def _importNode(self, node):
  16.184 +        """Import the object from the DOM node.
  16.185 +        """
  16.186 +        self.context.setExpression(
  16.187 +                              node.getAttribute('expression').encode('utf-8'))
  16.188 +        self.context.clear()
  16.189 +
  16.190 +    node = property(_exportNode, _importNode)
  16.191 +
  16.192 +
  16.193 +class TopicIndexNodeAdapter(NodeAdapterBase):
  16.194 +
  16.195 +    """Node im- and exporter for TopicIndex.
  16.196 +    """
  16.197 +
  16.198 +    __used_for__ = ITopicIndex
  16.199 +
  16.200 +    def _exportNode(self):
  16.201 +        """Export the object as a DOM node.
  16.202 +        """
  16.203 +        node = self._getObjectNode('index')
  16.204 +        for set in self.context.filteredSets.values():
  16.205 +            exporter = zapi.queryMultiAdapter((set, self.environ), INode)
  16.206 +            node.appendChild(exporter.node)
  16.207 +        return node
  16.208 +
  16.209 +    def _importNode(self, node):
  16.210 +        """Import the object from the DOM node.
  16.211 +        """
  16.212 +        for child in node.childNodes:
  16.213 +            if child.nodeName == 'filtered_set':
  16.214 +                set_id = str(child.getAttribute('name'))
  16.215 +                if set_id not in self.context.filteredSets:
  16.216 +                    set_meta_type = str(child.getAttribute('meta_type'))
  16.217 +                    self.context.addFilteredSet(set_id, set_meta_type, '')
  16.218 +                set = self.context.filteredSets[set_id]
  16.219 +                importer = zapi.queryMultiAdapter((set, self.environ), INode)
  16.220 +                importer.node = child
  16.221 +        self.context.clear()
  16.222 +
  16.223 +    node = property(_exportNode, _importNode)
    17.1 new file mode 100644
    17.2 --- /dev/null
    17.3 +++ b/PluginIndexes/tests/__init__.py
    17.4 @@ -0,0 +1,16 @@
    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 support tests.
   17.18 +
   17.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   17.20 +"""
    18.1 new file mode 100644
    18.2 --- /dev/null
    18.3 +++ b/PluginIndexes/tests/test_exportimport.py
    18.4 @@ -0,0 +1,290 @@
    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 export / import support unit tests.
   18.18 +
   18.19 +$Id: test_exportimport.py 40452 2005-12-01 18:20:45Z yuppie $
   18.20 +"""
   18.21 +
   18.22 +import unittest
   18.23 +import Testing
   18.24 +
   18.25 +from Products.Five import zcml
   18.26 +from Products.GenericSetup.testing import NodeAdapterTestCase
   18.27 +
   18.28 +_DATE_XML = """\
   18.29 +<index name="foo_date" meta_type="DateIndex">
   18.30 + <property name="index_naive_time_as_local">True</property>
   18.31 +</index>
   18.32 +"""
   18.33 +
   18.34 +_DATERANGE_XML = """\
   18.35 +<index name="foo_daterange" meta_type="DateRangeIndex" since_field="bar"
   18.36 +   until_field="baz"/>
   18.37 +"""
   18.38 +
   18.39 +_FIELD_XML = """\
   18.40 +<index name="foo_field" meta_type="FieldIndex">
   18.41 + <indexed_attr value="bar"/>
   18.42 +</index>
   18.43 +"""
   18.44 +
   18.45 +_KEYWORD_XML = """\
   18.46 +<index name="foo_keyword" meta_type="KeywordIndex">
   18.47 + <indexed_attr value="bar"/>
   18.48 +</index>
   18.49 +"""
   18.50 +
   18.51 +_PATH_XML = """\
   18.52 +<index name="foo_path" meta_type="PathIndex"/>
   18.53 +"""
   18.54 +
   18.55 +_VOCABULARY_XML = """\
   18.56 +<object name="foo_vocabulary" meta_type="Vocabulary" deprecated="True"/>
   18.57 +"""
   18.58 +
   18.59 +_TEXT_XML = """\
   18.60 +<index name="foo_text" meta_type="TextIndex" deprecated="True"/>
   18.61 +"""
   18.62 +
   18.63 +_SET_XML = """\
   18.64 +<filtered_set name="bar" meta_type="PythonFilteredSet" expression="True"/>
   18.65 +"""
   18.66 +
   18.67 +_TOPIC_XML = """\
   18.68 +<index name="foo_topic" meta_type="TopicIndex">
   18.69 + <filtered_set name="bar" meta_type="PythonFilteredSet" expression="True"/>
   18.70 + <filtered_set name="baz" meta_type="PythonFilteredSet" expression="False"/>
   18.71 +</index>
   18.72 +"""
   18.73 +
   18.74 +
   18.75 +class DateIndexNodeAdapterTests(NodeAdapterTestCase):
   18.76 +
   18.77 +    def _getTargetClass(self):
   18.78 +        from Products.GenericSetup.PluginIndexes.exportimport \
   18.79 +                import DateIndexNodeAdapter
   18.80 +
   18.81 +        return DateIndexNodeAdapter
   18.82 +
   18.83 +    def setUp(self):
   18.84 +        import Products.GenericSetup.PluginIndexes
   18.85 +        from Products.PluginIndexes.DateIndex.DateIndex import DateIndex
   18.86 +
   18.87 +        NodeAdapterTestCase.setUp(self)
   18.88 +        zcml.load_config('configure.zcml',
   18.89 +                         Products.GenericSetup.PluginIndexes)
   18.90 +
   18.91 +        self._obj = DateIndex('foo_date')
   18.92 +        self._XML = _DATE_XML
   18.93 +
   18.94 +
   18.95 +class DateRangeIndexNodeAdapterTests(NodeAdapterTestCase):
   18.96 +
   18.97 +    def _getTargetClass(self):
   18.98 +        from Products.GenericSetup.PluginIndexes.exportimport \
   18.99 +                import DateRangeIndexNodeAdapter
  18.100 +
  18.101 +        return DateRangeIndexNodeAdapter
  18.102 +
  18.103 +    def _populate(self, obj):
  18.104 +        obj._edit('bar', 'baz')
  18.105 +
  18.106 +    def setUp(self):
  18.107 +        import Products.GenericSetup.PluginIndexes
  18.108 +        from Products.PluginIndexes.DateRangeIndex.DateRangeIndex \
  18.109 +                import DateRangeIndex
  18.110 +
  18.111 +        NodeAdapterTestCase.setUp(self)
  18.112 +        zcml.load_config('configure.zcml',
  18.113 +                         Products.GenericSetup.PluginIndexes)
  18.114 +
  18.115 +        self._obj = DateRangeIndex('foo_daterange')
  18.116 +        self._XML = _DATERANGE_XML
  18.117 +
  18.118 +
  18.119 +class FieldIndexNodeAdapterTests(NodeAdapterTestCase):
  18.120 +
  18.121 +    def _getTargetClass(self):
  18.122 +        from Products.GenericSetup.PluginIndexes.exportimport \
  18.123 +                import PluggableIndexNodeAdapter
  18.124 +
  18.125 +        return PluggableIndexNodeAdapter
  18.126 +
  18.127 +    def _populate(self, obj):
  18.128 +        obj.indexed_attrs = ('bar',)
  18.129 +
  18.130 +    def setUp(self):
  18.131 +        import Products.GenericSetup.PluginIndexes
  18.132 +        from Products.PluginIndexes.FieldIndex.FieldIndex import FieldIndex
  18.133 +
  18.134 +        NodeAdapterTestCase.setUp(self)
  18.135 +        zcml.load_config('configure.zcml',
  18.136 +                         Products.GenericSetup.PluginIndexes)
  18.137 +
  18.138 +        self._obj = FieldIndex('foo_field')
  18.139 +        self._XML = _FIELD_XML
  18.140 +
  18.141 +
  18.142 +class KeywordIndexNodeAdapterTests(NodeAdapterTestCase):
  18.143 +
  18.144 +    def _getTargetClass(self):
  18.145 +        from Products.GenericSetup.PluginIndexes.exportimport \
  18.146 +                import PluggableIndexNodeAdapter
  18.147 +
  18.148 +        return PluggableIndexNodeAdapter
  18.149 +
  18.150 +    def _populate(self, obj):
  18.151 +        obj.indexed_attrs = ('bar',)
  18.152 +
  18.153 +    def setUp(self):
  18.154 +        import Products.GenericSetup.PluginIndexes
  18.155 +        from Products.PluginIndexes.KeywordIndex.KeywordIndex \
  18.156 +                import KeywordIndex
  18.157 +
  18.158 +        NodeAdapterTestCase.setUp(self)
  18.159 +        zcml.load_config('configure.zcml',
  18.160 +                         Products.GenericSetup.PluginIndexes)
  18.161 +
  18.162 +        self._obj = KeywordIndex('foo_keyword')
  18.163 +        self._XML = _KEYWORD_XML
  18.164 +
  18.165 +
  18.166 +class PathIndexNodeAdapterTests(NodeAdapterTestCase):
  18.167 +
  18.168 +    def _getTargetClass(self):
  18.169 +        from Products.GenericSetup.PluginIndexes.exportimport \
  18.170 +                import PathIndexNodeAdapter
  18.171 +
  18.172 +        return PathIndexNodeAdapter
  18.173 +
  18.174 +    def setUp(self):
  18.175 +        import Products.GenericSetup.PluginIndexes
  18.176 +        from Products.PluginIndexes.PathIndex.PathIndex import PathIndex
  18.177 +
  18.178 +        NodeAdapterTestCase.setUp(self)
  18.179 +        zcml.load_config('configure.zcml',
  18.180 +                         Products.GenericSetup.PluginIndexes)
  18.181 +
  18.182 +        self._obj = PathIndex('foo_path')
  18.183 +        self._XML = _PATH_XML
  18.184 +
  18.185 +
  18.186 +class VocabularyNodeAdapterTests(NodeAdapterTestCase):
  18.187 +
  18.188 +    def _getTargetClass(self):
  18.189 +        from Products.GenericSetup.PluginIndexes.exportimport \
  18.190 +                import VocabularyNodeAdapter
  18.191 +
  18.192 +        return VocabularyNodeAdapter
  18.193 +
  18.194 +    def setUp(self):
  18.195 +        import Products.GenericSetup.PluginIndexes
  18.196 +        from Products.PluginIndexes.TextIndex.Vocabulary import Vocabulary
  18.197 +
  18.198 +        NodeAdapterTestCase.setUp(self)
  18.199 +        zcml.load_config('configure.zcml',
  18.200 +                         Products.GenericSetup.PluginIndexes)
  18.201 +
  18.202 +        self._obj = Vocabulary('foo_vocabulary')
  18.203 +        self._XML = _VOCABULARY_XML
  18.204 +
  18.205 +    def test_importNode(self):
  18.206 +        pass
  18.207 +
  18.208 +
  18.209 +class TextIndexNodeAdapterTests(NodeAdapterTestCase):
  18.210 +
  18.211 +    def _getTargetClass(self):
  18.212 +        from Products.GenericSetup.PluginIndexes.exportimport \
  18.213 +                import TextIndexNodeAdapter
  18.214 +
  18.215 +        return TextIndexNodeAdapter
  18.216 +
  18.217 +    def setUp(self):
  18.218 +        import Products.GenericSetup.PluginIndexes
  18.219 +        from Products.PluginIndexes.TextIndex.TextIndex import TextIndex
  18.220 +
  18.221 +        NodeAdapterTestCase.setUp(self)
  18.222 +        zcml.load_config('configure.zcml',
  18.223 +                         Products.GenericSetup.PluginIndexes)
  18.224 +
  18.225 +        self._obj = TextIndex('foo_text')
  18.226 +        self._XML = _TEXT_XML
  18.227 +
  18.228 +    def test_importNode(self):
  18.229 +        pass
  18.230 +
  18.231 +
  18.232 +class FilteredSetNodeAdapterTests(NodeAdapterTestCase):
  18.233 +
  18.234 +    def _getTargetClass(self):
  18.235 +        from Products.GenericSetup.PluginIndexes.exportimport \
  18.236 +                import FilteredSetNodeAdapter
  18.237 +
  18.238 +        return FilteredSetNodeAdapter
  18.239 +
  18.240 +    def _populate(self, obj):
  18.241 +        obj.setExpression('True')
  18.242 +
  18.243 +    def setUp(self):
  18.244 +        import Products.GenericSetup.PluginIndexes
  18.245 +        from Products.PluginIndexes.TopicIndex.FilteredSet \
  18.246 +                import PythonFilteredSet
  18.247 +
  18.248 +        NodeAdapterTestCase.setUp(self)
  18.249 +        zcml.load_config('configure.zcml',
  18.250 +                         Products.GenericSetup.PluginIndexes)
  18.251 +
  18.252 +        self._obj = PythonFilteredSet('bar', '')
  18.253 +        self._XML = _SET_XML
  18.254 +
  18.255 +
  18.256 +class TopicIndexNodeAdapterTests(NodeAdapterTestCase):
  18.257 +
  18.258 +    def _getTargetClass(self):
  18.259 +        from Products.GenericSetup.PluginIndexes.exportimport \
  18.260 +                import TopicIndexNodeAdapter
  18.261 +
  18.262 +        return TopicIndexNodeAdapter
  18.263 +
  18.264 +    def _populate(self, obj):
  18.265 +        obj.addFilteredSet('bar', 'PythonFilteredSet', 'True')
  18.266 +        obj.addFilteredSet('baz', 'PythonFilteredSet', 'False')
  18.267 +
  18.268 +    def setUp(self):
  18.269 +        import Products.GenericSetup.PluginIndexes
  18.270 +        from Products.PluginIndexes.TopicIndex.TopicIndex import TopicIndex
  18.271 +
  18.272 +        NodeAdapterTestCase.setUp(self)
  18.273 +        zcml.load_config('configure.zcml',
  18.274 +                         Products.GenericSetup.PluginIndexes)
  18.275 +
  18.276 +        self._obj = TopicIndex('foo_topic')
  18.277 +        self._XML = _TOPIC_XML
  18.278 +
  18.279 +
  18.280 +def test_suite():
  18.281 +    return unittest.TestSuite((
  18.282 +        unittest.makeSuite(DateIndexNodeAdapterTests),
  18.283 +        unittest.makeSuite(DateRangeIndexNodeAdapterTests),
  18.284 +        unittest.makeSuite(FieldIndexNodeAdapterTests),
  18.285 +        unittest.makeSuite(KeywordIndexNodeAdapterTests),
  18.286 +        unittest.makeSuite(PathIndexNodeAdapterTests),
  18.287 +        unittest.makeSuite(VocabularyNodeAdapterTests),
  18.288 +        unittest.makeSuite(TextIndexNodeAdapterTests),
  18.289 +        unittest.makeSuite(FilteredSetNodeAdapterTests),
  18.290 +        unittest.makeSuite(TopicIndexNodeAdapterTests),
  18.291 +        ))
  18.292 +
  18.293 +if __name__ == '__main__':
  18.294 +    unittest.main(defaultTest='test_suite')
    19.1 new file mode 100644
    19.2 --- /dev/null
    19.3 +++ b/PythonScripts/__init__.py
    19.4 @@ -0,0 +1,16 @@
    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 +"""PythonScript support.
   19.18 +
   19.19 +$Id: __init__.py 40314 2005-11-22 13:21:47Z yuppie $
   19.20 +"""
    20.1 new file mode 100644
    20.2 --- /dev/null
    20.3 +++ b/PythonScripts/configure.zcml
    20.4 @@ -0,0 +1,18 @@
    20.5 +<configure
    20.6 +    xmlns="http://namespaces.zope.org/zope"
    20.7 +    xmlns:five="http://namespaces.zope.org/five"
    20.8 +    >
    20.9 +
   20.10 +  <adapter
   20.11 +      factory=".exportimport.PythonScriptBodyAdapter"
   20.12 +      provides="Products.GenericSetup.interfaces.IBody"
   20.13 +      for=".interfaces.IPythonScript
   20.14 +           Products.GenericSetup.interfaces.ISetupEnviron"
   20.15 +      />
   20.16 +
   20.17 +  <five:implements
   20.18 +      class="Products.PythonScripts.PythonScript.PythonScript"
   20.19 +      interface=".interfaces.IPythonScript"
   20.20 +      />
   20.21 +
   20.22 +</configure>
    21.1 new file mode 100644
    21.2 --- /dev/null
    21.3 +++ b/PythonScripts/exportimport.py
    21.4 @@ -0,0 +1,44 @@
    21.5 +##############################################################################
    21.6 +#
    21.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    21.8 +#
    21.9 +# This software is subject to the provisions of the Zope Public License,
   21.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   21.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   21.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   21.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   21.14 +# FOR A PARTICULAR PURPOSE.
   21.15 +#
   21.16 +##############################################################################
   21.17 +"""PythonScript export / import support.
   21.18 +
   21.19 +$Id: exportimport.py 40314 2005-11-22 13:21:47Z yuppie $
   21.20 +"""
   21.21 +
   21.22 +from Products.GenericSetup.utils import BodyAdapterBase
   21.23 +
   21.24 +from interfaces import IPythonScript
   21.25 +
   21.26 +
   21.27 +class PythonScriptBodyAdapter(BodyAdapterBase):
   21.28 +
   21.29 +    """Body im- and exporter for PythonScript.
   21.30 +    """
   21.31 +
   21.32 +    __used_for__ = IPythonScript
   21.33 +
   21.34 +    def _exportBody(self):
   21.35 +        """Export the object as a file body.
   21.36 +        """
   21.37 +        return self.context.read()
   21.38 +
   21.39 +    def _importBody(self, body):
   21.40 +        """Import the object from the file body.
   21.41 +        """
   21.42 +        self.context.write(body)
   21.43 +
   21.44 +    body = property(_exportBody, _importBody)
   21.45 +
   21.46 +    mime_type = 'text/plain'
   21.47 +
   21.48 +    suffix = '.py'
    22.1 new file mode 100644
    22.2 --- /dev/null
    22.3 +++ b/PythonScripts/interfaces.py
    22.4 @@ -0,0 +1,38 @@
    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 +"""PythonScripts interfaces.
   22.18 +
   22.19 +$Id: interfaces.py 40314 2005-11-22 13:21:47Z yuppie $
   22.20 +"""
   22.21 +
   22.22 +from zope.interface import Interface
   22.23 +
   22.24 +
   22.25 +class IPythonScript(Interface):
   22.26 +
   22.27 +    """Web-callable scripts written in a safe subset of Python.
   22.28 +
   22.29 +    The function may include standard python code, so long as it does not
   22.30 +    attempt to use the "exec" statement or certain restricted builtins.
   22.31 +    """
   22.32 +
   22.33 +    def read():
   22.34 +        """Generate a text representation of the Script source.
   22.35 +
   22.36 +        Includes specially formatted comment lines for parameters, bindings
   22.37 +        and the title.
   22.38 +        """
   22.39 +
   22.40 +    def write(text):
   22.41 +        """Change the Script by parsing a read()-style source text.
   22.42 +        """
    23.1 new file mode 100644
    23.2 --- /dev/null
    23.3 +++ b/PythonScripts/tests/__init__.py
    23.4 @@ -0,0 +1,16 @@
    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 +"""PythonScript support tests.
   23.18 +
   23.19 +$Id: __init__.py 40314 2005-11-22 13:21:47Z yuppie $
   23.20 +"""
    24.1 new file mode 100644
    24.2 --- /dev/null
    24.3 +++ b/PythonScripts/tests/test_exportimport.py
    24.4 @@ -0,0 +1,65 @@
    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 export / import support unit tests.
   24.18 +
   24.19 +$Id: test_exportimport.py 40314 2005-11-22 13:21:47Z yuppie $
   24.20 +"""
   24.21 +
   24.22 +import unittest
   24.23 +import Testing
   24.24 +
   24.25 +from Products.Five import zcml
   24.26 +
   24.27 +from Products.GenericSetup.testing import BodyAdapterTestCase
   24.28 +
   24.29 +
   24.30 +_PYTHONSCRIPT_BODY = """\
   24.31 +## Script (Python) "foo_script"
   24.32 +##bind container=container
   24.33 +##bind context=context
   24.34 +##bind namespace=
   24.35 +##bind script=script
   24.36 +##bind subpath=traverse_subpath
   24.37 +##parameters=
   24.38 +##title=
   24.39 +##
   24.40 +"""
   24.41 +
   24.42 +
   24.43 +class PythonScriptBodyAdapterTests(BodyAdapterTestCase):
   24.44 +
   24.45 +    def _getTargetClass(self):
   24.46 +        from Products.GenericSetup.PythonScripts.exportimport \
   24.47 +                import PythonScriptBodyAdapter
   24.48 +
   24.49 +        return PythonScriptBodyAdapter
   24.50 +
   24.51 +    def setUp(self):
   24.52 +        import Products.GenericSetup.PythonScripts
   24.53 +        from Products.PythonScripts.PythonScript import PythonScript
   24.54 +
   24.55 +        BodyAdapterTestCase.setUp(self)
   24.56 +        zcml.load_config('configure.zcml',
   24.57 +                         Products.GenericSetup.PythonScripts)
   24.58 +
   24.59 +        self._obj = PythonScript('foo_script')
   24.60 +        self._BODY = _PYTHONSCRIPT_BODY
   24.61 +
   24.62 +
   24.63 +def test_suite():
   24.64 +    return unittest.TestSuite((
   24.65 +        unittest.makeSuite(PythonScriptBodyAdapterTests),
   24.66 +        ))
   24.67 +
   24.68 +if __name__ == '__main__':
   24.69 +    unittest.main(defaultTest='test_suite')
    25.1 new file mode 100644
    25.2 --- /dev/null
    25.3 +++ b/README.txt
    25.4 @@ -0,0 +1,43 @@
    25.5 +GenericSetup Product README
    25.6 +
    25.7 +  Overview
    25.8 +
    25.9 +    This product provides a mini-framework for expressing the configured
   25.10 +    state of a Zope Site as a set of filesystem artifacts.  These artifacts
   25.11 +    consist of declarative XML files, which spell out the configuration
   25.12 +    settings for each "tool" in the site , and supporting scripts / templates,
   25.13 +    in their "canonical" filesystem representations.
   25.14 +
   25.15 +  Configurations Included
   25.16 +
   25.17 +    The 'setup_tool' knows how to export / import configurations and scripts
   25.18 +    for the following tools:
   25.19 +
   25.20 +      - (x) removal / creation of specified tools
   25.21 +
   25.22 +      - (x) itself :)
   25.23 +
   25.24 +      - (x) the role / permission map on the "site" object (its parent)
   25.25 +
   25.26 +      - (x) properties of the site object
   25.27 +
   25.28 +  Extending The Tool
   25.29 +
   25.30 +    Third-party products extend the tool by registering handlers for
   25.31 +    import / export of their unique tools.
   25.32 +
   25.33 +  Glossary
   25.34 +
   25.35 +    Site --
   25.36 +      The instance in the Zope URL space which defines a "zone of service"
   25.37 +      for a set of tools.
   25.38 +
   25.39 +    Profile --
   25.40 +      A "preset" configuration of a site, defined on the filesystem
   25.41 +
   25.42 +    Snapshot --
   25.43 +      "Frozen" site configuration, captured within the setup tool
   25.44 +
   25.45 +    "dotted name" --
   25.46 +      The Pythonic representation of the "path" to a given function /
   25.47 +      module, e.g. 'Products.GenericSetup.tool.exportToolset'.
    26.1 new file mode 100644
    26.2 --- /dev/null
    26.3 +++ b/ZCTextIndex/__init__.py
    26.4 @@ -0,0 +1,16 @@
    26.5 +##############################################################################
    26.6 +#
    26.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    26.8 +#
    26.9 +# This software is subject to the provisions of the Zope Public License,
   26.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   26.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   26.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   26.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   26.14 +# FOR A PARTICULAR PURPOSE.
   26.15 +#
   26.16 +##############################################################################
   26.17 +"""ZCTextIndex support.
   26.18 +
   26.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   26.20 +"""
    27.1 new file mode 100644
    27.2 --- /dev/null
    27.3 +++ b/ZCTextIndex/configure.zcml
    27.4 @@ -0,0 +1,19 @@
    27.5 +<configure
    27.6 +    xmlns="http://namespaces.zope.org/zope"
    27.7 +    >
    27.8 +
    27.9 +  <adapter
   27.10 +      factory=".exportimport.ZCLexiconNodeAdapter"
   27.11 +      provides="Products.GenericSetup.interfaces.INode"
   27.12 +      for="Products.ZCTextIndex.interfaces.IZCLexicon
   27.13 +           Products.GenericSetup.interfaces.ISetupEnviron"
   27.14 +      />
   27.15 +
   27.16 +  <adapter
   27.17 +      factory=".exportimport.ZCTextIndexNodeAdapter"
   27.18 +      provides="Products.GenericSetup.interfaces.INode"
   27.19 +      for="Products.ZCTextIndex.interfaces.IZCTextIndex
   27.20 +           Products.GenericSetup.interfaces.ISetupEnviron"
   27.21 +      />
   27.22 +
   27.23 +</configure>
    28.1 new file mode 100644
    28.2 --- /dev/null
    28.3 +++ b/ZCTextIndex/exportimport.py
    28.4 @@ -0,0 +1,113 @@
    28.5 +##############################################################################
    28.6 +#
    28.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    28.8 +#
    28.9 +# This software is subject to the provisions of the Zope Public License,
   28.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   28.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   28.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   28.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   28.14 +# FOR A PARTICULAR PURPOSE.
   28.15 +#
   28.16 +##############################################################################
   28.17 +"""ZCTextIndex export / import support.
   28.18 +
   28.19 +$Id: exportimport.py 40715 2005-12-12 10:33:40Z yuppie $
   28.20 +"""
   28.21 +
   28.22 +from BTrees.IOBTree import IOBTree
   28.23 +from BTrees.Length import Length
   28.24 +from BTrees.OIBTree import OIBTree
   28.25 +
   28.26 +from Products.GenericSetup.utils import NodeAdapterBase
   28.27 +
   28.28 +from Products.ZCTextIndex.interfaces import IZCLexicon
   28.29 +from Products.ZCTextIndex.interfaces import IZCTextIndex
   28.30 +from Products.ZCTextIndex.PipelineFactory import element_factory
   28.31 +
   28.32 +
   28.33 +class ZCLexiconNodeAdapter(NodeAdapterBase):
   28.34 +
   28.35 +    """Node im- and exporter for ZCTextIndex Lexicon.
   28.36 +    """
   28.37 +
   28.38 +    __used_for__ = IZCLexicon
   28.39 +
   28.40 +    def _exportNode(self):
   28.41 +        """Export the object as a DOM node.
   28.42 +        """
   28.43 +        node = self._getObjectNode('object')
   28.44 +        for element in self.context._pipeline:
   28.45 +            group, name = self._getKeys(element)
   28.46 +            child = self._doc.createElement('element')
   28.47 +            child.setAttribute('group', group)
   28.48 +            child.setAttribute('name', name)
   28.49 +            node.appendChild(child)
   28.50 +        return node
   28.51 +
   28.52 +    def _importNode(self, node):
   28.53 +        """Import the object from the DOM node.
   28.54 +        """
   28.55 +        pipeline = []
   28.56 +        for child in node.childNodes:
   28.57 +            if child.nodeName == 'element':
   28.58 +                element = element_factory.instantiate(
   28.59 +                      child.getAttribute('group').encode('utf-8'),
   28.60 +                      child.getAttribute('name').encode('utf-8'))
   28.61 +                pipeline.append(element)
   28.62 +        self.context._pipeline = tuple(pipeline)
   28.63 +        #clear lexicon
   28.64 +        self.context._wids = OIBTree()
   28.65 +        self.context._words = IOBTree()
   28.66 +        self.context.length = Length()
   28.67 +
   28.68 +    node = property(_exportNode, _importNode)
   28.69 +
   28.70 +    def _getKeys(self, element):
   28.71 +        for group in element_factory.getFactoryGroups():
   28.72 +            for name, factory in element_factory._groups[group].items():
   28.73 +                if factory == element.__class__:
   28.74 +                    return group, name
   28.75 +
   28.76 +
   28.77 +class ZCTextIndexNodeAdapter(NodeAdapterBase):
   28.78 +
   28.79 +    """Node im- and exporter for ZCTextIndex.
   28.80 +    """
   28.81 +
   28.82 +    __used_for__ = IZCTextIndex
   28.83 +
   28.84 +    def _exportNode(self):
   28.85 +        """Export the object as a DOM node.
   28.86 +        """
   28.87 +        node = self._getObjectNode('index')
   28.88 +
   28.89 +        for value in self.context.getIndexSourceNames():
   28.90 +            child = self._doc.createElement('indexed_attr')
   28.91 +            child.setAttribute('value', value)
   28.92 +            node.appendChild(child)
   28.93 +
   28.94 +        child = self._doc.createElement('extra')
   28.95 +        child.setAttribute('name', 'index_type')
   28.96 +        child.setAttribute('value', self.context.getIndexType())
   28.97 +        node.appendChild(child)
   28.98 +
   28.99 +        child = self._doc.createElement('extra')
  28.100 +        child.setAttribute('name', 'lexicon_id')
  28.101 +        child.setAttribute('value', self.context.lexicon_id)
  28.102 +        node.appendChild(child)
  28.103 +
  28.104 +        return node
  28.105 +
  28.106 +    def _importNode(self, node):
  28.107 +        """Import the object from the DOM node.
  28.108 +        """
  28.109 +        indexed_attrs = []
  28.110 +        for child in node.childNodes:
  28.111 +            if child.nodeName == 'indexed_attr':
  28.112 +                indexed_attrs.append(
  28.113 +                                  child.getAttribute('value').encode('utf-8'))
  28.114 +        self.context.indexed_attrs = indexed_attrs
  28.115 +        self.context.clear()
  28.116 +
  28.117 +    node = property(_exportNode, _importNode)
    29.1 new file mode 100644
    29.2 --- /dev/null
    29.3 +++ b/ZCTextIndex/tests/__init__.py
    29.4 @@ -0,0 +1,16 @@
    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 support tests.
   29.18 +
   29.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   29.20 +"""
    30.1 new file mode 100644
    30.2 --- /dev/null
    30.3 +++ b/ZCTextIndex/tests/test_exportimport.py
    30.4 @@ -0,0 +1,111 @@
    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 export / import support unit tests.
   30.18 +
   30.19 +$Id: test_exportimport.py 40452 2005-12-01 18:20:45Z yuppie $
   30.20 +"""
   30.21 +
   30.22 +import unittest
   30.23 +import Testing
   30.24 +
   30.25 +from Acquisition import Implicit
   30.26 +
   30.27 +from Products.Five import zcml
   30.28 +from Products.GenericSetup.testing import NodeAdapterTestCase
   30.29 +
   30.30 +_PLEXICON_XML = """\
   30.31 +<object name="foo_plexicon" meta_type="ZCTextIndex Lexicon">
   30.32 + <element name="Whitespace splitter" group="Word Splitter"/>
   30.33 + <element name="Case Normalizer" group="Case Normalizer"/>
   30.34 + <element name="Remove listed stop words only" group="Stop Words"/>
   30.35 +</object>
   30.36 +"""
   30.37 +
   30.38 +_ZCTEXT_XML = """\
   30.39 +<index name="foo_zctext" meta_type="ZCTextIndex">
   30.40 + <indexed_attr value="foo_zctext"/>
   30.41 + <extra name="index_type" value="Okapi BM25 Rank"/>
   30.42 + <extra name="lexicon_id" value="foo_plexicon"/>
   30.43 +</index>
   30.44 +"""
   30.45 +
   30.46 +
   30.47 +class _extra:
   30.48 +
   30.49 +    pass
   30.50 +
   30.51 +
   30.52 +class DummyCatalog(Implicit):
   30.53 +
   30.54 +    pass
   30.55 +
   30.56 +
   30.57 +class ZCLexiconNodeAdapterTests(NodeAdapterTestCase):
   30.58 +
   30.59 +    def _getTargetClass(self):
   30.60 +        from Products.GenericSetup.ZCTextIndex.exportimport \
   30.61 +                import ZCLexiconNodeAdapter
   30.62 +
   30.63 +        return ZCLexiconNodeAdapter
   30.64 +
   30.65 +    def _populate(self, obj):
   30.66 +        from Products.ZCTextIndex.Lexicon import CaseNormalizer
   30.67 +        from Products.ZCTextIndex.Lexicon import Splitter
   30.68 +        from Products.ZCTextIndex.Lexicon import StopWordRemover
   30.69 +        obj._pipeline = (Splitter(), CaseNormalizer(), StopWordRemover())
   30.70 +
   30.71 +    def setUp(self):
   30.72 +        import Products.GenericSetup.ZCTextIndex
   30.73 +        from Products.ZCTextIndex.ZCTextIndex import PLexicon
   30.74 +
   30.75 +        NodeAdapterTestCase.setUp(self)
   30.76 +        zcml.load_config('configure.zcml', Products.GenericSetup.ZCTextIndex)
   30.77 +
   30.78 +        self._obj = PLexicon('foo_plexicon')
   30.79 +        self._XML = _PLEXICON_XML
   30.80 +
   30.81 +
   30.82 +class ZCTextIndexNodeAdapterTests(NodeAdapterTestCase):
   30.83 +
   30.84 +    def _getTargetClass(self):
   30.85 +        from Products.GenericSetup.ZCTextIndex.exportimport \
   30.86 +                import ZCTextIndexNodeAdapter
   30.87 +
   30.88 +        return ZCTextIndexNodeAdapter
   30.89 +
   30.90 +    def setUp(self):
   30.91 +        import Products.GenericSetup.ZCTextIndex
   30.92 +        from Products.ZCTextIndex.ZCTextIndex import PLexicon
   30.93 +        from Products.ZCTextIndex.ZCTextIndex import ZCTextIndex
   30.94 +
   30.95 +        NodeAdapterTestCase.setUp(self)
   30.96 +        zcml.load_config('configure.zcml', Products.GenericSetup.ZCTextIndex)
   30.97 +
   30.98 +        catalog = DummyCatalog()
   30.99 +        catalog.foo_plexicon = PLexicon('foo_plexicon')
  30.100 +        extra = _extra()
  30.101 +        extra.lexicon_id = 'foo_plexicon'
  30.102 +        extra.index_type='Okapi BM25 Rank'
  30.103 +        self._obj = ZCTextIndex('foo_zctext', extra=extra,
  30.104 +                                caller=catalog).__of__(catalog)
  30.105 +        self._XML = _ZCTEXT_XML
  30.106 +
  30.107 +
  30.108 +def test_suite():
  30.109 +    return unittest.TestSuite((
  30.110 +        unittest.makeSuite(ZCLexiconNodeAdapterTests),
  30.111 +        unittest.makeSuite(ZCTextIndexNodeAdapterTests),
  30.112 +        ))
  30.113 +
  30.114 +if __name__ == '__main__':
  30.115 +    unittest.main(defaultTest='test_suite')
    31.1 new file mode 100644
    31.2 --- /dev/null
    31.3 +++ b/ZCatalog/__init__.py
    31.4 @@ -0,0 +1,16 @@
    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 +"""ZCatalog support.
   31.18 +
   31.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   31.20 +"""
    32.1 new file mode 100644
    32.2 --- /dev/null
    32.3 +++ b/ZCatalog/configure.zcml
    32.4 @@ -0,0 +1,12 @@
    32.5 +<configure
    32.6 +    xmlns="http://namespaces.zope.org/zope"
    32.7 +    >
    32.8 +
    32.9 +  <adapter
   32.10 +      factory=".exportimport.ZCatalogXMLAdapter"
   32.11 +      provides="Products.GenericSetup.interfaces.IBody"
   32.12 +      for="Products.ZCatalog.interfaces.IZCatalog
   32.13 +           Products.GenericSetup.interfaces.ISetupEnviron"
   32.14 +      />
   32.15 +
   32.16 +</configure>
    33.1 new file mode 100644
    33.2 --- /dev/null
    33.3 +++ b/ZCatalog/exportimport.py
    33.4 @@ -0,0 +1,133 @@
    33.5 +##############################################################################
    33.6 +#
    33.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    33.8 +#
    33.9 +# This software is subject to the provisions of the Zope Public License,
   33.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   33.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   33.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   33.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   33.14 +# FOR A PARTICULAR PURPOSE.
   33.15 +#
   33.16 +##############################################################################
   33.17 +"""ZCatalog export / import support.
   33.18 +
   33.19 +$Id: exportimport.py 40878 2005-12-18 21:57:56Z yuppie $
   33.20 +"""
   33.21 +
   33.22 +from zope.app import zapi
   33.23 +
   33.24 +from Products.GenericSetup.interfaces import INode
   33.25 +from Products.GenericSetup.utils import ObjectManagerHelpers
   33.26 +from Products.GenericSetup.utils import PropertyManagerHelpers
   33.27 +from Products.GenericSetup.utils import XMLAdapterBase
   33.28 +
   33.29 +from Products.ZCatalog.interfaces import IZCatalog
   33.30 +
   33.31 +
   33.32 +class _extra:
   33.33 +
   33.34 +    pass
   33.35 +
   33.36 +
   33.37 +class ZCatalogXMLAdapter(XMLAdapterBase, ObjectManagerHelpers,
   33.38 +                         PropertyManagerHelpers):
   33.39 +
   33.40 +    """XML im- and exporter for ZCatalog.
   33.41 +    """
   33.42 +
   33.43 +    __used_for__ = IZCatalog
   33.44 +
   33.45 +    _LOGGER_ID = 'catalog'
   33.46 +
   33.47 +    name = 'catalog'
   33.48 +
   33.49 +    def _exportNode(self):
   33.50 +        """Export the object as a DOM node.
   33.51 +        """
   33.52 +        node = self._getObjectNode('object')
   33.53 +        node.appendChild(self._extractProperties())
   33.54 +        node.appendChild(self._extractObjects())
   33.55 +        node.appendChild(self._extractIndexes())
   33.56 +        node.appendChild(self._extractColumns())
   33.57 +
   33.58 +        self._logger.info('Catalog exported.')
   33.59 +        return node
   33.60 +
   33.61 +    def _importNode(self, node):
   33.62 +        """Import the object from the DOM node.
   33.63 +        """
   33.64 +        if self.environ.shouldPurge():
   33.65 +            self._purgeProperties()
   33.66 +            self._purgeObjects()
   33.67 +            self._purgeIndexes()
   33.68 +            self._purgeColumns()
   33.69 +
   33.70 +        self._initProperties(node)
   33.71 +        self._initObjects(node)
   33.72 +        self._initIndexes(node)
   33.73 +        self._initColumns(node)
   33.74 +
   33.75 +        self._logger.info('Catalog imported.')
   33.76 +
   33.77 +    def _extractIndexes(self):
   33.78 +        fragment = self._doc.createDocumentFragment()
   33.79 +        indexes = self.context.getIndexObjects()[:]
   33.80 +        indexes.sort(lambda x,y: cmp(x.getId(), y.getId()))
   33.81 +        for idx in indexes:
   33.82 +            exporter = zapi.queryMultiAdapter((idx, self.environ), INode)
   33.83 +            if exporter:
   33.84 +                fragment.appendChild(exporter.node)
   33.85 +        return fragment
   33.86 +
   33.87 +    def _purgeIndexes(self):
   33.88 +        for idx_id in self.context.indexes():
   33.89 +            self.context.delIndex(idx_id)
   33.90 +
   33.91 +    def _initIndexes(self, node):
   33.92 +        for child in node.childNodes:
   33.93 +            if child.nodeName != 'index':
   33.94 +                continue
   33.95 +            if child.hasAttribute('deprecated'):
   33.96 +                continue
   33.97 +            zcatalog = self.context
   33.98 +
   33.99 +            idx_id = str(child.getAttribute('name'))
  33.100 +            if idx_id not in zcatalog.indexes():
  33.101 +                extra = _extra()
  33.102 +                for sub in child.childNodes:
  33.103 +                    if sub.nodeName == 'extra':
  33.104 +                        name = str(sub.getAttribute('name'))
  33.105 +                        value = str(sub.getAttribute('value'))
  33.106 +                        setattr(extra, name, value)
  33.107 +                extra = extra.__dict__ and extra or None
  33.108 +
  33.109 +                meta_type = str(child.getAttribute('meta_type'))
  33.110 +                zcatalog.addIndex(idx_id, meta_type, extra)
  33.111 +
  33.112 +            idx = zcatalog._catalog.getIndex(idx_id)
  33.113 +            importer = zapi.queryMultiAdapter((idx, self.environ), INode)
  33.114 +            if importer:
  33.115 +                importer.node = child
  33.116 +
  33.117 +    def _extractColumns(self):
  33.118 +        fragment = self._doc.createDocumentFragment()
  33.119 +        schema = self.context.schema()[:]
  33.120 +        schema.sort()
  33.121 +        for col in schema:
  33.122 +            child = self._doc.createElement('column')
  33.123 +            child.setAttribute('value', col)
  33.124 +            fragment.appendChild(child)
  33.125 +        return fragment
  33.126 +
  33.127 +    def _purgeColumns(self):
  33.128 +        for col in self.context.schema()[:]:
  33.129 +            self.context.delColumn(col)
  33.130 +
  33.131 +    def _initColumns(self, node):
  33.132 +        for child in node.childNodes:
  33.133 +            if child.nodeName != 'column':
  33.134 +                continue
  33.135 +            col = str(child.getAttribute('value'))
  33.136 +            if col not in self.context.schema()[:]:
  33.137 +                self.context.addColumn(col)
    34.1 new file mode 100644
    34.2 --- /dev/null
    34.3 +++ b/ZCatalog/tests/__init__.py
    34.4 @@ -0,0 +1,16 @@
    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 support tests.
   34.18 +
   34.19 +$Id: __init__.py 38551 2005-09-20 20:50:17Z yuppie $
   34.20 +"""
    35.1 new file mode 100644
    35.2 --- /dev/null
    35.3 +++ b/ZCatalog/tests/test_exportimport.py
    35.4 @@ -0,0 +1,163 @@
    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 export / import support unit tests.
   35.18 +
   35.19 +$Id: test_exportimport.py 40715 2005-12-12 10:33:40Z yuppie $
   35.20 +"""
   35.21 +
   35.22 +import unittest
   35.23 +import Testing
   35.24 +import Zope2
   35.25 +Zope2.startup()
   35.26 +
   35.27 +from Products.Five import zcml
   35.28 +from zope.app import zapi
   35.29 +
   35.30 +from Products.GenericSetup.interfaces import IBody
   35.31 +from Products.GenericSetup.testing import BodyAdapterTestCase
   35.32 +from Products.GenericSetup.testing import DummySetupEnviron
   35.33 +
   35.34 +
   35.35 +class _extra:
   35.36 +
   35.37 +    pass
   35.38 +
   35.39 +
   35.40 +_CATALOG_BODY = """\
   35.41 +<?xml version="1.0"?>
   35.42 +<object name="foo_catalog" meta_type="ZCatalog">
   35.43 + <property name="title"></property>
   35.44 + <object name="foo_plexicon" meta_type="ZCTextIndex Lexicon">
   35.45 +  <element name="Whitespace splitter" group="Word Splitter"/>
   35.46 +  <element name="Case Normalizer" group="Case Normalizer"/>
   35.47 +  <element name="Remove listed stop words only" group="Stop Words"/>
   35.48 + </object>
   35.49 +%s <index name="foo_date" meta_type="DateIndex">
   35.50 +  <property name="index_naive_time_as_local">True</property>
   35.51 + </index>
   35.52 + <index name="foo_daterange" meta_type="DateRangeIndex" since_field="bar"
   35.53 +    until_field="baz"/>
   35.54 + <index name="foo_field" meta_type="FieldIndex">
   35.55 +  <indexed_attr value="bar"/>
   35.56 + </index>
   35.57 + <index name="foo_keyword" meta_type="KeywordIndex">
   35.58 +  <indexed_attr value="bar"/>
   35.59 + </index>
   35.60 + <index name="foo_path" meta_type="PathIndex"/>
   35.61 +%s <index name="foo_topic" meta_type="TopicIndex">
   35.62 +  <filtered_set name="bar" meta_type="PythonFilteredSet" expression="True"/>
   35.63 +  <filtered_set name="baz" meta_type="PythonFilteredSet" expression="False"/>
   35.64 + </index>
   35.65 + <index name="foo_zctext" meta_type="ZCTextIndex">
   35.66 +  <indexed_attr value="foo_zctext"/>
   35.67 +  <extra name="index_type" value="Okapi BM25 Rank"/>
   35.68 +  <extra name="lexicon_id" value="foo_plexicon"/>
   35.69 + </index>
   35.70 + <column value="eggs"/>
   35.71 + <column value="spam"/>
   35.72 +</object>
   35.73 +"""
   35.74 +
   35.75 +_TEXT_XML = """\
   35.76 + <index name="foo_text" meta_type="TextIndex" deprecated="True"/>
   35.77 +"""
   35.78 +
   35.79 +_VOCABULARY_XML = """\
   35.80 + <object name="foo_vocabulary" meta_type="Vocabulary" deprecated="True"/>
   35.81 +"""
   35.82 +
   35.83 +
   35.84 +class ZCatalogXMLAdapterTests(BodyAdapterTestCase):
   35.85 +
   35.86 +    def _getTargetClass(self):
   35.87 +        from Products.GenericSetup.ZCatalog.exportimport \
   35.88 +                import ZCatalogXMLAdapter
   35.89 +
   35.90 +        return ZCatalogXMLAdapter
   35.91 +
   35.92 +    def _populate(self, obj):
   35.93 +        from Products.ZCTextIndex.Lexicon import CaseNormalizer
   35.94 +        from Products.ZCTextIndex.Lexicon import Splitter
   35.95 +        from Products.ZCTextIndex.Lexicon import StopWordRemover
   35.96 +        from Products.ZCTextIndex.ZCTextIndex import PLexicon
   35.97 +
   35.98 +        obj._setObject('foo_plexicon', PLexicon('foo_plexicon'))
   35.99 +        lex = obj.foo_plexicon
  35.100 +        lex._pipeline = (Splitter(), CaseNormalizer(), StopWordRemover())
  35.101 +
  35.102 +        obj.addIndex('foo_date', 'DateIndex')
  35.103 +
  35.104 +        obj.addIndex('foo_daterange', 'DateRangeIndex')
  35.105 +        idx = obj._catalog.getIndex('foo_daterange')
  35.106 +        idx._edit('bar', 'baz')
  35.107 +
  35.108 +        obj.addIndex('foo_field', 'FieldIndex')
  35.109 +        idx = obj._catalog.getIndex('foo_field')
  35.110 +        idx.indexed_attrs = ('bar',)
  35.111 +
  35.112 +        obj.addIndex('foo_keyword', 'KeywordIndex')
  35.113 +        idx = obj._catalog.getIndex('foo_keyword')
  35.114 +        idx.indexed_attrs = ('bar',)
  35.115 +
  35.116 +        obj.addIndex('foo_path', 'PathIndex')
  35.117 +
  35.118 +        obj.addIndex('foo_topic', 'TopicIndex')
  35.119 +        idx = obj._catalog.getIndex('foo_topic')
  35.120 +        idx.addFilteredSet('bar', 'PythonFilteredSet', 'True')
  35.121 +        idx.addFilteredSet('baz', 'PythonFilteredSet', 'False')
  35.122 +
  35.123 +        extra = _extra()
  35.124 +        extra.lexicon_id = 'foo_plexicon'
  35.125 +        extra.index_type = 'Okapi BM25 Rank'
  35.126 +        obj.addIndex('foo_zctext', 'ZCTextIndex', extra)
  35.127 +
  35.128 +        obj.addColumn('spam')
  35.129 +        obj.addColumn('eggs')
  35.130 +
  35.131 +    def _populate_special(self, obj):
  35.132 +        from Products.PluginIndexes.TextIndex.Vocabulary import Vocabulary
  35.133 +
  35.134 +        self._populate(self._obj)
  35.135 +        obj._setObject('foo_vocabulary', Vocabulary('foo_vocabulary'))
  35.136 +        obj.addIndex('foo_text', 'TextIndex')
  35.137 +
  35.138 +    def setUp(self):
  35.139 +        import Products.GenericSetup.PluginIndexes
  35.140 +        import Products.GenericSetup.ZCatalog
  35.141 +        import Products.GenericSetup.ZCTextIndex
  35.142 +        from Products.ZCatalog.ZCatalog import ZCatalog
  35.143 +
  35.144 +        BodyAdapterTestCase.setUp(self)
  35.145 +        zcml.load_config('configure.zcml',
  35.146 +                         Products.GenericSetup.PluginIndexes)
  35.147 +        zcml.load_config('configure.zcml', Products.GenericSetup.ZCatalog)
  35.148 +        zcml.load_config('configure.zcml', Products.GenericSetup.ZCTextIndex)
  35.149 +
  35.150 +        self._obj = ZCatalog('foo_catalog')
  35.151 +        self._BODY = _CATALOG_BODY % ('', '')
  35.152 +
  35.153 +    def test_body_get_special(self):
  35.154 +        self._populate_special(self._obj)
  35.155 +        context = DummySetupEnviron()
  35.156 +        adapted = zapi.getMultiAdapter((self._obj, context), IBody)
  35.157 +        self.assertEqual(adapted.body,
  35.158 +                         _CATALOG_BODY % (_VOCABULARY_XML, _TEXT_XML))
  35.159 +
  35.160 +
  35.161 +def test_suite():
  35.162 +    return unittest.TestSuite((
  35.163 +        unittest.makeSuite(ZCatalogXMLAdapterTests),
  35.164 +        ))
  35.165 +
  35.166 +if __name__ == '__main__':
  35.167 +    unittest.main(defaultTest='test_suite')
    36.1 new file mode 100644
    36.2 --- /dev/null
    36.3 +++ b/__init__.py
    36.4 @@ -0,0 +1,38 @@
    36.5 +""" GenericSetup product initialization.
    36.6 +
    36.7 +$Id: __init__.py,v 1.1.1.1 2005/08/08 19:38:37 tseaver Exp $
    36.8 +"""
    36.9 +
   36.10 +from AccessControl import ModuleSecurityInfo
   36.11 +
   36.12 +from interfaces import BASE, EXTENSION
   36.13 +from permissions import ManagePortal
   36.14 +from registry import _profile_registry as profile_registry
   36.15 +
   36.16 +security = ModuleSecurityInfo('Products.GenericSetup')
   36.17 +security.declareProtected(ManagePortal, 'profile_registry')
   36.18 +
   36.19 +def initialize(context):
   36.20 +
   36.21 +    import tool
   36.22 +
   36.23 +    context.registerClass(tool.SetupTool,
   36.24 +                          constructors=(#tool.addSetupToolForm,
   36.25 +                                        tool.addSetupTool,
   36.26 +                                        ),
   36.27 +                          permissions=(ManagePortal,),
   36.28 +                          interfaces=None,
   36.29 +                          icon='www/tool.png',
   36.30 +                         )
   36.31 +
   36.32 +# BBB: for setup tools created with CMF 1.5 if CMFSetup isn't installed
   36.33 +try:
   36.34 +    import Products.CMFSetup
   36.35 +except ImportError:
   36.36 +    import bbb
   36.37 +    import bbb.registry
   36.38 +    import bbb.tool
   36.39 +
   36.40 +    __module_aliases__ = (('Products.CMFSetup', bbb),
   36.41 +                          ('Products.CMFSetup.registry', bbb.registry),
   36.42 +                          ('Products.CMFSetup.tool', bbb.tool))
    37.1 new file mode 100644
    37.2 --- /dev/null
    37.3 +++ b/bbb/__init__.py
    37.4 @@ -0,0 +1,16 @@
    37.5 +##############################################################################
    37.6 +#
    37.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    37.8 +#
    37.9 +# This software is subject to the provisions of the Zope Public License,
   37.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   37.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   37.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   37.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   37.14 +# FOR A PARTICULAR PURPOSE.
   37.15 +#
   37.16 +##############################################################################
   37.17 +""" CMFSetup product initialization.
   37.18 +
   37.19 +$Id: __init__.py 40429 2005-11-30 22:12:58Z yuppie $
   37.20 +"""
    38.1 new file mode 100644
    38.2 --- /dev/null
    38.3 +++ b/bbb/registry.py
    38.4 @@ -0,0 +1,21 @@
    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 +""" Classes:  ImportStepRegistry, ExportStepRegistry
   38.18 +
   38.19 +$Id: registry.py 40429 2005-11-30 22:12:58Z yuppie $
   38.20 +"""
   38.21 +
   38.22 +# BBB: for setup tools created with CMF 1.5
   38.23 +from Products.GenericSetup.registry import ImportStepRegistry
   38.24 +from Products.GenericSetup.registry import ExportStepRegistry
   38.25 +from Products.GenericSetup.registry import ToolsetRegistry
    39.1 new file mode 100644
    39.2 --- /dev/null
    39.3 +++ b/bbb/tool.py
    39.4 @@ -0,0 +1,30 @@
    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:  SetupTool
   39.18 +
   39.19 +$Id: tool.py 40429 2005-11-30 22:12:58Z yuppie $
   39.20 +"""
   39.21 +
   39.22 +from Products.GenericSetup.tool import exportStepRegistries
   39.23 +from Products.GenericSetup.tool import importToolset
   39.24 +from Products.GenericSetup.tool import exportToolset
   39.25 +from Products.GenericSetup.tool import SetupTool as BaseTool
   39.26 +
   39.27 +
   39.28 +class SetupTool(BaseTool):
   39.29 +
   39.30 +    #BBB: for setup tools created with CMF 1.5
   39.31 +    id = 'portal_setup'
   39.32 +
   39.33 +    def __init__(self, id='portal_setup'):
   39.34 +        BaseTool.__init__(self, id)
    40.1 new file mode 100644
    40.2 --- /dev/null
    40.3 +++ b/browser/__init__.py
    40.4 @@ -0,0 +1,16 @@
    40.5 +##############################################################################
    40.6 +#
    40.7 +# Copyright (c) 2005 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 +"""GenericSetup browser views.
   40.18 +
   40.19 +$Id: __init__.py 40355 2005-11-24 11:39:06Z yuppie $
   40.20 +"""
    41.1 new file mode 100644
    41.2 --- /dev/null
    41.3 +++ b/browser/addWithPresettings.pt
    41.4 @@ -0,0 +1,46 @@
    41.5 +<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
    41.6 +<h2 tal:define="form_title view/title"
    41.7 +    tal:replace="structure context/manage_form_title">FORM TITLE</h2>
    41.8 +
    41.9 +<p class="form-help" tal:content="view/description">DESCRIPTION TEXT.</p>
   41.10 +
   41.11 +<form action="." method="post"
   41.12 +   tal:attributes="action request/ACTUAL_URL">
   41.13 +<table cellspacing="0" cellpadding="2" border="0">
   41.14 + <tr>
   41.15 +  <td>
   41.16 +   <div class="form-label">ID</div>
   41.17 +  </td>
   41.18 +  <td>
   41.19 +   <input type="text" name="add_input_name" size="40" />
   41.20 +  </td>
   41.21 + </tr>
   41.22 + <tr tal:condition="view/getProfileInfos">
   41.23 +  <td>
   41.24 +   <div class="form-label">Presettings</div>
   41.25 +  </td>
   41.26 +  <td>
   41.27 +   <select name="settings_id">
   41.28 +    <option value="" selected="selected">(None)</option>
   41.29 +    <optgroup label="PROFILE_TITLE"
   41.30 +       tal:repeat="profile view/getProfileInfos"
   41.31 +       tal:attributes="label profile/title">
   41.32 +     <option value="SETTINGS_ID"
   41.33 +             tal:repeat="obj_id profile/obj_ids"
   41.34 +             tal:attributes="value string:${profile/id}/${obj_id}"
   41.35 +             tal:content="obj_id">OBJ ID</option></optgroup>
   41.36 +   </select>
   41.37 +  </td>
   41.38 + </tr>
   41.39 + <tr>
   41.40 +  <td>
   41.41 +   &nbsp;
   41.42 +  </td>
   41.43 +  <td>
   41.44 +   <input class="form-element" type="submit" name="submit_add" value="Add" /> 
   41.45 +  </td>
   41.46 + </tr>
   41.47 +</table>
   41.48 +</form>
   41.49 +
   41.50 +<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
    42.1 new file mode 100644
    42.2 --- /dev/null
    42.3 +++ b/browser/utils.py
    42.4 @@ -0,0 +1,39 @@
    42.5 +##############################################################################
    42.6 +#
    42.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    42.8 +#
    42.9 +# This software is subject to the provisions of the Zope Public License,
   42.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   42.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   42.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   42.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   42.14 +# FOR A PARTICULAR PURPOSE.
   42.15 +#
   42.16 +##############################################################################
   42.17 +"""GenericSetup browser view utils.
   42.18 +
   42.19 +$Id: utils.py 40715 2005-12-12 10:33:40Z yuppie $
   42.20 +"""
   42.21 +
   42.22 +class AddWithPresettingsViewBase:
   42.23 +
   42.24 +    """Base class for add views with selectable presettings.
   42.25 +    """
   42.26 +
   42.27 +    def title(self):
   42.28 +        return u'Add %s' % self.klass.meta_type
   42.29 +
   42.30 +    def __call__(self, add_input_name='', settings_id='', submit_add=''):
   42.31 +        if submit_add:
   42.32 +            obj = self.klass('temp')
   42.33 +            if settings_id:
   42.34 +                ids = settings_id.split('/')
   42.35 +                profile_id = ids[0]
   42.36 +                obj_path = ids[1:]
   42.37 +                if not add_input_name:
   42.38 +                    self.request.set('add_input_name', obj_path[-1])
   42.39 +                self._initSettings(obj, profile_id, obj_path)
   42.40 +            self.context.add(obj)
   42.41 +            self.request.response.redirect(self.context.nextURL())
   42.42 +            return ''
   42.43 +        return self.index()
    43.1 new file mode 100644
    43.2 --- /dev/null
    43.3 +++ b/configure.zcml
    43.4 @@ -0,0 +1,53 @@
    43.5 +<configure
    43.6 +    xmlns="http://namespaces.zope.org/zope"
    43.7 +    >
    43.8 +
    43.9 +  <include package=".MailHost"/>
   43.10 +
   43.11 +  <include package=".OFSP"/>
   43.12 +
   43.13 +  <include package=".PluginIndexes"/>
   43.14 +
   43.15 +  <include package=".PythonScripts"/>
   43.16 +
   43.17 +  <include package=".ZCatalog"/>
   43.18 +
   43.19 +  <include package=".ZCTextIndex"/>
   43.20 +
   43.21 +  <adapter
   43.22 +      factory=".content.CSVAwareFileAdapter"
   43.23 +      provides="Products.GenericSetup.interfaces.IFilesystemExporter"
   43.24 +      for="Products.GenericSetup.interfaces.ICSVAware"
   43.25 +      />
   43.26 +
   43.27 +  <adapter
   43.28 +      factory=".content.CSVAwareFileAdapter"
   43.29 +      provides="Products.GenericSetup.interfaces.IFilesystemImporter"
   43.30 +      for="Products.GenericSetup.interfaces.ICSVAware"
   43.31 +      />
   43.32 +
   43.33 +  <adapter
   43.34 +      factory=".content.INIAwareFileAdapter"
   43.35 +      provides="Products.GenericSetup.interfaces.IFilesystemExporter"
   43.36 +      for="Products.GenericSetup.interfaces.IINIAware"
   43.37 +      />
   43.38 +
   43.39 +  <adapter
   43.40 +      factory=".content.INIAwareFileAdapter"
   43.41 +      provides="Products.GenericSetup.interfaces.IFilesystemImporter"
   43.42 +      for="Products.GenericSetup.interfaces.IINIAware"
   43.43 +      />
   43.44 +
   43.45 +  <adapter
   43.46 +      factory=".content.DAVAwareFileAdapter"
   43.47 +      provides="Products.GenericSetup.interfaces.IFilesystemExporter"
   43.48 +      for="Products.GenericSetup.interfaces.IDAVAware"
   43.49 +      />
   43.50 +
   43.51 +  <adapter
   43.52 +      factory=".content.DAVAwareFileAdapter"
   43.53 +      provides="Products.GenericSetup.interfaces.IFilesystemImporter"
   43.54 +      for="Products.GenericSetup.interfaces.IDAVAware"
   43.55 +      />
   43.56 +
   43.57 +</configure>
    44.1 new file mode 100644
    44.2 --- /dev/null
    44.3 +++ b/content.py
    44.4 @@ -0,0 +1,409 @@
    44.5 +##############################################################################
    44.6 +#
    44.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    44.8 +#
    44.9 +# This software is subject to the provisions of the Zope Public License,
   44.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   44.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   44.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   44.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   44.14 +# FOR A PARTICULAR PURPOSE.
   44.15 +#
   44.16 +##############################################################################
   44.17 +"""Filesystem exporter / importer adapters.
   44.18 +
   44.19 +$Id: content.py 41651 2006-02-17 22:10:23Z tseaver $
   44.20 +"""
   44.21 +
   44.22 +from csv import reader
   44.23 +from csv import register_dialect
   44.24 +from csv import writer
   44.25 +from ConfigParser import ConfigParser
   44.26 +import re
   44.27 +from StringIO import StringIO
   44.28 +
   44.29 +from zope.interface import implements
   44.30 +from zope.interface import directlyProvides
   44.31 +from zope.app import zapi
   44.32 +
   44.33 +from interfaces import IContentFactory
   44.34 +from interfaces import IContentFactoryName
   44.35 +from interfaces import IFilesystemExporter
   44.36 +from interfaces import IFilesystemImporter
   44.37 +from interfaces import IINIAware
   44.38 +from interfaces import ISetupTool
   44.39 +from utils import _getDottedName
   44.40 +from utils import _resolveDottedName
   44.41 +
   44.42 +#
   44.43 +#   setup_tool handlers
   44.44 +#
   44.45 +def exportSiteStructure(context):
   44.46 +    IFilesystemExporter(context.getSite()).export(context, 'structure', True)
   44.47 +
   44.48 +def importSiteStructure(context):
   44.49 +    IFilesystemImporter(context.getSite()).import_(context, 'structure', True)
   44.50 +
   44.51 +
   44.52 +#
   44.53 +#   Filesystem export/import adapters
   44.54 +#
   44.55 +class FolderishExporterImporter(object):
   44.56 +    """ Tree-walking exporter / importer for "folderish" types.
   44.57 +
   44.58 +    Folderish instances are mapped to directories within the 'structure'
   44.59 +    portion of the profile, where the folder's relative path within the site
   44.60 +    corresponds to the path of its directory under 'structure'.
   44.61 +
   44.62 +    The subobjects of a folderish instance are enumerated in the '.objects'
   44.63 +    file in the corresponding directory.  This file is a CSV file, with one
   44.64 +    row per subobject, with the following wtructure::
   44.65 +
   44.66 +     "<subobject id>","<subobject portal_type>"
   44.67 +
   44.68 +    Subobjects themselves are represented as individual files or
   44.69 +    subdirectories within the parent's directory.
   44.70 +    """
   44.71 +
   44.72 +    implements(IFilesystemExporter, IFilesystemImporter)
   44.73 +
   44.74 +    def __init__(self, context):
   44.75 +        self.context = context
   44.76 +
   44.77 +    def listExportableItems(self):
   44.78 +        """ See IFilesystemExporter.
   44.79 +        """
   44.80 +        exportable = self.context.objectItems()
   44.81 +        exportable = [x for x in exportable
   44.82 +                        if not ISetupTool.providedBy(x[1])]
   44.83 +        exportable = [x + (IFilesystemExporter(x[1], None),)
   44.84 +                        for x in exportable]
   44.85 +        return exportable
   44.86 +
   44.87 +    def export(self, export_context, subdir, root=False):
   44.88 +        """ See IFilesystemExporter.
   44.89 +        """
   44.90 +        context = self.context
   44.91 +
   44.92 +        if not root:
   44.93 +            subdir = '%s/%s' % (subdir, context.getId())
   44.94 +
   44.95 +        exportable = self.listExportableItems()
   44.96 +
   44.97 +        stream = StringIO()
   44.98 +        csv_writer = writer(stream)
   44.99 +
  44.100 +        for object_id, object, adapter in exportable:
  44.101 +
  44.102 +            factory_namer = IContentFactoryName(object, None)
  44.103 +            if factory_namer is None:
  44.104 +                factory_name = _getDottedName(object.__class__)
  44.105 +            else:
  44.106 +                factory_name = factory_namer()
  44.107 +
  44.108 +            csv_writer.writerow((object_id, factory_name))
  44.109 +
  44.110 +        export_context.writeDataFile('.objects',
  44.111 +                                     text=stream.getvalue(),
  44.112 +                                     content_type='text/comma-separated-values',
  44.113 +                                     subdir=subdir,
  44.114 +                                    )
  44.115 +
  44.116 +        prop_adapter = IINIAware(context, None)
  44.117 +
  44.118 +        if prop_adapter is not None:
  44.119 +            export_context.writeDataFile('.properties',
  44.120 +                                         text=prop_adapter.as_ini(),
  44.121 +                                         content_type='text/plain',
  44.122 +                                         subdir=subdir,
  44.123 +                                        )
  44.124 +
  44.125 +        for object_id, object, adapter in exportable:
  44.126 +            if adapter is not None:
  44.127 +                adapter.export(export_context, subdir)
  44.128 +
  44.129 +    def import_(self, import_context, subdir, root=False):
  44.130 +        """ See IFilesystemImporter.
  44.131 +        """
  44.132 +        context = self.context
  44.133 +        if not root:
  44.134 +            subdir = '%s/%s' % (subdir, context.getId())
  44.135 +
  44.136 +        prop_adapter = IINIAware(context, None)
  44.137 +
  44.138 +        if prop_adapter is not None:
  44.139 +            prop_text = import_context.readDataFile('.properties',
  44.140 +                                                    subdir=subdir,
  44.141 +                                                   )
  44.142 +            if prop_text is not None:
  44.143 +                prop_adapter.put_ini(prop_text)
  44.144 +
  44.145 +        preserve = import_context.readDataFile('.preserve', subdir)
  44.146 +        must_preserve = self._mustPreserve()
  44.147 +
  44.148 +        prior = context.objectIds()
  44.149 +
  44.150 +        if not preserve:
  44.151 +            preserve = []
  44.152 +        else:
  44.153 +            preserve = _globtest(preserve, prior)
  44.154 +
  44.155 +        preserve.extend([x[0] for x in must_preserve])
  44.156 +
  44.157 +        for id in prior:
  44.158 +            if id not in preserve:
  44.159 +                context._delObject(id)
  44.160 +
  44.161 +        objects = import_context.readDataFile('.objects', subdir)
  44.162 +        if objects is None:
  44.163 +            return
  44.164 +
  44.165 +        dialect = 'excel'
  44.166 +        stream = StringIO(objects)
  44.167 +
  44.168 +        rowiter = reader(stream, dialect)
  44.169 +
  44.170 +        existing = context.objectIds()
  44.171 +
  44.172 +        for object_id, type_name in rowiter:
  44.173 +
  44.174 +            if object_id not in existing:
  44.175 +                object = self._makeInstance(object_id, type_name,
  44.176 +                                            subdir, import_context)
  44.177 +                if object is None:
  44.178 +                    logger = import_context.getLogger('SFWA')
  44.179 +                    logger.warning("Couldn't make instance: %s/%s" %
  44.180 +                                   (subdir, object_id))
  44.181 +                    continue
  44.182 +
  44.183 +            wrapped = context._getOb(object_id)
  44.184 +
  44.185 +            IFilesystemImporter(wrapped).import_(import_context, subdir)
  44.186 +
  44.187 +    def _makeInstance(self, instance_id, type_name, subdir, import_context):
  44.188 +
  44.189 +        context = self.context
  44.190 +        class _OldStyleClass:
  44.191 +            pass
  44.192 +
  44.193 +        if '.' in type_name:
  44.194 +
  44.195 +            factory = _resolveDottedName(type_name)
  44.196 +
  44.197 +            if getattr(factory, '__bases__', None) is not None:
  44.198 +
  44.199 +                def _factory(instance_id,
  44.200 +                             container=self.context,
  44.201 +                             klass=factory):
  44.202 +                    try:
  44.203 +                        instance = klass(instance_id)
  44.204 +                    except (TypeError, ValueError):
  44.205 +                        instance = klass()
  44.206 +                    instance._setId(instance_id)
  44.207 +                    container._setObject(instance_id, instance)
  44.208 +
  44.209 +                    return instance
  44.210 +
  44.211 +                factory = _factory
  44.212 +
  44.213 +        else:
  44.214 +            factory = zapi.queryAdapter(self.context,
  44.215 +                                        IContentFactory,
  44.216 +                                        name=type_name,
  44.217 +                                       )
  44.218 +        if factory is None:
  44.219 +            return None
  44.220 +
  44.221 +        try:
  44.222 +            instance = factory(instance_id)
  44.223 +        except ValueError: # invalid type
  44.224 +            return None
  44.225 +
  44.226 +        if context._getOb(instance_id, None) is None:
  44.227 +            context._setObject(instance_id, instance) 
  44.228 +
  44.229 +        return context._getOb(instance_id)
  44.230 +
  44.231 +    def _mustPreserve(self):
  44.232 +        return [x for x in self.context.objectItems()
  44.233 +                        if ISetupTool.providedBy(x[1])]
  44.234 + 
  44.235 +
  44.236 +def _globtest(globpattern, namelist):
  44.237 +    """ Filter names in 'namelist', returning those which match 'globpattern'.
  44.238 +    """
  44.239 +    import re
  44.240 +    pattern = globpattern.replace(".", r"\.")       # mask dots
  44.241 +    pattern = pattern.replace("*", r".*")           # change glob sequence
  44.242 +    pattern = pattern.replace("?", r".")            # change glob char
  44.243 +    pattern = '|'.join(pattern.split())             # 'or' each line
  44.244 +
  44.245 +    compiled = re.compile(pattern)
  44.246 +
  44.247 +    return filter(compiled.match, namelist)
  44.248 +
  44.249 +
  44.250 +class CSVAwareFileAdapter(object):
  44.251 +    """ Adapter for content whose "natural" representation is CSV.
  44.252 +    """
  44.253 +    implements(IFilesystemExporter, IFilesystemImporter)
  44.254 +
  44.255 +    def __init__(self, context):
  44.256 +        self.context = context
  44.257 +
  44.258 +    def export(self, export_context, subdir, root=False):
  44.259 +        """ See IFilesystemExporter.
  44.260 +        """
  44.261 +        export_context.writeDataFile('%s.csv' % self.context.getId(),
  44.262 +                                     self.context.as_csv(),
  44.263 +                                     'text/comma-separated-values',
  44.264 +                                     subdir,
  44.265 +                                    )
  44.266 +
  44.267 +    def listExportableItems(self):
  44.268 +        """ See IFilesystemExporter.
  44.269 +        """
  44.270 +        return ()
  44.271 +
  44.272 +    def import_(self, import_context, subdir, root=False):
  44.273 +        """ See IFilesystemImporter.
  44.274 +        """
  44.275 +        cid = self.context.getId()
  44.276 +        data = import_context.readDataFile('%s.csv' % cid, subdir)
  44.277 +        if data is None:
  44.278 +            logger = import_context.getLogger('CSAFA')
  44.279 +            logger.info('no .csv file for %s/%s' % (subdir, cid))
  44.280 +        else:
  44.281 +            stream = StringIO(data)
  44.282 +            self.context.put_csv(stream)
  44.283 +
  44.284 +class INIAwareFileAdapter(object):
  44.285 +    """ Exporter/importer for content whose "natural" representation is an
  44.286 +        '.ini' file.
  44.287 +    """
  44.288 +    implements(IFilesystemExporter, IFilesystemImporter)
  44.289 +
  44.290 +    def __init__(self, context):
  44.291 +        self.context = context
  44.292 +
  44.293 +    def export(self, export_context, subdir, root=False):
  44.294 +        """ See IFilesystemExporter.
  44.295 +        """
  44.296 +        export_context.writeDataFile('%s.ini' % self.context.getId(),
  44.297 +                                     self.context.as_ini(),
  44.298 +                                     'text/plain',
  44.299 +                                     subdir,
  44.300 +                                    )
  44.301 +
  44.302 +    def listExportableItems(self):
  44.303 +        """ See IFilesystemExporter.
  44.304 +        """
  44.305 +        return ()
  44.306 +
  44.307 +    def import_(self, import_context, subdir, root=False):
  44.308 +        """ See IFilesystemImporter.
  44.309 +        """
  44.310 +        cid = self.context.getId()
  44.311 +        data = import_context.readDataFile('%s.ini' % cid, subdir)
  44.312 +        if data is None:
  44.313 +            logger = import_context.getLogger('SGAIFA')
  44.314 +            logger.info('no .ini file for %s/%s' % (subdir, cid))
  44.315 +        else:
  44.316 +            self.context.put_ini(data)
  44.317 +
  44.318 +class SimpleINIAware(object):
  44.319 +    """ Exporter/importer for content which doesn't know from INI.
  44.320 +    """
  44.321 +    implements(IINIAware,)
  44.322 +
  44.323 +    def __init__(self, context):
  44.324 +        self.context = context
  44.325 +
  44.326 +    def getId(self):
  44.327 +        return self.context.getId()
  44.328 +
  44.329 +    def as_ini(self):
  44.330 +        """
  44.331 +        """
  44.332 +        context = self.context
  44.333 +        parser = ConfigParser()
  44.334 +        stream = StringIO()
  44.335 +        for k, v in context.propertyItems():
  44.336 +            parser.set('DEFAULT', k, str(v))
  44.337 +        parser.write(stream)
  44.338 +        return stream.getvalue()
  44.339 +
  44.340 +    def put_ini(self, text):
  44.341 +        """
  44.342 +        """
  44.343 +        context = self.context
  44.344 +        parser = ConfigParser()
  44.345 +        parser.readfp(StringIO(text))
  44.346 +        for option, value in parser.defaults().items():
  44.347 +            prop_type = context.getPropertyType(option)
  44.348 +            if prop_type is None:
  44.349 +                context._setProperty(option, value, 'string')
  44.350 +            else:
  44.351 +                context._updateProperty(option, value)
  44.352 +
  44.353 +class FauxDAVRequest:
  44.354 +
  44.355 +    def __init__(self, **kw):
  44.356 +        self._data = {}
  44.357 +        self._headers = {}
  44.358 +        self._data.update(kw)
  44.359 +
  44.360 +    def __getitem__(self, key):
  44.361 +        return self._data[key]
  44.362 +
  44.363 +    def get(self, key, default=None):
  44.364 +        return self._data.get(key, default)
  44.365 +
  44.366 +    def get_header(self, key, default=None):
  44.367 +        return self._headers.get(key, default)
  44.368 +
  44.369 +class FauxDAVResponse:
  44.370 +    def setHeader(self, key, value, lock=False):
  44.371 +        pass  # stub this out to mollify webdav.Resource
  44.372 +    def setStatus(self, value, reason=None):
  44.373 +        pass  # stub this out to mollify webdav.Resource
  44.374 +
  44.375 +class DAVAwareFileAdapter(object):
  44.376 +    """ Exporter/importer for content who handle their own FTP / DAV PUTs.
  44.377 +    """
  44.378 +    implements(IFilesystemExporter, IFilesystemImporter)
  44.379 +
  44.380 +    def __init__(self, context):
  44.381 +        self.context = context
  44.382 +
  44.383 +    def _getFileName(self):
  44.384 +        """ Return the name under which our file data is stored.
  44.385 +        """
  44.386 +        return '%s' % self.context.getId()
  44.387 +
  44.388 +    def export(self, export_context, subdir, root=False):
  44.389 +        """ See IFilesystemExporter.
  44.390 +        """
  44.391 +        export_context.writeDataFile(self._getFileName(),
  44.392 +                                     self.context.manage_FTPget(),
  44.393 +                                     'text/plain',
  44.394 +                                     subdir,
  44.395 +                                    )
  44.396 +
  44.397 +    def listExportableItems(self):
  44.398 +        """ See IFilesystemExporter.
  44.399 +        """
  44.400 +        return ()
  44.401 +
  44.402 +    def import_(self, import_context, subdir, root=False):
  44.403 +        """ See IFilesystemImporter.
  44.404 +        """
  44.405 +        cid = self.context.getId()
  44.406 +        data = import_context.readDataFile(self._getFileName(), subdir)
  44.407 +        if data is None:
  44.408 +            logger = import_context.getLogger('SGAIFA')
  44.409 +            logger.info('no .ini file for %s/%s' % (subdir, cid))
  44.410 +        else:
  44.411 +            request = FauxDAVRequest(BODY=data, BODYFILE=StringIO(data))
  44.412 +            response = FauxDAVResponse()
  44.413 +            self.context.PUT(request, response)
    45.1 new file mode 100644
    45.2 --- /dev/null
    45.3 +++ b/context.py
    45.4 @@ -0,0 +1,645 @@
    45.5 +##############################################################################
    45.6 +#
    45.7 +# Copyright (c) 2004 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 +""" Various context implementations for export / import of configurations.
   45.18 +
   45.19 +Wrappers representing the state of an import / export operation.
   45.20 +
   45.21 +$Id: context.py 41502 2006-01-30 17:48:12Z efge $
   45.22 +"""
   45.23 +
   45.24 +import logging
   45.25 +import os
   45.26 +import time
   45.27 +from StringIO import StringIO
   45.28 +from tarfile import TarFile
   45.29 +from tarfile import TarInfo
   45.30 +
   45.31 +from AccessControl import ClassSecurityInfo
   45.32 +from Acquisition import aq_inner
   45.33 +from Acquisition import aq_parent
   45.34 +from Acquisition import aq_self
   45.35 +from Acquisition import Implicit
   45.36 +from DateTime.DateTime import DateTime
   45.37 +from Globals import InitializeClass
   45.38 +from OFS.DTMLDocument import DTMLDocument
   45.39 +from OFS.Folder import Folder
   45.40 +from OFS.Image import File
   45.41 +from OFS.Image import Image
   45.42 +from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
   45.43 +from Products.PythonScripts.PythonScript import PythonScript
   45.44 +from zope.interface import implements
   45.45 +
   45.46 +from interfaces import IExportContext
   45.47 +from interfaces import IImportContext
   45.48 +from interfaces import IWriteLogger
   45.49 +from interfaces import SKIPPED_FILES
   45.50 +from interfaces import SKIPPED_SUFFIXES
   45.51 +from permissions import ManagePortal
   45.52 +
   45.53 +
   45.54 +class Logger:
   45.55 +
   45.56 +    implements(IWriteLogger)
   45.57 +
   45.58 +    def __init__(self, id, messages):
   45.59 +        """Initialize the logger with a name and an optional level.
   45.60 +        """
   45.61 +        self._id = id
   45.62 +        self._messages = messages
   45.63 +        self._logger = logging.getLogger('GenericSetup.%s' % id)
   45.64 +
   45.65 +    def debug(self, msg, *args, **kwargs):
   45.66 +        """Log 'msg % args' with severity 'DEBUG'.
   45.67 +        """
   45.68 +        self.log(logging.DEBUG, msg, *args, **kwargs)
   45.69 +
   45.70 +    def info(self, msg, *args, **kwargs):
   45.71 +        """Log 'msg % args' with severity 'INFO'.
   45.72 +        """
   45.73 +        self.log(logging.INFO, msg, *args, **kwargs)
   45.74 +
   45.75 +    def warning(self, msg, *args, **kwargs):
   45.76 +        """Log 'msg % args' with severity 'WARNING'.
   45.77 +        """
   45.78 +        self.log(logging.WARNING, msg, *args, **kwargs)
   45.79 +
   45.80 +    def error(self, msg, *args, **kwargs):
   45.81 +        """Log 'msg % args' with severity 'ERROR'.
   45.82 +        """
   45.83 +        self.log(logging.ERROR, msg, *args, **kwargs)
   45.84 +
   45.85 +    def exception(self, msg, *args):
   45.86 +        """Convenience method for logging an ERROR with exception information.
   45.87 +        """
   45.88 +        self.error(msg, *args, **{'exc_info': 1})
   45.89 +
   45.90 +    def critical(self, msg, *args, **kwargs):
   45.91 +        """Log 'msg % args' with severity 'CRITICAL'.
   45.92 +        """
   45.93 +        self.log(logging.CRITICAL, msg, *args, **kwargs)
   45.94 +
   45.95 +    def log(self, level, msg, *args, **kwargs):
   45.96 +        """Log 'msg % args' with the integer severity 'level'.
   45.97 +        """
   45.98 +        self._messages.append((level, self._id, msg))
   45.99 +        self._logger.log(level, msg, *args, **kwargs)
  45.100 +
  45.101 +
  45.102 +class BaseContext( Implicit ):
  45.103 +
  45.104 +    security = ClassSecurityInfo()
  45.105 +
  45.106 +    def __init__( self, tool, encoding ):
  45.107 +
  45.108 +        self._tool = tool
  45.109 +        self._site = aq_parent( aq_inner( tool ) )
  45.110 +        self._loggers = {}
  45.111 +        self._messages = []
  45.112 +        self._encoding = encoding
  45.113 +        self._should_purge = True
  45.114 +
  45.115 +    security.declareProtected( ManagePortal, 'getSite' )
  45.116 +    def getSite( self ):
  45.117 +
  45.118 +        """ See ISetupContext.
  45.119 +        """
  45.120 +        return aq_self(self._site)
  45.121 +
  45.122 +    security.declareProtected( ManagePortal, 'getSetupTool' )
  45.123 +    def getSetupTool( self ):
  45.124 +
  45.125 +        """ See ISetupContext.
  45.126 +        """
  45.127 +        return self._tool
  45.128 +
  45.129 +    security.declareProtected( ManagePortal, 'getEncoding' )
  45.130 +    def getEncoding( self ):
  45.131 +
  45.132 +        """ See ISetupContext.
  45.133 +        """
  45.134 +        return self._encoding
  45.135 +
  45.136 +    security.declareProtected( ManagePortal, 'getLogger' )
  45.137 +    def getLogger( self, name ):
  45.138 +        """ See ISetupContext.
  45.139 +        """
  45.140 +        return self._loggers.setdefault(name, Logger(name, self._messages))
  45.141 +
  45.142 +    security.declareProtected( ManagePortal, 'listNotes' )
  45.143 +    def listNotes(self):
  45.144 +
  45.145 +        """ See ISetupContext.
  45.146 +        """
  45.147 +        return self._messages[:]
  45.148 +
  45.149 +    security.declareProtected( ManagePortal, 'clearNotes' )
  45.150 +    def clearNotes(self):
  45.151 +
  45.152 +        """ See ISetupContext.
  45.153 +        """
  45.154 +        self._messages[:] = []
  45.155 +
  45.156 +    security.declareProtected( ManagePortal, 'shouldPurge' )
  45.157 +    def shouldPurge( self ):
  45.158 +
  45.159 +        """ See ISetupContext.
  45.160 +        """
  45.161 +        return self._should_purge
  45.162 +
  45.163 +
  45.164 +class DirectoryImportContext( BaseContext ):
  45.165 +
  45.166 +    implements(IImportContext)
  45.167 +
  45.168 +    security = ClassSecurityInfo()
  45.169 +
  45.170 +    def __init__( self
  45.171 +                , tool
  45.172 +                , profile_path
  45.173 +                , should_purge=False
  45.174 +                , encoding=None
  45.175 +                ):
  45.176 +
  45.177 +        BaseContext.__init__( self, tool, encoding )
  45.178 +        self._profile_path = profile_path
  45.179 +        self._should_purge = bool( should_purge )
  45.180 +
  45.181 +    security.declareProtected( ManagePortal, 'readDataFile' )
  45.182 +    def readDataFile( self, filename, subdir=None ):
  45.183 +
  45.184 +        """ See IImportContext.
  45.185 +        """
  45.186 +        if subdir is None:
  45.187 +            full_path = os.path.join( self._profile_path, filename )
  45.188 +        else:
  45.189 +            full_path = os.path.join( self._profile_path, subdir, filename )
  45.190 +
  45.191 +        if not os.path.exists( full_path ):
  45.192 +            return None
  45.193 +
  45.194 +        file = open( full_path, 'rb' )
  45.195 +        result = file.read()
  45.196 +        file.close()
  45.197 +
  45.198 +        return result
  45.199 +
  45.200 +    security.declareProtected( ManagePortal, 'getLastModified' )
  45.201 +    def getLastModified( self, path ):
  45.202 +
  45.203 +        """ See IImportContext.
  45.204 +        """
  45.205 +        full_path = os.path.join( self._profile_path, path )
  45.206 +
  45.207 +        if not os.path.exists( full_path ):
  45.208 +            return None
  45.209 +
  45.210 +        return DateTime( os.path.getmtime( full_path ) )
  45.211 +
  45.212 +    security.declareProtected( ManagePortal, 'isDirectory' )
  45.213 +    def isDirectory( self, path ):
  45.214 +
  45.215 +        """ See IImportContext.
  45.216 +        """
  45.217 +        full_path = os.path.join( self._profile_path, path )
  45.218 +
  45.219 +        if not os.path.exists( full_path ):
  45.220 +            return None
  45.221 +
  45.222 +        return os.path.isdir( full_path )
  45.223 +
  45.224 +    security.declareProtected( ManagePortal, 'listDirectory' )
  45.225 +    def listDirectory(self, path, skip=SKIPPED_FILES,
  45.226 +                      skip_suffixes=SKIPPED_SUFFIXES):
  45.227 +
  45.228 +        """ See IImportContext.
  45.229 +        """
  45.230 +        if path is None:
  45.231 +            path = ''
  45.232 +
  45.233 +        full_path = os.path.join( self._profile_path, path )
  45.234 +
  45.235 +        if not os.path.exists( full_path ) or not os.path.isdir( full_path ):
  45.236 +            return None
  45.237 +
  45.238 +        names = []
  45.239 +        for name in os.listdir(full_path):
  45.240 +            if name in skip:
  45.241 +                continue
  45.242 +            if [s for s in skip_suffixes if name.endswith(s)]:
  45.243 +                continue
  45.244 +            names.append(name)
  45.245 +
  45.246 +        return names
  45.247 +
  45.248 +InitializeClass( DirectoryImportContext )
  45.249 +
  45.250 +
  45.251 +class DirectoryExportContext( BaseContext ):
  45.252 +
  45.253 +    implements(IExportContext)
  45.254 +
  45.255 +    security = ClassSecurityInfo()
  45.256 +
  45.257 +    def __init__( self, tool, profile_path, encoding=None ):
  45.258 +
  45.259 +        BaseContext.__init__( self, tool, encoding )
  45.260 +        self._profile_path = profile_path
  45.261 +
  45.262 +    security.declareProtected( ManagePortal, 'writeDataFile' )
  45.263 +    def writeDataFile( self, filename, text, content_type, subdir=None ):
  45.264 +
  45.265 +        """ See IExportContext.
  45.266 +        """
  45.267 +        if subdir is None:
  45.268 +            prefix = self._profile_path
  45.269 +        else:
  45.270 +            prefix = os.path.join( self._profile_path, subdir )
  45.271 +
  45.272 +        full_path = os.path.join( prefix, filename )
  45.273 +
  45.274 +        if not os.path.exists( prefix ):
  45.275 +            os.makedirs( prefix )
  45.276 +
  45.277 +        mode = content_type.startswith( 'text/' ) and 'w' or 'wb'
  45.278 +
  45.279 +        file = open( full_path, mode )
  45.280 +        file.write( text )
  45.281 +        file.close()
  45.282 +
  45.283 +InitializeClass( DirectoryExportContext )
  45.284 +
  45.285 +
  45.286 +class TarballImportContext( BaseContext ):
  45.287 +
  45.288 +    implements(IImportContext)
  45.289 +
  45.290 +    security = ClassSecurityInfo()
  45.291 +
  45.292 +    def __init__( self, tool, archive_bits, encoding=None, should_purge=False ):
  45.293 +
  45.294 +        BaseContext.__init__( self, tool, encoding )
  45.295 +        timestamp = time.gmtime()
  45.296 +        self._archive_stream = StringIO(archive_bits)
  45.297 +        self._archive = TarFile.open( 'foo.bar', 'r:gz'
  45.298 +                                    , self._archive_stream )
  45.299 +        self._should_purge = bool( should_purge )
  45.300 +
  45.301 +    def readDataFile( self, filename, subdir=None ):
  45.302 +
  45.303 +        """ See IImportContext.
  45.304 +        """
  45.305 +        if subdir is not None:
  45.306 +            filename = '/'.join( ( subdir, filename ) )
  45.307 +
  45.308 +        try:
  45.309 +            file = self._archive.extractfile( filename )
  45.310 +        except KeyError:
  45.311 +            return None
  45.312 +
  45.313 +        return file.read()
  45.314 +
  45.315 +    def getLastModified( self, path ):
  45.316 +
  45.317 +        """ See IImportContext.
  45.318 +        """
  45.319 +        info = self._getTarInfo( path )
  45.320 +        return info and info.mtime or None
  45.321 +
  45.322 +    def isDirectory( self, path ):
  45.323 +
  45.324 +        """ See IImportContext.
  45.325 +        """
  45.326 +        info = self._getTarInfo( path )
  45.327 +
  45.328 +        if info is not None:
  45.329 +            return info.isdir()
  45.330 +
  45.331 +    def listDirectory(self, path, skip=SKIPPED_FILES,
  45.332 +                      skip_suffixes=SKIPPED_SUFFIXES):
  45.333 +
  45.334 +        """ See IImportContext.
  45.335 +        """
  45.336 +        if path is None:  # root is special case:  no leading '/'
  45.337 +            path = ''
  45.338 +        else:
  45.339 +            if not self.isDirectory(path):
  45.340 +                return None
  45.341 +
  45.342 +            if path[-1] != '/':
  45.343 +                path = path + '/'
  45.344 +
  45.345 +        pfx_len = len(path)
  45.346 +
  45.347 +        names = []
  45.348 +        for name in self._archive.getnames():
  45.349 +            if name == path or not name.startswith(path):
  45.350 +                continue
  45.351 +            name = name[pfx_len:]
  45.352 +            if '/' in name or name in skip:
  45.353 +                continue
  45.354 +            if [s for s in skip_suffixes if name.endswith(s)]:
  45.355 +                continue
  45.356 +            names.append(name)
  45.357 +
  45.358 +        return names
  45.359 +
  45.360 +    def shouldPurge( self ):
  45.361 +
  45.362 +        """ See IImportContext.
  45.363 +        """
  45.364 +        return self._should_purge
  45.365 +
  45.366 +    def _getTarInfo( self, path ):
  45.367 +        if path[-1] == '/':
  45.368 +            path = path[:-1]
  45.369 +        try:
  45.370 +            return self._archive.getmember( path )
  45.371 +        except KeyError:
  45.372 +            pass
  45.373 +        try:
  45.374 +            return self._archive.getmember( path + '/' )
  45.375 +        except KeyError:
  45.376 +            return None
  45.377 +
  45.378 +
  45.379 +class TarballExportContext( BaseContext ):
  45.380 +
  45.381 +    implements(IExportContext)
  45.382 +
  45.383 +    security = ClassSecurityInfo()
  45.384 +
  45.385 +    def __init__( self, tool, encoding=None ):
  45.386 +
  45.387 +        BaseContext.__init__( self, tool, encoding )
  45.388 +
  45.389 +        timestamp = time.gmtime()
  45.390 +        archive_name = ( 'setup_tool-%4d%02d%02d%02d%02d%02d.tar.gz'
  45.391 +                       % timestamp[:6] )
  45.392 +
  45.393 +        self._archive_stream = StringIO()
  45.394 +        self._archive_filename = archive_name
  45.395 +        self._archive = TarFile.open( archive_name, 'w:gz'
  45.396 +                                    , self._archive_stream )
  45.397 +
  45.398 +    security.declareProtected( ManagePortal, 'writeDataFile' )
  45.399 +    def writeDataFile( self, filename, text, content_type, subdir=None ):
  45.400 +
  45.401 +        """ See IExportContext.
  45.402 +        """
  45.403 +        if subdir is not None:
  45.404 +            filename = '/'.join( ( subdir, filename ) )
  45.405 +
  45.406 +        stream = StringIO( text )
  45.407 +        info = TarInfo( filename )
  45.408 +        info.size = len( text )
  45.409 +        info.mtime = time.time()
  45.410 +        self._archive.addfile( info, stream )
  45.411 +
  45.412 +    security.declareProtected( ManagePortal, 'getArchive' )
  45.413 +    def getArchive( self ):
  45.414 +
  45.415 +        """ Close the archive, and return it as a big string.
  45.416 +        """
  45.417 +        self._archive.close()
  45.418 +        return self._archive_stream.getvalue()
  45.419 +
  45.420 +    security.declareProtected( ManagePortal, 'getArchiveFilename' )
  45.421 +    def getArchiveFilename( self ):
  45.422 +
  45.423 +        """ Close the archive, and return it as a big string.
  45.424 +        """
  45.425 +        return self._archive_filename
  45.426 +
  45.427 +InitializeClass( TarballExportContext )
  45.428 +
  45.429 +
  45.430 +class SnapshotExportContext( BaseContext ):
  45.431 +
  45.432 +    implements(IExportContext)
  45.433 +
  45.434 +    security = ClassSecurityInfo()
  45.435 +
  45.436 +    def __init__( self, tool, snapshot_id, encoding=None ):
  45.437 +
  45.438 +        BaseContext.__init__( self, tool, encoding )
  45.439 +        self._snapshot_id = snapshot_id
  45.440 +
  45.441 +    security.declareProtected( ManagePortal, 'writeDataFile' )
  45.442 +    def writeDataFile( self, filename, text, content_type, subdir=None ):
  45.443 +
  45.444 +        """ See IExportContext.
  45.445 +        """
  45.446 +        if subdir is not None:
  45.447 +            filename = '/'.join( ( subdir, filename ) )
  45.448 +
  45.449 +        sep = filename.rfind('/')
  45.450 +        if sep != -1:
  45.451 +            subdir = filename[:sep]
  45.452 +            filename = filename[sep+1:]
  45.453 +        folder = self._ensureSnapshotsFolder( subdir )
  45.454 +
  45.455 +        # TODO: switch on content_type
  45.456 +        ob = self._createObjectByType( filename, text, content_type )
  45.457 +        folder._setObject( str( filename ), ob ) # No Unicode IDs!
  45.458 +
  45.459 +    security.declareProtected( ManagePortal, 'getSnapshotURL' )
  45.460 +    def getSnapshotURL( self ):
  45.461 +
  45.462 +        """ See IExportContext.
  45.463 +        """
  45.464 +        return '%s/%s' % ( self._tool.absolute_url(), self._snapshot_id )
  45.465 +
  45.466 +    security.declareProtected( ManagePortal, 'getSnapshotFolder' )
  45.467 +    def getSnapshotFolder( self ):
  45.468 +
  45.469 +        """ See IExportContext.
  45.470 +        """
  45.471 +        return self._ensureSnapshotsFolder()
  45.472 +
  45.473 +    #
  45.474 +    #   Helper methods
  45.475 +    #
  45.476 +    security.declarePrivate( '_createObjectByType' )
  45.477 +    def _createObjectByType( self, name, body, content_type ):
  45.478 +
  45.479 +        if isinstance( body, unicode ):
  45.480 +            encoding = self.getEncoding()
  45.481 +            if encoding is None:
  45.482 +                body = body.encode()
  45.483 +            else:
  45.484 +                body = body.encode( encoding )
  45.485 +
  45.486 +        if name.endswith('.py'):
  45.487 +
  45.488 +            ob = PythonScript( name )
  45.489 +            ob.write( body )
  45.490 +
  45.491 +        elif name.endswith('.dtml'):
  45.492 +
  45.493 +            ob = DTMLDocument( '', __name__=name )
  45.494 +            ob.munge( body )
  45.495 +
  45.496 +        elif content_type in ('text/html', 'text/xml' ):
  45.497 +
  45.498 +            ob = ZopePageTemplate( name, body
  45.499 +                                 , content_type=content_type )
  45.500 +
  45.501 +        elif content_type[:6]=='image/':
  45.502 +
  45.503 +            ob=Image( name, '', body, content_type=content_type )
  45.504 +
  45.505 +        else:
  45.506 +            ob=File( name, '', body, content_type=content_type )
  45.507 +
  45.508 +        return ob
  45.509 +
  45.510 +    security.declarePrivate( '_ensureSnapshotsFolder' )
  45.511 +    def _ensureSnapshotsFolder( self, subdir=None ):
  45.512 +
  45.513 +        """ Ensure that the appropriate snapshot folder exists.
  45.514 +        """
  45.515 +        path = [ 'snapshots', self._snapshot_id ]
  45.516 +
  45.517 +        if subdir is not None:
  45.518 +            path.extend( subdir.split( '/' ) )
  45.519 +
  45.520 +        current = self._tool
  45.521 +
  45.522 +        for element in path:
  45.523 +
  45.524 +            if element not in current.objectIds():
  45.525 +                # No Unicode IDs!
  45.526 +                current._setObject( str( element ), Folder( element ) )
  45.527 +
  45.528 +            current = current._getOb( element )
  45.529 +
  45.530 +        return current
  45.531 +
  45.532 +InitializeClass( SnapshotExportContext )
  45.533 +
  45.534 +
  45.535 +class SnapshotImportContext( BaseContext ):
  45.536 +
  45.537 +    implements(IImportContext)
  45.538 +
  45.539 +    security = ClassSecurityInfo()
  45.540 +
  45.541 +    def __init__( self
  45.542 +                , tool
  45.543 +                , snapshot_id
  45.544 +                , should_purge=False
  45.545 +                , encoding=None
  45.546 +                ):
  45.547 +
  45.548 +        BaseContext.__init__( self, tool, encoding )
  45.549 +        self._snapshot_id = snapshot_id
  45.550 +        self._encoding = encoding
  45.551 +        self._should_purge = bool( should_purge )
  45.552 +
  45.553 +    security.declareProtected( ManagePortal, 'readDataFile' )
  45.554 +    def readDataFile( self, filename, subdir=None ):
  45.555 +
  45.556 +        """ See IImportContext.
  45.557 +        """
  45.558 +        if subdir is not None:
  45.559 +            filename = '/'.join( ( subdir, filename ) )
  45.560 +
  45.561 +        sep = filename.rfind('/')
  45.562 +        if sep != -1:
  45.563 +            subdir = filename[:sep]
  45.564 +            filename = filename[sep+1:]
  45.565 +        try:
  45.566 +            snapshot = self._getSnapshotFolder( subdir )
  45.567 +            object = snapshot._getOb( filename )
  45.568 +        except ( AttributeError, KeyError ):
  45.569 +            return None
  45.570 +
  45.571 +        try:
  45.572 +            return object.read()
  45.573 +        except AttributeError:
  45.574 +            return object.manage_FTPget()
  45.575 +
  45.576 +    security.declareProtected( ManagePortal, 'getLastModified' )
  45.577 +    def getLastModified( self, path ):
  45.578 +
  45.579 +        """ See IImportContext.
  45.580 +        """
  45.581 +        try:
  45.582 +            snapshot = self._getSnapshotFolder()
  45.583 +            object = snapshot.restrictedTraverse( path )
  45.584 +        except ( AttributeError, KeyError ):
  45.585 +            return None
  45.586 +        else:
  45.587 +            return object.bobobase_modification_time()
  45.588 +
  45.589 +    security.declareProtected( ManagePortal, 'isDirectory' )
  45.590 +    def isDirectory( self, path ):
  45.591 +
  45.592 +        """ See IImportContext.
  45.593 +        """
  45.594 +        try:
  45.595 +            snapshot = self._getSnapshotFolder()
  45.596 +            object = snapshot.restrictedTraverse( path )
  45.597 +        except ( AttributeError, KeyError ):
  45.598 +            return None
  45.599 +        else:
  45.600 +            folderish = getattr( object, 'isPrincipiaFolderish', False )
  45.601 +            return bool( folderish )
  45.602 +
  45.603 +    security.declareProtected( ManagePortal, 'listDirectory' )
  45.604 +    def listDirectory(self, path, skip=(), skip_suffixes=()):
  45.605 +
  45.606 +        """ See IImportContext.
  45.607 +        """
  45.608 +        try:
  45.609 +            snapshot = self._getSnapshotFolder()
  45.610 +            subdir = snapshot.restrictedTraverse( path )
  45.611 +        except ( AttributeError, KeyError ):
  45.612 +            return None
  45.613 +        else:
  45.614 +            if not getattr( subdir, 'isPrincipiaFolderish', False ):
  45.615 +                return None
  45.616 +
  45.617 +            names = []
  45.618 +            for name in subdir.objectIds():
  45.619 +                if name in skip:
  45.620 +                    continue
  45.621 +                if [s for s in skip_suffixes if name.endswith(s)]:
  45.622 +                    continue
  45.623 +                names.append(name)
  45.624 +
  45.625 +            return names
  45.626 +
  45.627 +    security.declareProtected( ManagePortal, 'shouldPurge' )
  45.628 +    def shouldPurge( self ):
  45.629 +
  45.630 +        """ See IImportContext.
  45.631 +        """
  45.632 +        return self._should_purge
  45.633 +
  45.634 +    #
  45.635 +    #   Helper methods
  45.636 +    #
  45.637 +    security.declarePrivate( '_getSnapshotFolder' )
  45.638 +    def _getSnapshotFolder( self, subdir=None ):
  45.639 +
  45.640 +        """ Return the appropriate snapshot (sub)folder.
  45.641 +        """
  45.642 +        path = [ 'snapshots', self._snapshot_id ]
  45.643 +
  45.644 +        if subdir is not None:
  45.645 +            path.extend( subdir.split( '/' ) )
  45.646 +
  45.647 +        return self._tool.restrictedTraverse( path )
  45.648 +
  45.649 +InitializeClass( SnapshotImportContext )
    46.1 new file mode 100644
    46.2 --- /dev/null
    46.3 +++ b/differ.py
    46.4 @@ -0,0 +1,231 @@
    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 +""" Diff utilities for comparing configurations.
   46.18 +
   46.19 +$Id: differ.py 41383 2006-01-20 13:49:16Z efge $
   46.20 +"""
   46.21 +
   46.22 +from difflib import unified_diff
   46.23 +import re
   46.24 +
   46.25 +from Globals import InitializeClass
   46.26 +from AccessControl import ClassSecurityInfo
   46.27 +
   46.28 +from interfaces import SKIPPED_FILES
   46.29 +
   46.30 +BLANKS_REGEX = re.compile( r'^\s*$' )
   46.31 +
   46.32 +def unidiff( a
   46.33 +           , b
   46.34 +           , filename_a='original'
   46.35 +           , timestamp_a=None
   46.36 +           , filename_b='modified'
   46.37 +           , timestamp_b=None
   46.38 +           , ignore_blanks=False
   46.39 +           ):
   46.40 +    r"""Compare two sequences of lines; generate the resulting delta.
   46.41 +
   46.42 +    Each sequence must contain individual single-line strings
   46.43 +    ending with newlines. Such sequences can be obtained from the
   46.44 +    `readlines()` method of file-like objects.  The delta
   46.45 +    generated also consists of newline-terminated strings, ready
   46.46 +    to be printed as-is via the writeline() method of a file-like
   46.47 +    object.
   46.48 +
   46.49 +    Note that the last line of a file may *not* have a newline;
   46.50 +    this is reported in the same way that GNU diff reports this.
   46.51 +    *This method only supports UNIX line ending conventions.*
   46.52 +
   46.53 +        filename_a and filename_b are used to generate the header,
   46.54 +        allowing other tools to determine what 'files' were used
   46.55 +        to generate this output.
   46.56 +
   46.57 +        timestamp_a and timestamp_b, when supplied, are expected
   46.58 +        to be last-modified timestamps to be inserted in the
   46.59 +        header, as floating point values since the epoch.
   46.60 +
   46.61 +    Example:
   46.62 +
   46.63 +    >>> print ''.join(UniDiffer().compare(
   46.64 +    ...     'one\ntwo\nthree\n'.splitlines(1),
   46.65 +    ...     'ore\ntree\nemu\n'.splitlines(1))),
   46.66 +    +++ original
   46.67 +    --- modified
   46.68 +    @@ -1,3 +1,3 @@
   46.69 +    -one
   46.70 +    +ore
   46.71 +    -two
   46.72 +    -three
   46.73 +    +tree
   46.74 +    +emu
   46.75 +    """
   46.76 +    if isinstance( a, basestring ):
   46.77 +        a = a.splitlines()
   46.78 +
   46.79 +    if isinstance( b, basestring ):
   46.80 +        b = b.splitlines()
   46.81 +
   46.82 +    if ignore_blanks:
   46.83 +        a = [ x for x in a if not BLANKS_REGEX.match( x ) ]
   46.84 +        b = [ x for x in b if not BLANKS_REGEX.match( x ) ]
   46.85 +
   46.86 +    return unified_diff( a
   46.87 +                       , b
   46.88 +                       , filename_a
   46.89 +                       , filename_b
   46.90 +                       , timestamp_a
   46.91 +                       , timestamp_b
   46.92 +                       , lineterm=""
   46.93 +                       )
   46.94 +
   46.95 +class ConfigDiff:
   46.96 +
   46.97 +    security = ClassSecurityInfo()
   46.98 +
   46.99 +    def __init__( self
  46.100 +                , lhs
  46.101 +                , rhs
  46.102 +                , missing_as_empty=False
  46.103 +                , ignore_blanks=False
  46.104 +                , skip=SKIPPED_FILES
  46.105 +                ):
  46.106 +        self._lhs = lhs
  46.107 +        self._rhs = rhs
  46.108 +        self._missing_as_empty = missing_as_empty
  46.109 +        self._ignore_blanks=ignore_blanks
  46.110 +        self._skip = skip
  46.111 +
  46.112 +    security.declarePrivate( 'compareDirectories' )
  46.113 +    def compareDirectories( self, subdir=None ):
  46.114 +
  46.115 +        lhs_files = self._lhs.listDirectory( subdir, self._skip )
  46.116 +        if lhs_files is None:
  46.117 +            lhs_files = []
  46.118 +
  46.119 +        rhs_files = self._rhs.listDirectory( subdir, self._skip )
  46.120 +        if rhs_files is None:
  46.121 +            rhs_files = []
  46.122 +
  46.123 +        added = [ f for f in rhs_files if f not in lhs_files ]
  46.124 +        removed = [ f for f in lhs_files if f not in rhs_files ]
  46.125 +        all_files = lhs_files + added
  46.126 +        all_files.sort()
  46.127 +
  46.128 +        result = []
  46.129 +
  46.130 +        for filename in all_files:
  46.131 +
  46.132 +            if subdir is None:
  46.133 +                pathname = filename
  46.134 +            else:
  46.135 +                pathname = '%s/%s' % ( subdir, filename )
  46.136 +
  46.137 +            if filename not in added:
  46.138 +                isDirectory = self._lhs.isDirectory( pathname )
  46.139 +            else:
  46.140 +                isDirectory = self._rhs.isDirectory( pathname )
  46.141 +
  46.142 +            if not self._missing_as_empty and filename in removed:
  46.143 +
  46.144 +                if isDirectory:
  46.145 +                    result.append( '** Directory %s removed\n' % pathname )
  46.146 +                    result.extend( self.compareDirectories( pathname ) )
  46.147 +                else:
  46.148 +                    result.append( '** File %s removed\n' % pathname )
  46.149 +
  46.150 +            elif not self._missing_as_empty and filename in added:
  46.151 +
  46.152 +                if isDirectory:
  46.153 +                    result.append( '** Directory %s added\n' % pathname )
  46.154 +                    result.extend( self.compareDirectories( pathname ) )
  46.155 +                else:
  46.156 +                    result.append( '** File %s added\n' % pathname )
  46.157 +
  46.158 +            elif isDirectory:
  46.159 +
  46.160 +                result.extend( self.compareDirectories( pathname ) )
  46.161 +
  46.162 +                if ( filename not in added + removed and
  46.163 +                    not self._rhs.isDirectory( pathname ) ):
  46.164 +
  46.165 +                    result.append( '** Directory %s replaced with a file of '
  46.166 +                                   'the same name\n' % pathname )
  46.167 +
  46.168 +                    if self._missing_as_empty:
  46.169 +                        result.extend( self.compareFiles( filename, subdir ) )
  46.170 +            else:
  46.171 +                if ( filename not in added + removed and
  46.172 +                     self._rhs.isDirectory( pathname ) ):
  46.173 +
  46.174 +                    result.append( '** File %s replaced with a directory of '
  46.175 +                                   'the same name\n' % pathname )
  46.176 +
  46.177 +                    if self._missing_as_empty:
  46.178 +                        result.extend( self.compareFiles( filename, subdir ) )
  46.179 +
  46.180 +                    result.extend( self.compareDirectories( pathname ) )
  46.181 +                else:
  46.182 +                    result.extend( self.compareFiles( filename, subdir ) )
  46.183 +
  46.184 +        return result
  46.185 +
  46.186 +    security.declarePrivate( 'compareFiles' )
  46.187 +    def compareFiles( self, filename, subdir=None ):
  46.188 +
  46.189 +        if subdir is None:
  46.190 +            path = filename
  46.191 +        else:
  46.192 +            path = '%s/%s' % ( subdir, filename )
  46.193 +
  46.194 +        lhs_file = self._lhs.readDataFile( filename, subdir )
  46.195 +        lhs_time = self._lhs.getLastModified( path )
  46.196 +
  46.197 +        if lhs_file is None:
  46.198 +            assert self._missing_as_empty
  46.199 +            lhs_file = ''
  46.200 +            lhs_time = 0
  46.201 +
  46.202 +        rhs_file = self._rhs.readDataFile( filename, subdir )
  46.203 +        rhs_time = self._rhs.getLastModified( path )
  46.204 +
  46.205 +        if rhs_file is None:
  46.206 +            assert self._missing_as_empty
  46.207 +            rhs_file = ''
  46.208 +            rhs_time = 0
  46.209 +
  46.210 +        if lhs_file == rhs_file:
  46.211 +            diff_lines = []
  46.212 +        else:
  46.213 +            diff_lines = unidiff( lhs_file
  46.214 +                                , rhs_file
  46.215 +                                , filename_a=path
  46.216 +                                , timestamp_a=lhs_time
  46.217 +                                , filename_b=path
  46.218 +                                , timestamp_b=rhs_time
  46.219 +                                , ignore_blanks=self._ignore_blanks
  46.220 +                                )
  46.221 +            diff_lines = list( diff_lines ) # generator
  46.222 +
  46.223 +        if len( diff_lines ) == 0: # No *real* difference found
  46.224 +            return []
  46.225 +
  46.226 +        diff_lines.insert( 0, 'Index: %s' % path )
  46.227 +        diff_lines.insert( 1, '=' * 67 )
  46.228 +
  46.229 +        return diff_lines
  46.230 +
  46.231 +    security.declarePrivate( 'compare' )
  46.232 +    def compare( self ):
  46.233 +        return '\n'.join( self.compareDirectories() )
  46.234 +
  46.235 +InitializeClass( ConfigDiff )
    47.1 new file mode 100644
    47.2 --- /dev/null
    47.3 +++ b/doc/handlers.txt
    47.4 @@ -0,0 +1,83 @@
    47.5 +How-to: Writing setup handlers for GenericSetup
    47.6 +
    47.7 +  If your products subclass existing tools or provide new tools (or new
    47.8 +  sub-object classes) they might need their own setup handlers in order to
    47.9 +  make use of GenericSetup.
   47.10 +
   47.11 +  Step 1:
   47.12 +
   47.13 +    Identify those classes in your product that need their own setup handlers.
   47.14 +    In theory you don't need your own handlers for classes which implement a
   47.15 +    CMF tool interface that already has a setup adapter. In practice the
   47.16 +    adapters shipped with the CMF sometimes use methods that are not part of
   47.17 +    the interface, so you have to verify they really work for your classes.
   47.18 +
   47.19 +  Step 2:
   47.20 +
   47.21 +    Make sure those classes that need setup handlers have Zope 3 style
   47.22 +    interfaces. Later you will write setup adapters for those interfaces.
   47.23 +
   47.24 +  Step 3:
   47.25 +
   47.26 +    Create an 'exportimport' module inside your product. If you plan to write
   47.27 +    many setup handlers this can be a sub-package.
   47.28 +
   47.29 +  Step 4:
   47.30 +
   47.31 +    Decide which kind of setup handler you need:
   47.32 +
   47.33 +    - 'body adapter':
   47.34 +    For objects represented by a complete file body. Provides IBody.
   47.35 +
   47.36 +    - 'XML adapter':
   47.37 +    'body adapter' in XML format. Also provides IBody, but has its own base
   47.38 +    class because XML is the preferred format.
   47.39 +
   47.40 +    - 'node adapter':
   47.41 +    For sub-objects represented by an XML node of the parents XML document.
   47.42 +    Provides INode. This is useful for sub-objects of complex tools. Custom
   47.43 +    catalog index or action classes need that kind of adapter.
   47.44 +
   47.45 +    - 'import and export steps':
   47.46 +    Top level handlers that can be registered as import step or export step
   47.47 +    and call the body adapters. Maybe these will become obsolete for tools,
   47.48 +    but currently they are required.
   47.49 +
   47.50 +    If you use the base classes from GenericSetup.utils, XML and node adapters
   47.51 +    are implemented in a very similar way. Both can mix in
   47.52 +    ObjectManagerHelpers and PropertyManagerHelpers.
   47.53 +
   47.54 +  Step 5:
   47.55 +
   47.56 +    CMFCore.exportimport contains many examples for XML and node adapters. If
   47.57 +    you need a pure body adapter, GenericSetup.PythonScripts would be a good
   47.58 +    example. Follow those examples and write your own multi adapter, register
   47.59 +    it for the interface of your class and for ISetupEnviron and don't forget
   47.60 +    to write unit tests.
   47.61 +
   47.62 +    Adapters follow the convention that 'self.context' is always the primary
   47.63 +    adapted object, so the minimal setup context (ISetupEnviron) used in these
   47.64 +    multi adapters is 'self.environ'.
   47.65 +
   47.66 +    XML and body adapters are always also small node adapters. This way the
   47.67 +    XML file of the container contains the information that is necessary to
   47.68 +    create an empty object. The handler of the container has to set up
   47.69 +    sub-objects before we can adapt them and configure them with their own
   47.70 +    handlers. The base classes in GenericSetup.utils will care about that.
   47.71 +
   47.72 +  Step 6:
   47.73 +
   47.74 +    If your adapter is a top-level adapter (e.g for a tool), you need import
   47.75 +    and export steps that know how to use the adapter. Again there are many
   47.76 +    examples in CMFCore.exportimport.
   47.77 +
   47.78 +    To make those steps available you have to add them to export_steps.xml and
   47.79 +    import_steps.xml of a setup profile and to load that profile into the
   47.80 +    setup tool.
   47.81 +
   47.82 +  Step 7:
   47.83 +
   47.84 +    Now you are done. To ship default settings with your product, make your
   47.85 +    settings through the ZMI (or set your stuff up the old way if you have old
   47.86 +    setup code like an Install.py) and export your settings using the setup
   47.87 +    tool.
    48.1 new file mode 100644
    48.2 --- /dev/null
    48.3 +++ b/doc/profiles.txt
    48.4 @@ -0,0 +1,48 @@
    48.5 +Profiles
    48.6 +
    48.7 +  Overview
    48.8 +
    48.9 +    There are two different kinds of profiles: Base profiles and extension
   48.10 +    profiles. Base profiles have no dependencies. Extension profiles are
   48.11 +    profile fragments used to modify base profiles. They can be shipped with
   48.12 +    add-on products or used for customization steps. Importing an extension
   48.13 +    profile adds or overwrites existing settings in a fine-grained way. You
   48.14 +    can't export extension profiles. Snapshots and exports always represent
   48.15 +    the merged settings.
   48.16 +
   48.17 +  Update Directives
   48.18 +
   48.19 +    For some XML elements there are additional attributes and values to
   48.20 +    specify update directives. They are only useful for extension profiles and
   48.21 +    you will never see them in snapshots and exports.
   48.22 +
   48.23 +    The following directives are generally useful for container elements and
   48.24 +    implemented by some setup handlers. Products using GenericSetup can also
   48.25 +    implement other update directives.
   48.26 +
   48.27 +    'id="*"' wildcard
   48.28 +
   48.29 +      Updates all existing items in the container with the same settings.
   48.30 +
   48.31 +    'remove'
   48.32 +
   48.33 +      Removes the specified item if it exists.
   48.34 +
   48.35 +    'insert-before' and 'insert-after'
   48.36 +
   48.37 +      'insert-before' and 'insert-after' specify the position of a new item
   48.38 +      relative to an existing item. If they are omitted or not valid, items
   48.39 +      are appended. You can also use '*' as wildcard. This will insert the new
   48.40 +      item at the top (before all existing items) or the bottom (after all
   48.41 +      existing items). If an item with the given ID exists already, it is
   48.42 +      moved to the specified position. This directive makes only sense for
   48.43 +      ordered containers.
   48.44 +
   48.45 +  Other Special Directives
   48.46 +
   48.47 +    'purge'
   48.48 +
   48.49 +      By default existing settings are purged before applying settings from
   48.50 +      base profiles. Extension profiles are applied in update mode. This
   48.51 +      directive overrides the default behavior. If True the existing settings
   48.52 +      of the current object are always purged, if False they are not purged.
    49.1 new file mode 100644
    49.2 --- /dev/null
    49.3 +++ b/exceptions.py
    49.4 @@ -0,0 +1,22 @@
    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 exceptions.
   49.18 +
   49.19 +$Id: exceptions.py 38575 2005-09-24 09:01:32Z yuppie $
   49.20 +"""
   49.21 +
   49.22 +from AccessControl import ModuleSecurityInfo
   49.23 +security = ModuleSecurityInfo('Products.GenericSetup.exceptions')
   49.24 +
   49.25 +security.declarePublic('BadRequest')
   49.26 +from zExceptions import BadRequest
    50.1 new file mode 100644
    50.2 --- /dev/null
    50.3 +++ b/interfaces.py
    50.4 @@ -0,0 +1,735 @@
    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 interfaces
   50.18 +
   50.19 +$Id: interfaces.py 41502 2006-01-30 17:48:12Z efge $
   50.20 +"""
   50.21 +
   50.22 +from zope.interface import Interface
   50.23 +from zope.schema import Text
   50.24 +from zope.schema import TextLine
   50.25 +
   50.26 +BASE, EXTENSION = range(1, 3)
   50.27 +SKIPPED_FILES = ('CVS', '.svn', '_svn', '_darcs')
   50.28 +SKIPPED_SUFFIXES = ('~',)
   50.29 +
   50.30 +
   50.31 +class IPseudoInterface( Interface ):
   50.32 +
   50.33 +    """ API documentation;  not testable / enforceable.
   50.34 +    """
   50.35 +
   50.36 +
   50.37 +class ISetupEnviron(Interface):
   50.38 +
   50.39 +    """Context for im- and export adapters.
   50.40 +    """
   50.41 +
   50.42 +    def getLogger(name):
   50.43 +        """Get a logger with the specified name, creating it if necessary.
   50.44 +        """
   50.45 +
   50.46 +    def shouldPurge():
   50.47 +        """When installing, should the existing setup be purged?
   50.48 +        """
   50.49 +
   50.50 +
   50.51 +class ISetupContext(ISetupEnviron):
   50.52 +
   50.53 +    """ Context used for export / import plugins.
   50.54 +    """
   50.55 +    def getSite():
   50.56 +
   50.57 +        """ Return the site object being configured / dumped.
   50.58 +        """
   50.59 +
   50.60 +    def getSetupTool():
   50.61 +
   50.62 +        """ Return the site object being configured / dumped.
   50.63 +        """
   50.64 +
   50.65 +    def getEncoding():
   50.66 +
   50.67 +        """ Get the encoding used for configuration data within the site.
   50.68 +
   50.69 +        o Return None if the data should not be encoded.
   50.70 +        """
   50.71 +
   50.72 +    def listNotes():
   50.73 +        """ Return notes recorded by this context.
   50.74 +        
   50.75 +        o Result a sequence of (component, message) tuples
   50.76 +        """
   50.77 +
   50.78 +    def clearNotes():
   50.79 +        """ Clear all notes recorded by this context.
   50.80 +        """
   50.81 +
   50.82 +class IImportContext( ISetupContext ):
   50.83 +
   50.84 +    def readDataFile( filename, subdir=None ):
   50.85 +
   50.86 +        """ Search the current configuration for the requested file.
   50.87 +
   50.88 +        o 'filename' is the name (without path elements) of the file.
   50.89 +
   50.90 +        o 'subdir' is an optional subdirectory;  if not supplied, search
   50.91 +          only the "root" directory.
   50.92 +
   50.93 +        o Return the file contents as a string, or None if the
   50.94 +          file cannot be found.
   50.95 +        """
   50.96 +
   50.97 +    def getLastModified( path ):
   50.98 +
   50.99 +        """ Return the modification timestamp of the item at 'path'.
  50.100 +
  50.101 +        o Result will be a DateTime instance.
  50.102 +
  50.103 +        o Search profiles in the configuration in order.
  50.104 +
  50.105 +        o If the context is filesystem based, return the 'stat' timestamp
  50.106 +          of the file / directory to which 'path' points.
  50.107 +
  50.108 +        o If the context is ZODB-based, return the Zope modification time
  50.109 +          of the object to which 'path' points.
  50.110 +
  50.111 +        o Return None if 'path' does not point to any object.
  50.112 +        """
  50.113 +
  50.114 +    def isDirectory( path ):
  50.115 +
  50.116 +        """ Test whether path points to a directory / folder.
  50.117 +
  50.118 +        o If the context is filesystem based, check that 'path' points to
  50.119 +          a subdirectory within the "root" directory.
  50.120 +
  50.121 +        o If the context is ZODB-based, check that 'path' points to a
  50.122 +          "container" under the context's tool.
  50.123 +
  50.124 +        o Return None if 'path' does not resolve;  otherwise, return a
  50.125 +          bool.
  50.126 +        """
  50.127 +
  50.128 +    def listDirectory( path, skip=SKIPPED_FILES ):
  50.129 +
  50.130 +        """ List IDs of the contents of a  directory / folder.
  50.131 +
  50.132 +        o Omit names in 'skip'.
  50.133 +
  50.134 +        o If 'path' does not point to a directory / folder, return None.
  50.135 +        """
  50.136 +
  50.137 +
  50.138 +class IImportPlugin( IPseudoInterface ):
  50.139 +
  50.140 +    """ Signature for callables used to import portions of site configuration.
  50.141 +    """
  50.142 +    def __call__( context ):
  50.143 +
  50.144 +        """ Perform the setup step.
  50.145 +
  50.146 +        o Return a message describing the work done.
  50.147 +
  50.148 +        o 'context' must implement IImportContext.
  50.149 +        """
  50.150 +
  50.151 +class IExportContext( ISetupContext ):
  50.152 +
  50.153 +    def writeDataFile( filename, text, content_type, subdir=None ):
  50.154 +
  50.155 +        """ Write data into the specified location.
  50.156 +
  50.157 +        o 'filename' is the unqualified name of the file.
  50.158 +
  50.159 +        o 'text' is the content of the file.
  50.160 +
  50.161 +        o 'content_type' is the MIMEtype of the file.
  50.162 +
  50.163 +        o 'subdir', if passed, is a path to a subdirectory / folder in
  50.164 +          which to write the file;  if not passed, write the file to the
  50.165 +          "root" of the target.
  50.166 +        """
  50.167 +
  50.168 +class IExportPlugin( IPseudoInterface ):
  50.169 +
  50.170 +    """ Signature for callables used to export portions of site configuration.
  50.171 +    """
  50.172 +    def __call__( context ):
  50.173 +
  50.174 +        """ Write export data for the site wrapped by context.
  50.175 +
  50.176 +        o Return a message describing the work done.
  50.177 +
  50.178 +        o 'context' must implement IExportContext.  The plugin will use
  50.179 +          its 'writeDataFile' method for each file to be exported.
  50.180 +        """
  50.181 +
  50.182 +class IStepRegistry( Interface ):
  50.183 +
  50.184 +    """ Base interface for step registries.
  50.185 +    """
  50.186 +    def listSteps():
  50.187 +
  50.188 +        """ Return a sequence of IDs of registered steps.
  50.189 +
  50.190 +        o Order is not significant.
  50.191 +        """
  50.192 +
  50.193 +    def listStepMetadata():
  50.194 +
  50.195 +        """ Return a sequence of mappings describing registered steps.
  50.196 +
  50.197 +        o Mappings will be ordered alphabetically.
  50.198 +        """
  50.199 +
  50.200 +    def getStepMetadata( key, default=None ):
  50.201 +
  50.202 +        """ Return a mapping of metadata for the step identified by 'key'.
  50.203 +
  50.204 +        o Return 'default' if no such step is registered.
  50.205 +
  50.206 +        o The 'handler' metadata is available via 'getStep'.
  50.207 +        """
  50.208 +
  50.209 +    def generateXML():
  50.210 +
  50.211 +        """ Return a round-trippable XML representation of the registry.
  50.212 +
  50.213 +        o 'handler' values are serialized using their dotted names.
  50.214 +        """
  50.215 +
  50.216 +    def parseXML( text ):
  50.217 +
  50.218 +        """ Parse 'text'.
  50.219 +        """
  50.220 +
  50.221 +class IImportStepRegistry( IStepRegistry ):
  50.222 +
  50.223 +    """ API for import step registry.
  50.224 +    """
  50.225 +    def sortSteps():
  50.226 +
  50.227 +        """ Return a sequence of registered step IDs
  50.228 +
  50.229 +        o Sequence is sorted topologically by dependency, with the dependent
  50.230 +          steps *after* the steps they depend on.
  50.231 +        """
  50.232 +
  50.233 +    def checkComplete():
  50.234 +
  50.235 +        """ Return a sequence of ( node, edge ) tuples for unsatisifed deps.
  50.236 +        """
  50.237 +
  50.238 +    def getStep( key, default=None ):
  50.239 +
  50.240 +        """ Return the IImportPlugin registered for 'key'.
  50.241 +
  50.242 +        o Return 'default' if no such step is registered.
  50.243 +        """
  50.244 +
  50.245 +    def registerStep( id
  50.246 +                    , version
  50.247 +                    , handler
  50.248 +                    , dependencies=()
  50.249 +                    , title=None
  50.250 +                    , description=None
  50.251 +                    ):
  50.252 +        """ Register a setup step.
  50.253 +
  50.254 +        o 'id' is a unique name for this step,
  50.255 +
  50.256 +        o 'version' is a string for comparing versions, it is preferred to
  50.257 +          be a yyyy/mm/dd-ii formatted string (date plus two-digit
  50.258 +          ordinal).  when comparing two version strings, the version with
  50.259 +          the lower sort order is considered the older version.
  50.260 +
  50.261 +          - Newer versions of a step supplant older ones.
  50.262 +
  50.263 +          - Attempting to register an older one after a newer one results
  50.264 +            in a KeyError.
  50.265 +
  50.266 +        o 'handler' should implement IImportPlugin.
  50.267 +
  50.268 +        o 'dependencies' is a tuple of step ids which have to run before
  50.269 +          this step in order to be able to run at all. Registration of
  50.270 +          steps that have unmet dependencies are deferred until the
  50.271 +          dependencies have been registered.
  50.272 +
  50.273 +        o 'title' is a one-line UI description for this step.
  50.274 +          If None, the first line of the documentation string of the handler
  50.275 +          is used, or the id if no docstring can be found.
  50.276 +
  50.277 +        o 'description' is a one-line UI description for this step.
  50.278 +          If None, the remaining line of the documentation string of
  50.279 +          the handler is used, or default to ''.
  50.280 +        """
  50.281 +
  50.282 +class IExportStepRegistry( IStepRegistry ):
  50.283 +
  50.284 +    """ API for export step registry.
  50.285 +    """
  50.286 +    def getStep( key, default=None ):
  50.287 +
  50.288 +        """ Return the IExportPlugin registered for 'key'.
  50.289 +
  50.290 +        o Return 'default' if no such step is registered.
  50.291 +        """
  50.292 +
  50.293 +    def registerStep( id, handler, title=None, description=None ):
  50.294 +
  50.295 +        """ Register an export step.
  50.296 +
  50.297 +        o 'id' is the unique identifier for this step
  50.298 +
  50.299 +        o 'handler' should implement IExportPlugin.
  50.300 +
  50.301 +        o 'title' is a one-line UI description for this step.
  50.302 +          If None, the first line of the documentation string of the step
  50.303 +          is used, or the id if no docstring can be found.
  50.304 +
  50.305 +        o 'description' is a one-line UI description for this step.
  50.306 +          If None, the remaining line of the documentation string of
  50.307 +          the step is used, or default to ''.
  50.308 +        """
  50.309 +
  50.310 +class IToolsetRegistry( Interface ):
  50.311 +
  50.312 +    """ API for toolset registry.
  50.313 +    """
  50.314 +    def listForbiddenTools():
  50.315 +
  50.316 +        """ Return a list of IDs of tools which must be removed, if present.
  50.317 +        """
  50.318 +
  50.319 +    def addForbiddenTool(tool_id ):
  50.320 +
  50.321 +        """ Add 'tool_id' to the list of forbidden tools.
  50.322 +
  50.323 +        o Raise KeyError if 'tool_id' is already in the list.
  50.324 +
  50.325 +        o Raise ValueError if 'tool_id' is in the "required" list.
  50.326 +        """
  50.327 +
  50.328 +    def listRequiredTools():
  50.329 +
  50.330 +        """ Return a list of IDs of tools which must be present.
  50.331 +        """
  50.332 +
  50.333 +    def getRequiredToolInfo( tool_id ):
  50.334 +
  50.335 +        """ Return a mapping describing a partiuclar required tool.
  50.336 +
  50.337 +        o Keys include:
  50.338 +
  50.339 +          'id' -- the ID of the tool
  50.340 +
  50.341 +          'class' -- a dotted path to its class
  50.342 +
  50.343 +        o Raise KeyError if 'tool_id' id not a known tool.
  50.344 +        """
  50.345 +
  50.346 +    def listRequiredToolInfo():
  50.347 +
  50.348 +        """ Return a list of IDs of tools which must be present.
  50.349 +        """
  50.350 +
  50.351 +    def addRequiredTool( tool_id, dotted_name ):
  50.352 +
  50.353 +        """ Add a tool to our "required" list.
  50.354 +
  50.355 +        o 'tool_id' is the tool's ID.
  50.356 +
  50.357 +        o 'dotted_name' is a dotted (importable) name of the tool's class.
  50.358 +
  50.359 +        o Raise KeyError if we have already registered a class for 'tool_id'.
  50.360 +
  50.361 +        o Raise ValueError if 'tool_id' is in the "forbidden" list.
  50.362 +        """
  50.363 +
  50.364 +class IProfileRegistry( Interface ):
  50.365 +
  50.366 +    """ API for profile registry.
  50.367 +    """
  50.368 +    def getProfileInfo( profile_id, for_=None ):
  50.369 +
  50.370 +        """ Return a mapping describing a registered filesystem profile.
  50.371 +
  50.372 +        o Keys include:
  50.373 +
  50.374 +          'id' -- the ID of the profile
  50.375 +
  50.376 +          'title' -- its title
  50.377 +
  50.378 +          'description' -- a textual description of the profile
  50.379 +
  50.380 +          'path' -- a path to the profile on the filesystem.
  50.381 +
  50.382 +          'product' -- the name of the product to which 'path' is
  50.383 +             relative (None for absolute paths).
  50.384 +
  50.385 +          'type' -- either BASE or EXTENSION
  50.386 +        
  50.387 +        o 'for_', if passed, should be the interface specifying the "site
  50.388 +            type" for which the profile is relevant, e.g.
  50.389 +            Products.CMFCore.interfaces.ISiteRoot or
  50.390 +            Products.PluggableAuthService.interfaces.IPluggableAuthService.
  50.391 +            If 'None', list all profiles.
  50.392 +        """
  50.393 +
  50.394 +    def listProfiles( for_=None ):
  50.395 +
  50.396 +        """ Return a list of IDs for registered profiles.
  50.397 +        
  50.398 +        o 'for_', if passed, should be the interface specifying the "site
  50.399 +            type" for which the profile is relevant, e.g.
  50.400 +            Products.CMFCore.interfaces.ISiteRoot or
  50.401 +            Products.PluggableAuthService.interfaces.IPluggableAuthService.
  50.402 +            If 'None', list all profiles.
  50.403 +        """
  50.404 +
  50.405 +    def listProfileInfo( for_=None ):
  50.406 +
  50.407 +        """ Return a list of mappings describing registered profiles.
  50.408 +
  50.409 +        o See 'getProfileInfo' for a description of the mappings' keys.
  50.410 +        
  50.411 +        o 'for_', if passed, should be the interface specifying the "site
  50.412 +            type" for which the profile is relevant, e.g.
  50.413 +            Products.CMFCore.interfaces.ISiteRoot or
  50.414 +            Products.PluggableAuthService.interfaces.IPluggableAuthService.
  50.415 +            If 'None', list all profiles.
  50.416 +        """
  50.417 +
  50.418 +    def registerProfile( name
  50.419 +                       , title
  50.420 +                       , description
  50.421 +                       , path
  50.422 +                       , product=None
  50.423 +                       , profile_type=BASE
  50.424 +                       , for_=None
  50.425 +                       ):
  50.426 +        """ Add a new profile to the registry.
  50.427 +
  50.428 +        o If an existing profile is already registered for 'product:name',
  50.429 +          raise KeyError.
  50.430 +
  50.431 +        o If 'product' is passed, then 'path' should be interpreted as
  50.432 +          relative to the corresponding product directory.
  50.433 +        
  50.434 +        o 'for_', if passed, should be the interface specifying the "site
  50.435 +          type" for which the profile is relevant, e.g.
  50.436 +          Products.CMFCore.interfaces.ISiteRoot or
  50.437 +          Products.PluggableAuthService.interfaces.IPluggableAuthService.
  50.438 +          If 'None', the profile might be used in any site.
  50.439 +        """
  50.440 +
  50.441 +class ISetupTool( Interface ):
  50.442 +
  50.443 +    """ API for SetupTool.
  50.444 +    """
  50.445 +
  50.446 +    def getEncoding():
  50.447 +
  50.448 +        """ Get the encoding used for configuration data within the site.
  50.449 +
  50.450 +        o Return None if the data should not be encoded.
  50.451 +        """
  50.452 +
  50.453 +    def getImportContextID():
  50.454 +
  50.455 +        """ Get the ID of the active import context.
  50.456 +        """
  50.457 +
  50.458 +    def setImportContext( context_id ):
  50.459 +
  50.460 +        """ Set the ID of the active import context and update the registries.
  50.461 +        """
  50.462 +
  50.463 +    def getImportStepRegistry():
  50.464 +
  50.465 +        """ Return the IImportStepRegistry for the tool.
  50.466 +        """
  50.467 +
  50.468 +    def getExportStepRegistry():
  50.469 +
  50.470 +        """ Return the IExportStepRegistry for the tool.
  50.471 +        """
  50.472 +
  50.473 +    def getToolsetRegistry():
  50.474 +
  50.475 +        """ Return the IToolsetRegistry for the tool.
  50.476 +        """
  50.477 +
  50.478 +    def runImportStep( step_id, run_dependencies=True, purge_old=None ):
  50.479 +
  50.480 +        """ Execute a given setup step
  50.481 +
  50.482 +        o 'step_id' is the ID of the step to run.
  50.483 +
  50.484 +        o If 'purge_old' is True, then run the step after purging any
  50.485 +          "old" setup first (this is the responsibility of the step,
  50.486 +          which must check the context we supply).
  50.487 +
  50.488 +        o If 'run_dependencies' is True, then run any out-of-date
  50.489 +          dependency steps first.
  50.490 +
  50.491 +        o Return a mapping, with keys:
  50.492 +
  50.493 +          'steps' -- a sequence of IDs of the steps run.
  50.494 +
  50.495 +          'messages' -- a dictionary holding messages returned from each
  50.496 +            step
  50.497 +        """
  50.498 +
  50.499 +    def runAllImportSteps( purge_old=None ):
  50.500 +
  50.501 +        """ Run all setup steps in dependency order.
  50.502 +
  50.503 +        o If 'purge_old' is True, then run each step after purging any
  50.504 +          "old" setup first (this is the responsibility of the step,
  50.505 +          which must check the context we supply).
  50.506 +
  50.507 +        o Return a mapping, with keys:
  50.508 +
  50.509 +          'steps' -- a sequence of IDs of the steps run.
  50.510 +
  50.511 +          'messages' -- a dictionary holding messages returned from each
  50.512 +            step
  50.513 +        """
  50.514 +
  50.515 +    def runExportStep( step_id ):
  50.516 +
  50.517 +        """ Generate a tarball containing artifacts from one export step.
  50.518 +
  50.519 +        o 'step_id' identifies the export step.
  50.520 +
  50.521 +        o Return a mapping, with keys:
  50.522 +
  50.523 +          'steps' -- a sequence of IDs of the steps run.
  50.524 +
  50.525 +          'messages' -- a dictionary holding messages returned from each
  50.526 +            step
  50.527 +
  50.528 +          'tarball' -- the stringified tar-gz data.
  50.529 +        """
  50.530 +
  50.531 +    def runAllExportSteps():
  50.532 +
  50.533 +        """ Generate a tarball containing artifacts from all export steps.
  50.534 +
  50.535 +        o Return a mapping, with keys:
  50.536 +
  50.537 +          'steps' -- a sequence of IDs of the steps run.
  50.538 +
  50.539 +          'messages' -- a dictionary holding messages returned from each
  50.540 +            step
  50.541 +
  50.542 +          'tarball' -- the stringified tar-gz data.
  50.543 +        """
  50.544 +
  50.545 +    def createSnapshot( snapshot_id ):
  50.546 +
  50.547 +        """ Create a snapshot folder using all steps.
  50.548 +
  50.549 +        o 'snapshot_id' is the ID of the new folder.
  50.550 +        """
  50.551 +
  50.552 +    def compareConfigurations( lhs_context
  50.553 +                             , rhs_context
  50.554 +                             , missing_as_empty=False
  50.555 +                             , ignore_whitespace=False
  50.556 +                             ):
  50.557 +        """ Compare two configurations.
  50.558 +
  50.559 +        o 'lhs_context' and 'rhs_context' must implement IImportContext.
  50.560 +
  50.561 +        o If 'missing_as_empty', then compare files not present as though
  50.562 +          they were zero-length;  otherwise, omit such files.
  50.563 +
  50.564 +        o If 'ignore_whitespace', then suppress diffs due only to whitespace
  50.565 +          (c.f:  'diff -wbB')
  50.566 +        """
  50.567 +
  50.568 +
  50.569 +class IWriteLogger(Interface):
  50.570 +
  50.571 +    """Write methods used by the python logging Logger.
  50.572 +    """
  50.573 +
  50.574 +    def debug(msg, *args, **kwargs):
  50.575 +        """Log 'msg % args' with severity 'DEBUG'.
  50.576 +        """
  50.577 +
  50.578 +    def info(msg, *args, **kwargs):
  50.579 +        """Log 'msg % args' with severity 'INFO'.
  50.580 +        """
  50.581 +
  50.582 +    def warning(msg, *args, **kwargs):
  50.583 +        """Log 'msg % args' with severity 'WARNING'.
  50.584 +        """
  50.585 +
  50.586 +    def error(msg, *args, **kwargs):
  50.587 +        """Log 'msg % args' with severity 'ERROR'.
  50.588 +        """
  50.589 +
  50.590 +    def exception(msg, *args):
  50.591 +        """Convenience method for logging an ERROR with exception information.
  50.592 +        """
  50.593 +
  50.594 +    def critical(msg, *args, **kwargs):
  50.595 +        """Log 'msg % args' with severity 'CRITICAL'.
  50.596 +        """
  50.597 +
  50.598 +    def log(level, msg, *args, **kwargs):
  50.599 +        """Log 'msg % args' with the integer severity 'level'.
  50.600 +        """
  50.601 +
  50.602 +
  50.603 +class INode(Interface):
  50.604 +
  50.605 +    """Node im- and exporter.
  50.606 +    """
  50.607 +
  50.608 +    node = Text(description=u'Im- and export the object as a DOM node.')
  50.609 +
  50.610 +
  50.611 +class IBody(INode):
  50.612 +
  50.613 +    """Body im- and exporter.
  50.614 +    """
  50.615 +
  50.616 +    body = Text(description=u'Im- and export the object as a file body.')
  50.617 +
  50.618 +    mime_type = TextLine(description=u'MIME type of the file body.')
  50.619 +
  50.620 +    name = TextLine(description=u'Enforce this name for the file.')
  50.621 +
  50.622 +    suffix = TextLine(description=u'Suffix for the file.')
  50.623 +
  50.624 +
  50.625 +class IFilesystemExporter(Interface):
  50.626 +    """ Plugin interface for site structure export.
  50.627 +    """
  50.628 +    def export(export_context, subdir, root=False):
  50.629 +        """ Export our 'context' using the API of 'export_context'.
  50.630 +
  50.631 +        o 'export_context' must implement
  50.632 +          Products.GenericSupport.interfaces.IExportContext.
  50.633 +
  50.634 +        o 'subdir', if passed, is the relative subdirectory containing our
  50.635 +          context within the site.
  50.636 +
  50.637 +        o 'root', if true, indicates that the current context is the
  50.638 +          "root" of an import (this may be used to adjust paths when
  50.639 +          interacting with the context).
  50.640 +        """
  50.641 +
  50.642 +    def listExportableItems():
  50.643 +        """ Return a sequence of the child items to be exported.
  50.644 +
  50.645 +        o Each item in the returned sequence will be a tuple,
  50.646 +          (id, object, adapter) where adapter must implement
  50.647 +          IFilesystemExporter.
  50.648 +        """
  50.649 +
  50.650 +class IFilesystemImporter(Interface):
  50.651 +    """ Plugin interface for site structure export.
  50.652 +    """
  50.653 +    def import_(import_context, subdir, root=False):
  50.654 +        """ Import our 'context' using the API of 'import_context'.
  50.655 +
  50.656 +        o 'import_context' must implement
  50.657 +          Products.GenericSupport.interfaces.IImportContext.
  50.658 +
  50.659 +        o 'subdir', if passed, is the relative subdirectory containing our
  50.660 +          context within the site.
  50.661 +
  50.662 +        o 'root', if true, indicates that the current context is the
  50.663 +          "root" of an import (this may be used to adjust paths when
  50.664 +          interacting with the context).
  50.665 +        """
  50.666 +
  50.667 +class IContentFactory(Interface):
  50.668 +    """ Adapter interface for factories specific to a container.
  50.669 +    """
  50.670 +    def __call__(id):
  50.671 +        """ Return a new instance, seated in the context under 'id'.
  50.672 +        """
  50.673 +
  50.674 +class IContentFactoryName(Interface):
  50.675 +    """ Adapter interface for finding the name of the ICF for an object.
  50.676 +    """
  50.677 +    def __call__():
  50.678 +        """ Return a string, suitable for looking up an IContentFactory.
  50.679 +        
  50.680 +        o The string should allow finding a factory for our context's
  50.681 +          container which would create an "empty" instance of the same
  50.682 +          type as our context.
  50.683 +        """
  50.684 +
  50.685 +class ICSVAware(Interface):
  50.686 +    """ Interface for objects which dump / load 'text/comma-separated-values'.
  50.687 +    """
  50.688 +    def getId():
  50.689 +        """ Return the Zope id of the object.
  50.690 +        """
  50.691 +
  50.692 +    def as_csv():
  50.693 +        """ Return a string representing the object as CSV.
  50.694 +        """
  50.695 +
  50.696 +    def put_csv(fd):
  50.697 +        """ Parse CSV and update the object.
  50.698 +
  50.699 +        o 'fd' must be a file-like object whose 'read' method returns
  50.700 +          CSV text parseable by the 'csv.reader'.
  50.701 +        """
  50.702 +
  50.703 +class IINIAware(Interface):
  50.704 +    """ Interface for objects which dump / load INI-format files..
  50.705 +    """
  50.706 +    def getId():
  50.707 +        """ Return the Zope id of the object.
  50.708 +        """
  50.709 +
  50.710 +    def as_ini():
  50.711 +        """ Return a string representing the object as INI.
  50.712 +        """
  50.713 +
  50.714 +    def put_ini(stream_or_text):
  50.715 +        """ Parse INI-formatted text and update the object.
  50.716 +
  50.717 +        o 'stream_or_text' must be either a string, or else a stream
  50.718 +          directly parseable by ConfigParser.
  50.719 +        """
  50.720 +
  50.721 +class IDAVAware(Interface):
  50.722 +    """ Interface for objects which handle their own FTP / DAV operations.
  50.723 +    """
  50.724 +    def getId():
  50.725 +        """ Return the Zope id of the object.
  50.726 +        """
  50.727 +
  50.728 +    def manage_FTPget():
  50.729 +        """ Return a string representing the object as a file.
  50.730 +        """
  50.731 +
  50.732 +    def PUT(REQUEST, RESPONSE):
  50.733 +        """ Parse file content and update the object.
  50.734 +
  50.735 +        o 'REQUEST' will have a 'get' method, which will have the 
  50.736 +          content object in its "BODY" key.  It will also have 'get_header'
  50.737 +          method, whose headers (e.g., "Content-Type") may affect the
  50.738 +          processing of the body.
  50.739 +        """
    51.1 new file mode 100644
    51.2 --- /dev/null
    51.3 +++ b/permissions.py
    51.4 @@ -0,0 +1,18 @@
    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 +""" GenericSetup product permissions.
   51.18 +
   51.19 +$Id: permissions.py 38575 2005-09-24 09:01:32Z yuppie $
   51.20 +"""
   51.21 +
   51.22 +ManagePortal = 'Manage Site'
    52.1 new file mode 100644
    52.2 --- /dev/null
    52.3 +++ b/registry.py
    52.4 @@ -0,0 +1,767 @@
    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 +""" Classes:  ImportStepRegistry, ExportStepRegistry
   52.18 +
   52.19 +$Id: registry.py 40339 2005-11-23 11:43:31Z yuppie $
   52.20 +"""
   52.21 +
   52.22 +from xml.sax import parseString
   52.23 +
   52.24 +from AccessControl import ClassSecurityInfo
   52.25 +from Acquisition import Implicit
   52.26 +from Globals import InitializeClass
   52.27 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   52.28 +from zope.interface import implements
   52.29 +
   52.30 +from interfaces import BASE
   52.31 +from interfaces import IImportStepRegistry
   52.32 +from interfaces import IExportStepRegistry
   52.33 +from interfaces import IToolsetRegistry
   52.34 +from interfaces import IProfileRegistry
   52.35 +from permissions import ManagePortal
   52.36 +from utils import HandlerBase
   52.37 +from utils import _xmldir
   52.38 +from utils import _getDottedName
   52.39 +from utils import _resolveDottedName
   52.40 +from utils import _extractDocstring
   52.41 +
   52.42 +
   52.43 +class ImportStepRegistry( Implicit ):
   52.44 +
   52.45 +    """ Manage knowledge about steps to create / configure site.
   52.46 +
   52.47 +    o Steps are composed together to define a site profile.
   52.48 +    """
   52.49 +    implements(IImportStepRegistry)
   52.50 +
   52.51 +    security = ClassSecurityInfo()
   52.52 +
   52.53 +    def __init__( self ):
   52.54 +
   52.55 +        self.clear()
   52.56 +
   52.57 +    security.declareProtected( ManagePortal, 'listSteps' )
   52.58 +    def listSteps( self ):
   52.59 +
   52.60 +        """ Return a sequence of IDs of registered steps.
   52.61 +
   52.62 +        o Order is not significant.
   52.63 +        """
   52.64 +        return self._registered.keys()
   52.65 +
   52.66 +    security.declareProtected( ManagePortal, 'sortSteps' )
   52.67 +    def sortSteps( self ):
   52.68 +
   52.69 +        """ Return a sequence of registered step IDs
   52.70 +
   52.71 +        o Sequence is sorted topologically by dependency, with the dependent
   52.72 +          steps *after* the steps they depend on.
   52.73 +        """
   52.74 +        return self._computeTopologicalSort()
   52.75 +
   52.76 +    security.declareProtected( ManagePortal, 'checkComplete' )
   52.77 +    def checkComplete( self ):
   52.78 +
   52.79 +        """ Return a sequence of ( node, edge ) tuples for unsatisifed deps.
   52.80 +        """
   52.81 +        result = []
   52.82 +        seen = {}
   52.83 +
   52.84 +        graph = self._computeTopologicalSort()
   52.85 +
   52.86 +        for node in graph:
   52.87 +
   52.88 +            dependencies = self.getStepMetadata( node )[ 'dependencies' ]
   52.89 +
   52.90 +            for dependency in dependencies:
   52.91 +
   52.92 +                if seen.get( dependency ) is None:
   52.93 +                    result.append( ( node, dependency ) )
   52.94 +
   52.95 +            seen[ node ] = 1
   52.96 +
   52.97 +        return result
   52.98 +
   52.99 +    security.declareProtected( ManagePortal, 'getStepMetadata' )
  52.100 +    def getStepMetadata( self, key, default=None ):
  52.101 +
  52.102 +        """ Return a mapping of metadata for the step identified by 'key'.
  52.103 +
  52.104 +        o Return 'default' if no such step is registered.
  52.105 +
  52.106 +        o The 'handler' metadata is available via 'getStep'.
  52.107 +        """
  52.108 +        result = {}
  52.109 +
  52.110 +        info = self._registered.get( key )
  52.111 +
  52.112 +        if info is None:
  52.113 +            return default
  52.114 +
  52.115 +        return info.copy()
  52.116 +
  52.117 +    security.declareProtected( ManagePortal, 'listStepMetadata' )
  52.118 +    def listStepMetadata( self ):
  52.119 +
  52.120 +        """ Return a sequence of mappings describing registered steps.
  52.121 +
  52.122 +        o Mappings will be ordered alphabetically.
  52.123 +        """
  52.124 +        step_ids = self.listSteps()
  52.125 +        step_ids.sort()
  52.126 +        return [ self.getStepMetadata( x ) for x in step_ids ]
  52.127 +
  52.128 +    security.declareProtected( ManagePortal, 'generateXML' )
  52.129 +    def generateXML( self ):
  52.130 +
  52.131 +        """ Return a round-trippable XML representation of the registry.
  52.132 +
  52.133 +        o 'handler' values are serialized using their dotted names.
  52.134 +        """
  52.135 +        return self._exportTemplate()
  52.136 +
  52.137 +    security.declarePrivate( 'getStep' )
  52.138 +    def getStep( self, key, default=None ):
  52.139 +
  52.140 +        """ Return the IImportPlugin registered for 'key'.
  52.141 +
  52.142 +        o Return 'default' if no such step is registered.
  52.143 +        """
  52.144 +        marker = object()
  52.145 +        info = self._registered.get( key, marker )
  52.146 +
  52.147 +        if info is marker:
  52.148 +            return default
  52.149 +
  52.150 +        return _resolveDottedName( info[ 'handler' ] )
  52.151 +
  52.152 +    security.declarePrivate( 'registerStep' )
  52.153 +    def registerStep( self
  52.154 +                    , id
  52.155 +                    , version
  52.156 +                    , handler
  52.157 +                    , dependencies=()
  52.158 +                    , title=None
  52.159 +                    , description=None
  52.160 +                    ):
  52.161 +        """ Register a setup step.
  52.162 +
  52.163 +        o 'id' is a unique name for this step,
  52.164 +
  52.165 +        o 'version' is a string for comparing versions, it is preferred to
  52.166 +          be a yyyy/mm/dd-ii formatted string (date plus two-digit
  52.167 +          ordinal).  when comparing two version strings, the version with
  52.168 +          the lower sort order is considered the older version.
  52.169 +
  52.170 +          - Newer versions of a step supplant older ones.
  52.171 +
  52.172 +          - Attempting to register an older one after a newer one results
  52.173 +            in a KeyError.
  52.174 +
  52.175 +        o 'handler' should implement IImportPlugin.
  52.176 +
  52.177 +        o 'dependencies' is a tuple of step ids which have to run before
  52.178 +          this step in order to be able to run at all. Registration of
  52.179 +          steps that have unmet dependencies are deferred until the
  52.180 +          dependencies have been registered.
  52.181 +
  52.182 +        o 'title' is a one-line UI description for this step.
  52.183 +          If None, the first line of the documentation string of the handler
  52.184 +          is used, or the id if no docstring can be found.
  52.185 +
  52.186 +        o 'description' is a one-line UI description for this step.
  52.187 +          If None, the remaining line of the documentation string of
  52.188 +          the handler is used, or default to ''.
  52.189 +        """
  52.190 +        already = self.getStepMetadata( id )
  52.191 +
  52.192 +        if already and already[ 'version' ] > version:
  52.193 +            raise KeyError( 'Existing registration for step %s, version %s'
  52.194 +                          % ( id, already[ 'version' ] ) )
  52.195 +
  52.196 +        if title is None or description is None:
  52.197 +
  52.198 +            t, d = _extractDocstring( handler, id, '' )
  52.199 +
  52.200 +            title = title or t
  52.201 +            description = description or d
  52.202 +
  52.203 +        info = { 'id'           : id
  52.204 +               , 'version'      : version
  52.205 +               , 'handler'      : _getDottedName( handler )
  52.206 +               , 'dependencies' : dependencies
  52.207 +               , 'title'        : title
  52.208 +               , 'description'  : description
  52.209 +               }
  52.210 +
  52.211 +        self._registered[ id ] = info
  52.212 +
  52.213 +    security.declarePrivate( 'parseXML' )
  52.214 +    def parseXML( self, text, encoding=None ):
  52.215 +
  52.216 +        """ Parse 'text'.
  52.217 +        """
  52.218 +        reader = getattr( text, 'read', None )
  52.219 +
  52.220 +        if reader is not None:
  52.221 +            text = reader()
  52.222 +
  52.223 +        parser = _ImportStepRegistryParser( encoding )
  52.224 +        parseString( text, parser )
  52.225 +
  52.226 +        return parser._parsed
  52.227 +
  52.228 +    security.declarePrivate( 'clear' )
  52.229 +    def clear( self ):
  52.230 +
  52.231 +        self._registered = {}
  52.232 +
  52.233 +    #
  52.234 +    #   Helper methods
  52.235 +    #
  52.236 +    security.declarePrivate( '_computeTopologicalSort' )
  52.237 +    def _computeTopologicalSort( self ):
  52.238 +
  52.239 +        result = []
  52.240 +
  52.241 +        graph = [ ( x[ 'id' ], x[ 'dependencies' ] )
  52.242 +                    for x in self._registered.values() ]
  52.243 +
  52.244 +        for node, edges in graph:
  52.245 +
  52.246 +            after = -1
  52.247 +
  52.248 +            for edge in edges:
  52.249 +
  52.250 +                if edge in result:
  52.251 +                    after = max( after, result.index( edge ) )
  52.252 +
  52.253 +            result.insert( after + 1, node )
  52.254 +
  52.255 +        return result
  52.256 +
  52.257 +    security.declarePrivate( '_exportTemplate' )
  52.258 +    _exportTemplate = PageTemplateFile( 'isrExport.xml', _xmldir )
  52.259 +
  52.260 +InitializeClass( ImportStepRegistry )
  52.261 +
  52.262 +
  52.263 +class ExportStepRegistry( Implicit ):
  52.264 +
  52.265 +    """ Registry of known site-configuration export steps.
  52.266 +
  52.267 +    o Each step is registered with a unique id.
  52.268 +
  52.269 +    o When called, with the portal object passed in as an argument,
  52.270 +      the step must return a sequence of three-tuples,
  52.271 +      ( 'data', 'content_type', 'filename' ), one for each file exported
  52.272 +      by the step.
  52.273 +
  52.274 +      - 'data' is a string containing the file data;
  52.275 +
  52.276 +      - 'content_type' is the MIME type of the data;
  52.277 +
  52.278 +      - 'filename' is a suggested filename for use when downloading.
  52.279 +
  52.280 +    """
  52.281 +    implements(IExportStepRegistry)
  52.282 +
  52.283 +    security = ClassSecurityInfo()
  52.284 +
  52.285 +    def __init__( self ):
  52.286 +
  52.287 +        self.clear()
  52.288 +
  52.289 +    security.declareProtected( ManagePortal, 'listSteps' )
  52.290 +    def listSteps( self ):
  52.291 +
  52.292 +        """ Return a list of registered step IDs.
  52.293 +        """
  52.294 +        return self._registered.keys()
  52.295 +
  52.296 +    security.declareProtected( ManagePortal, 'getStepMetadata' )
  52.297 +    def getStepMetadata( self, key, default=None ):
  52.298 +
  52.299 +        """ Return a mapping of metadata for the step identified by 'key'.
  52.300 +
  52.301 +        o Return 'default' if no such step is registered.
  52.302 +
  52.303 +        o The 'handler' metadata is available via 'getStep'.
  52.304 +        """
  52.305 +        info = self._registered.get( key )
  52.306 +
  52.307 +        if info is None:
  52.308 +            return default
  52.309 +
  52.310 +        return info.copy()
  52.311 +
  52.312 +    security.declareProtected( ManagePortal, 'listStepMetadata' )
  52.313 +    def listStepMetadata( self ):
  52.314 +
  52.315 +        """ Return a sequence of mappings describing registered steps.
  52.316 +
  52.317 +        o Steps will be alphabetical by ID.
  52.318 +        """
  52.319 +        step_ids = self.listSteps()
  52.320 +        step_ids.sort()
  52.321 +        return [ self.getStepMetadata( x ) for x in step_ids ]
  52.322 +
  52.323 +    security.declareProtected( ManagePortal, 'generateXML' )
  52.324 +    def generateXML( self ):
  52.325 +
  52.326 +        """ Return a round-trippable XML representation of the registry.
  52.327 +
  52.328 +        o 'handler' values are serialized using their dotted names.
  52.329 +        """
  52.330 +        return self._exportTemplate()
  52.331 +
  52.332 +    security.declarePrivate( 'getStep' )
  52.333 +    def getStep( self, key, default=None ):
  52.334 +
  52.335 +        """ Return the IExportPlugin registered for 'key'.
  52.336 +
  52.337 +        o Return 'default' if no such step is registered.
  52.338 +        """
  52.339 +        marker = object()
  52.340 +        info = self._registered.get( key, marker )
  52.341 +
  52.342 +        if info is marker:
  52.343 +            return default
  52.344 +
  52.345 +        return _resolveDottedName( info[ 'handler' ] )
  52.346 +
  52.347 +    security.declarePrivate( 'registerStep' )
  52.348 +    def registerStep( self, id, handler, title=None, description=None ):
  52.349 +
  52.350 +        """ Register an export step.
  52.351 +
  52.352 +        o 'id' is the unique identifier for this step
  52.353 +
  52.354 +        o 'step' should implement IExportPlugin.
  52.355 +
  52.356 +        o 'title' is a one-line UI description for this step.
  52.357 +          If None, the first line of the documentation string of the step
  52.358 +          is used, or the id if no docstring can be found.
  52.359 +
  52.360 +        o 'description' is a one-line UI description for this step.
  52.361 +          If None, the remaining line of the documentation string of
  52.362 +          the step is used, or default to ''.
  52.363 +        """
  52.364 +        if title is None or description is None:
  52.365 +
  52.366 +            t, d = _extractDocstring( handler, id, '' )
  52.367 +
  52.368 +            title = title or t
  52.369 +            description = description or d
  52.370 +
  52.371 +        info = { 'id'           : id
  52.372 +               , 'handler'      : _getDottedName( handler )
  52.373 +               , 'title'        : title
  52.374 +               , 'description'  : description
  52.375 +               }
  52.376 +
  52.377 +        self._registered[ id ] = info
  52.378 +
  52.379 +    security.declarePrivate( 'parseXML' )
  52.380 +    def parseXML( self, text, encoding=None ):
  52.381 +
  52.382 +        """ Parse 'text'.
  52.383 +        """
  52.384 +        reader = getattr( text, 'read', None )
  52.385 +
  52.386 +        if reader is not None:
  52.387 +            text = reader()
  52.388 +
  52.389 +        parser = _ExportStepRegistryParser( encoding )
  52.390 +        parseString( text, parser )
  52.391 +
  52.392 +        return parser._parsed
  52.393 +
  52.394 +    security.declarePrivate( 'clear' )
  52.395 +    def clear( self ):
  52.396 +
  52.397 +        self._registered = {}
  52.398 +
  52.399 +    #
  52.400 +    #   Helper methods
  52.401 +    #
  52.402 +    security.declarePrivate( '_exportTemplate' )
  52.403 +    _exportTemplate = PageTemplateFile( 'esrExport.xml', _xmldir )
  52.404 +
  52.405 +InitializeClass( ExportStepRegistry )
  52.406 +
  52.407 +class ToolsetRegistry( Implicit ):
  52.408 +
  52.409 +    """ Track required / forbidden tools.
  52.410 +    """
  52.411 +    implements(IToolsetRegistry)
  52.412 +
  52.413 +    security = ClassSecurityInfo()
  52.414 +    security.setDefaultAccess( 'allow' )
  52.415 +
  52.416 +    def __init__( self ):
  52.417 +
  52.418 +        self.clear()
  52.419 +
  52.420 +    #
  52.421 +    #   Toolset API
  52.422 +    #
  52.423 +    security.declareProtected( ManagePortal, 'listForbiddenTools' )
  52.424 +    def listForbiddenTools( self ):
  52.425 +
  52.426 +        """ See IToolsetRegistry.
  52.427 +        """
  52.428 +        result = list( self._forbidden )
  52.429 +        result.sort()
  52.430 +        return result
  52.431 +
  52.432 +    security.declareProtected( ManagePortal, 'addForbiddenTool' )
  52.433 +    def addForbiddenTool( self, tool_id ):
  52.434 +
  52.435 +        """ See IToolsetRegistry.
  52.436 +        """
  52.437 +        if tool_id in self._forbidden:
  52.438 +            return
  52.439 +
  52.440 +        if self._required.get( tool_id ) is not None:
  52.441 +            raise ValueError, 'Tool %s is required!' % tool_id
  52.442 +
  52.443 +        self._forbidden.append( tool_id )
  52.444 +
  52.445 +    security.declareProtected( ManagePortal, 'listRequiredTools' )
  52.446 +    def listRequiredTools( self ):
  52.447 +
  52.448 +        """ See IToolsetRegistry.
  52.449 +        """
  52.450 +        result = list( self._required.keys() )
  52.451 +        result.sort()
  52.452 +        return result
  52.453 +
  52.454 +    security.declareProtected( ManagePortal, 'getRequiredToolInfo' )
  52.455 +    def getRequiredToolInfo( self, tool_id ):
  52.456 +
  52.457 +        """ See IToolsetRegistry.
  52.458 +        """
  52.459 +        return self._required[ tool_id ]
  52.460 +
  52.461 +    security.declareProtected( ManagePortal, 'listRequiredToolInfo' )
  52.462 +    def listRequiredToolInfo( self ):
  52.463 +
  52.464 +        """ See IToolsetRegistry.
  52.465 +        """
  52.466 +        return [ self.getRequiredToolInfo( x )
  52.467 +                        for x in self.listRequiredTools() ]
  52.468 +
  52.469 +    security.declareProtected( ManagePortal, 'addRequiredTool' )
  52.470 +    def addRequiredTool( self, tool_id, dotted_name ):
  52.471 +
  52.472 +        """ See IToolsetRegistry.
  52.473 +        """
  52.474 +        if tool_id in self._forbidden:
  52.475 +            raise ValueError, "Forbidden tool ID: %s" % tool_id
  52.476 +
  52.477 +        self._required[ tool_id ] = { 'id' : tool_id
  52.478 +                                    , 'class' : dotted_name
  52.479 +                                    }
  52.480 +
  52.481 +    security.declareProtected( ManagePortal, 'generateXML' )
  52.482 +    def generateXML( self ):
  52.483 +
  52.484 +        """ Pseudo API.
  52.485 +        """
  52.486 +        return self._toolsetConfig()
  52.487 +
  52.488 +    security.declareProtected( ManagePortal, 'parseXML' )
  52.489 +    def parseXML( self, text, encoding=None ):
  52.490 +
  52.491 +        """ Pseudo-API
  52.492 +        """
  52.493 +        reader = getattr( text, 'read', None )
  52.494 +
  52.495 +        if reader is not None:
  52.496 +            text = reader()
  52.497 +
  52.498 +        parser = _ToolsetParser( encoding )
  52.499 +        parseString( text, parser )
  52.500 +
  52.501 +        for tool_id in parser._forbidden:
  52.502 +            self.addForbiddenTool( tool_id )
  52.503 +
  52.504 +        for tool_id, dotted_name in parser._required.items():
  52.505 +            self.addRequiredTool( tool_id, dotted_name )
  52.506 +
  52.507 +    security.declarePrivate( 'clear' )
  52.508 +    def clear( self ):
  52.509 +
  52.510 +        self._forbidden = []
  52.511 +        self._required = {}
  52.512 +
  52.513 +    #
  52.514 +    #   Helper methods.
  52.515 +    #
  52.516 +    security.declarePrivate( '_toolsetConfig' )
  52.517 +    _toolsetConfig = PageTemplateFile( 'tscExport.xml'
  52.518 +                                     , _xmldir
  52.519 +                                     , __name__='toolsetConfig'
  52.520 +                                     )
  52.521 +
  52.522 +InitializeClass( ToolsetRegistry )
  52.523 +
  52.524 +class ProfileRegistry( Implicit ):
  52.525 +
  52.526 +    """ Track registered profiles.
  52.527 +    """
  52.528 +    implements(IProfileRegistry)
  52.529 +
  52.530 +    security = ClassSecurityInfo()
  52.531 +    security.setDefaultAccess( 'allow' )
  52.532 +
  52.533 +    def __init__( self ):
  52.534 +
  52.535 +        self.clear()
  52.536 +
  52.537 +    security.declareProtected( ManagePortal, '' )
  52.538 +    def getProfileInfo( self, profile_id, for_=None ):
  52.539 +
  52.540 +        """ See IProfileRegistry.
  52.541 +        """
  52.542 +        result = self._profile_info[ profile_id ]
  52.543 +        if for_ is not None:
  52.544 +            if not issubclass( for_, result['for'] ):
  52.545 +                raise KeyError, profile_id
  52.546 +        return result.copy()
  52.547 +
  52.548 +    security.declareProtected( ManagePortal, 'listProfiles' )
  52.549 +    def listProfiles( self, for_=None ):
  52.550 +
  52.551 +        """ See IProfileRegistry.
  52.552 +        """
  52.553 +        result = []
  52.554 +        for profile_id in self._profile_ids:
  52.555 +            info = self.getProfileInfo( profile_id )
  52.556 +            if for_ is None or issubclass( for_, info['for'] ):
  52.557 +                result.append( profile_id )
  52.558 +        return tuple( result )
  52.559 +
  52.560 +    security.declareProtected( ManagePortal, 'listProfileInfo' )
  52.561 +    def listProfileInfo( self, for_=None ):
  52.562 +
  52.563 +        """ See IProfileRegistry.
  52.564 +        """
  52.565 +        candidates = [ self.getProfileInfo( id )
  52.566 +                        for id in self.listProfiles() ]
  52.567 +        return [ x for x in candidates if for_ is None or x['for'] is None or
  52.568 +                 issubclass( for_, x['for'] ) ]
  52.569 +
  52.570 +    security.declareProtected( ManagePortal, 'registerProfile' )
  52.571 +    def registerProfile( self
  52.572 +                       , name
  52.573 +                       , title
  52.574 +                       , description
  52.575 +                       , path
  52.576 +                       , product=None
  52.577 +                       , profile_type=BASE
  52.578 +                       , for_=None
  52.579 +                       ):
  52.580 +        """ See IProfileRegistry.
  52.581 +        """
  52.582 +        profile_id = '%s:%s' % (product or 'other', name)
  52.583 +        if self._profile_info.get( profile_id ) is not None:
  52.584 +            raise KeyError, 'Duplicate profile ID: %s' % profile_id
  52.585 +
  52.586 +        self._profile_ids.append( profile_id )
  52.587 +
  52.588 +        info = { 'id' : profile_id
  52.589 +               , 'title' : title
  52.590 +               , 'description' : description
  52.591 +               , 'path' : path
  52.592 +               , 'product' : product
  52.593 +               , 'type': profile_type
  52.594 +               , 'for': for_
  52.595 +               }
  52.596 +
  52.597 +        self._profile_info[ profile_id ] = info
  52.598 +
  52.599 +    security.declarePrivate( 'clear' )
  52.600 +    def clear( self ):
  52.601 +
  52.602 +        self._profile_info = {}
  52.603 +        self._profile_ids = []
  52.604 +
  52.605 +InitializeClass( ProfileRegistry )
  52.606 +
  52.607 +_profile_registry = ProfileRegistry()
  52.608 +
  52.609 +class _ImportStepRegistryParser( HandlerBase ):
  52.610 +
  52.611 +    security = ClassSecurityInfo()
  52.612 +    security.declareObjectPrivate()
  52.613 +    security.setDefaultAccess( 'deny' )
  52.614 +
  52.615 +    def __init__( self, encoding ):
  52.616 +
  52.617 +        self._encoding = encoding
  52.618 +        self._started = False
  52.619 +        self._pending = None
  52.620 +        self._parsed = []
  52.621 +
  52.622 +    def startElement( self, name, attrs ):
  52.623 +
  52.624 +        if name == 'import-steps':
  52.625 +
  52.626 +            if self._started:
  52.627 +                raise ValueError, 'Duplicated setup-steps element: %s' % name
  52.628 +
  52.629 +            self._started = True
  52.630 +
  52.631 +        elif name == 'import-step':
  52.632 +
  52.633 +            if self._pending is not None:
  52.634 +                raise ValueError, 'Cannot nest setup-step elements'
  52.635 +
  52.636 +            self._pending = dict( [ ( k, self._extract( attrs, k ) )
  52.637 +                                    for k in attrs.keys() ] )
  52.638 +
  52.639 +            self._pending[ 'dependencies' ] = []
  52.640 +
  52.641 +        elif name == 'dependency':
  52.642 +
  52.643 +            if not self._pending:
  52.644 +                raise ValueError, 'Dependency outside of step'
  52.645 +
  52.646 +            depended = self._extract( attrs, 'step' )
  52.647 +            self._pending[ 'dependencies' ].append( depended )
  52.648 +
  52.649 +        else:
  52.650 +            raise ValueError, 'Unknown element %s' % name
  52.651 +
  52.652 +    def characters( self, content ):
  52.653 +
  52.654 +        if self._pending is not None:
  52.655 +            content = self._encode( content )
  52.656 +            self._pending.setdefault( 'description', [] ).append( content )
  52.657 +
  52.658 +    def endElement(self, name):
  52.659 +
  52.660 +        if name == 'import-steps':
  52.661 +            pass
  52.662 +
  52.663 +        elif name == 'import-step':
  52.664 +
  52.665 +            if self._pending is None:
  52.666 +                raise ValueError, 'No pending step!'
  52.667 +
  52.668 +            deps = tuple( self._pending[ 'dependencies' ] )
  52.669 +            self._pending[ 'dependencies' ] = deps
  52.670 +
  52.671 +            desc = ''.join( self._pending[ 'description' ] )
  52.672 +            self._pending[ 'description' ] = desc
  52.673 +
  52.674 +            self._parsed.append( self._pending )
  52.675 +            self._pending = None
  52.676 +
  52.677 +InitializeClass( _ImportStepRegistryParser )
  52.678 +
  52.679 +class _ExportStepRegistryParser( HandlerBase ):
  52.680 +
  52.681 +    security = ClassSecurityInfo()
  52.682 +    security.declareObjectPrivate()
  52.683 +    security.setDefaultAccess( 'deny' )
  52.684 +
  52.685 +    def __init__( self, encoding ):
  52.686 +
  52.687 +        self._encoding = encoding
  52.688 +        self._started = False
  52.689 +        self._pending = None
  52.690 +        self._parsed = []
  52.691 +
  52.692 +    def startElement( self, name, attrs ):
  52.693 +
  52.694 +        if name == 'export-steps':
  52.695 +
  52.696 +            if self._started:
  52.697 +                raise ValueError, 'Duplicated export-steps element: %s' % name
  52.698 +
  52.699 +            self._started = True
  52.700 +
  52.701 +        elif name == 'export-step':
  52.702 +
  52.703 +            if self._pending is not None:
  52.704 +                raise ValueError, 'Cannot nest export-step elements'
  52.705 +
  52.706 +            self._pending = dict( [ ( k, self._extract( attrs, k ) )
  52.707 +                                    for k in attrs.keys() ] )
  52.708 +
  52.709 +        else:
  52.710 +            raise ValueError, 'Unknown element %s' % name
  52.711 +
  52.712 +    def characters( self, content ):
  52.713 +
  52.714 +        if self._pending is not None:
  52.715 +            content = self._encode( content )
  52.716 +            self._pending.setdefault( 'description', [] ).append( content )
  52.717 +
  52.718 +    def endElement(self, name):
  52.719 +
  52.720 +        if name == 'export-steps':
  52.721 +            pass
  52.722 +
  52.723 +        elif name == 'export-step':
  52.724 +
  52.725 +            if self._pending is None:
  52.726 +                raise ValueError, 'No pending step!'
  52.727 +
  52.728 +            desc = ''.join( self._pending[ 'description' ] )
  52.729 +            self._pending[ 'description' ] = desc
  52.730 +
  52.731 +            self._parsed.append( self._pending )
  52.732 +            self._pending = None
  52.733 +
  52.734 +InitializeClass( _ExportStepRegistryParser )
  52.735 +
  52.736 +
  52.737 +class _ToolsetParser( HandlerBase ):
  52.738 +
  52.739 +    security = ClassSecurityInfo()
  52.740 +    security.declareObjectPrivate()
  52.741 +    security.setDefaultAccess( 'deny' )
  52.742 +
  52.743 +    def __init__( self, encoding ):
  52.744 +
  52.745 +        self._encoding = encoding
  52.746 +        self._required = {}
  52.747 +        self._forbidden = []
  52.748 +
  52.749 +    def startElement( self, name, attrs ):
  52.750 +
  52.751 +        if name == 'tool-setup':
  52.752 +            pass
  52.753 +
  52.754 +        elif name == 'forbidden':
  52.755 +
  52.756 +            tool_id = self._extract( attrs, 'tool_id' )
  52.757 +
  52.758 +            if tool_id not in self._forbidden:
  52.759 +                self._forbidden.append( tool_id )
  52.760 +
  52.761 +        elif name == 'required':
  52.762 +
  52.763 +            tool_id = self._extract( attrs, 'tool_id' )
  52.764 +            dotted_name = self._extract( attrs, 'class' )
  52.765 +            self._required[ tool_id ] = dotted_name
  52.766 +
  52.767 +        else:
  52.768 +            raise ValueError, 'Unknown element %s' % name
  52.769 +
  52.770 +
  52.771 +InitializeClass( _ToolsetParser )
    53.1 new file mode 100644
    53.2 --- /dev/null
    53.3 +++ b/rolemap.py
    53.4 @@ -0,0 +1,216 @@
    53.5 +##############################################################################
    53.6 +#
    53.7 +# Copyright (c) 2004 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 +""" GenericSetup:  Role-permission export / import
   53.18 +
   53.19 +$Id: rolemap.py 41625 2006-02-15 11:39:43Z yuppie $
   53.20 +"""
   53.21 +
   53.22 +from AccessControl import ClassSecurityInfo
   53.23 +from AccessControl.Permission import Permission
   53.24 +from Globals import InitializeClass
   53.25 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   53.26 +
   53.27 +from permissions import ManagePortal
   53.28 +from utils import _xmldir
   53.29 +from utils import ConfiguratorBase
   53.30 +from utils import CONVERTER, DEFAULT, KEY
   53.31 +
   53.32 +
   53.33 +#
   53.34 +#   Configurator entry points
   53.35 +#
   53.36 +_FILENAME = 'rolemap.xml'
   53.37 +
   53.38 +def importRolemap( context ):
   53.39 +
   53.40 +    """ Import roles / permission map from an XML file.
   53.41 +
   53.42 +    o 'context' must implement IImportContext.
   53.43 +
   53.44 +    o Register via Python:
   53.45 +
   53.46 +      registry = site.setup_tool.setup_steps
   53.47 +      registry.registerStep( 'importRolemap'
   53.48 +                           , '20040518-01'
   53.49 +                           , Products.GenericSetup.rolemap.importRolemap
   53.50 +                           , ()
   53.51 +                           , 'Role / Permission import'
   53.52 +                           , 'Import additional roles, and map '
   53.53 +                           'roles to permissions'
   53.54 +                           )
   53.55 +
   53.56 +    o Register via XML:
   53.57 +
   53.58 +      <setup-step id="importRolemap"
   53.59 +                  version="20040518-01"
   53.60 +                  handler="Products.GenericSetup.rolemap.importRolemap"
   53.61 +                  title="Role / Permission import"
   53.62 +      >Import additional roles, and map roles to permissions.</setup-step>
   53.63 +
   53.64 +    """
   53.65 +    site = context.getSite()
   53.66 +    encoding = context.getEncoding()
   53.67 +    logger = context.getLogger('rolemap')
   53.68 +
   53.69 +    if context.shouldPurge():
   53.70 +
   53.71 +        items = site.__dict__.items()
   53.72 +
   53.73 +        for k, v in items: # XXX: WAAA
   53.74 +
   53.75 +            if k == '__ac_roles__':
   53.76 +                delattr( site, k )
   53.77 +
   53.78 +            if k.startswith( '_' ) and k.endswith( '_Permission' ):
   53.79 +                delattr( site, k )
   53.80 +
   53.81 +    text = context.readDataFile( _FILENAME )
   53.82 +
   53.83 +    if text is not None:
   53.84 +
   53.85 +        rc = RolemapConfigurator( site, encoding )
   53.86 +        rolemap_info = rc.parseXML( text )
   53.87 +
   53.88 +        immediate_roles = list( getattr(site, '__ac_roles__', []) )
   53.89 +        already = {}
   53.90 +
   53.91 +        for role in site.valid_roles():
   53.92 +            already[ role ] = 1
   53.93 +
   53.94 +        for role in rolemap_info[ 'roles' ]:
   53.95 +
   53.96 +            if already.get( role ) is None:
   53.97 +                immediate_roles.append( role )
   53.98 +                already[ role ] = 1
   53.99 +
  53.100 +        immediate_roles.sort()
  53.101 +        site.__ac_roles__ = tuple( immediate_roles )
  53.102 +
  53.103 +        for permission in rolemap_info[ 'permissions' ]:
  53.104 +
  53.105 +            site.manage_permission( permission[ 'name' ]
  53.106 +                                  , permission[ 'roles' ]
  53.107 +                                  , permission[ 'acquire' ]
  53.108 +                                  )
  53.109 +
  53.110 +    logger.info('Role / permission map imported.')
  53.111 +
  53.112 +
  53.113 +def exportRolemap( context ):
  53.114 +
  53.115 +    """ Export roles / permission map as an XML file
  53.116 +
  53.117 +    o 'context' must implement IExportContext.
  53.118 +
  53.119 +    o Register via Python:
  53.120 +
  53.121 +      registry = site.setup_tool.export_steps
  53.122 +      registry.registerStep( 'exportRolemap'
  53.123 +                           , Products.GenericSetup.rolemap.exportRolemap
  53.124 +                           , 'Role / Permission export'
  53.125 +                           , 'Export additional roles, and '
  53.126 +                             'role / permission map '
  53.127 +                           )
  53.128 +
  53.129 +    o Register via XML:
  53.130 +
  53.131 +      <export-script id="exportRolemap"
  53.132 +                     version="20040518-01"
  53.133 +                     handler="Products.GenericSetup.rolemap.exportRolemap"
  53.134 +                     title="Role / Permission export"
  53.135 +      >Export additional roles, and role / permission map.</export-script>
  53.136 +
  53.137 +    """
  53.138 +    site = context.getSite()
  53.139 +    logger = context.getLogger('rolemap')
  53.140 +
  53.141 +    rc = RolemapConfigurator( site ).__of__( site )
  53.142 +    text = rc.generateXML()
  53.143 +
  53.144 +    context.writeDataFile( _FILENAME, text, 'text/xml' )
  53.145 +
  53.146 +    logger.info('Role / permission map exported.')
  53.147 +
  53.148 +
  53.149 +class RolemapConfigurator(ConfiguratorBase):
  53.150 +    """ Synthesize XML description of sitewide role-permission settings.
  53.151 +    """
  53.152 +    security = ClassSecurityInfo()
  53.153 +
  53.154 +    security.declareProtected( ManagePortal, 'listRoles' )
  53.155 +    def listRoles( self ):
  53.156 +
  53.157 +        """ List the valid role IDs for our site.
  53.158 +        """
  53.159 +        return self._site.valid_roles()
  53.160 +
  53.161 +    security.declareProtected( ManagePortal, 'listPermissions' )
  53.162 +    def listPermissions( self ):
  53.163 +
  53.164 +        """ List permissions for export.
  53.165 +
  53.166 +        o Returns a sqeuence of mappings describing locally-modified
  53.167 +          permission / role settings.  Keys include:
  53.168 +
  53.169 +          'permission' -- the name of the permission
  53.170 +
  53.171 +          'acquire' -- a flag indicating whether to acquire roles from the
  53.172 +              site's container
  53.173 +
  53.174 +          'roles' -- the list of roles which have the permission.
  53.175 +
  53.176 +        o Do not include permissions which both acquire and which define
  53.177 +          no local changes to the acquired policy.
  53.178 +        """
  53.179 +        permissions = []
  53.180 +        valid_roles = self.listRoles()
  53.181 +
  53.182 +        for perm in self._site.ac_inherited_permissions( 1 ):
  53.183 +
  53.184 +            name = perm[ 0 ]
  53.185 +            p = Permission( name, perm[ 1 ], self._site )
  53.186 +            roles = p.getRoles( default=[] )
  53.187 +            acquire = isinstance( roles, list )  # tuple means don't acquire
  53.188 +            roles = [ r for r in roles if r in valid_roles ]
  53.189 +            roles.sort()
  53.190 +
  53.191 +            if roles or not acquire:
  53.192 +                permissions.append( { 'name'    : name
  53.193 +                                    , 'acquire' : acquire
  53.194 +                                    , 'roles'   : roles
  53.195 +                                    } )
  53.196 +
  53.197 +        return permissions
  53.198 +
  53.199 +    def _getExportTemplate(self):
  53.200 +
  53.201 +        return PageTemplateFile('rmeExport.xml', _xmldir)
  53.202 +
  53.203 +    def _getImportMapping(self):
  53.204 +
  53.205 +        return {
  53.206 +          'rolemap':
  53.207 +            { 'roles':       {CONVERTER: self._convertToUnique, DEFAULT: ()},
  53.208 +              'permissions': {CONVERTER: self._convertToUnique} },
  53.209 +          'roles':
  53.210 +            { 'role':        {KEY: None} },
  53.211 +          'role':
  53.212 +            { 'name':        {KEY: None} },
  53.213 +          'permissions':
  53.214 +            { 'permission':  {KEY: None, DEFAULT: ()} },
  53.215 +          'permission':
  53.216 +            { 'name':        {},
  53.217 +              'role':        {KEY: 'roles'},
  53.218 +              'acquire':     {CONVERTER: self._convertToBoolean} } }
  53.219 +
  53.220 +InitializeClass(RolemapConfigurator)
    54.1 new file mode 100644
    54.2 --- /dev/null
    54.3 +++ b/testing.py
    54.4 @@ -0,0 +1,144 @@
    54.5 +##############################################################################
    54.6 +#
    54.7 +# Copyright (c) 2005 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 +"""Node adapter testing utils.
   54.18 +
   54.19 +$Id: testing.py 41445 2006-01-25 18:32:47Z yuppie $
   54.20 +"""
   54.21 +
   54.22 +import unittest
   54.23 +import Testing
   54.24 +
   54.25 +from xml.dom.minidom import parseString
   54.26 +
   54.27 +import Products.Five
   54.28 +from Products.Five import zcml
   54.29 +from zope.app import zapi
   54.30 +from zope.interface import implements
   54.31 +from zope.interface.verify import verifyClass
   54.32 +
   54.33 +from interfaces import IBody
   54.34 +from interfaces import INode
   54.35 +from interfaces import ISetupEnviron
   54.36 +
   54.37 +try:
   54.38 +    from zope.app.testing.placelesssetup import PlacelessSetup
   54.39 +except ImportError:  # BBB, Zope3 < 3.1
   54.40 +    from zope.app.tests.placelesssetup import PlacelessSetup
   54.41 +
   54.42 +
   54.43 +class DummyLogger:
   54.44 +
   54.45 +    def __init__(self, id, messages):
   54.46 +        self._id = id
   54.47 +        self._messages = messages
   54.48 +
   54.49 +    def info(self, msg, *args, **kwargs):
   54.50 +        self._messages.append((20, self._id, msg))
   54.51 +
   54.52 +    def warning(self, msg, *args, **kwargs):
   54.53 +        self._messages.append((30, self._id, msg))
   54.54 +
   54.55 +
   54.56 +class DummySetupEnviron(object):
   54.57 +
   54.58 +    """Context for body im- and exporter.
   54.59 +    """
   54.60 +
   54.61 +    implements(ISetupEnviron)
   54.62 +
   54.63 +    def __init__(self):
   54.64 +        self._notes = []
   54.65 +        self._should_purge = True
   54.66 +
   54.67 +    def getLogger(self, name):
   54.68 +        return DummyLogger(name, self._notes)
   54.69 +
   54.70 +    def shouldPurge(self):
   54.71 +        return self._should_purge
   54.72 +
   54.73 +
   54.74 +class _AdapterTestCaseBase(PlacelessSetup, unittest.TestCase):
   54.75 +
   54.76 +    def _populate(self, obj):
   54.77 +        pass
   54.78 +
   54.79 +    def _verifyImport(self, obj):
   54.80 +        pass
   54.81 +
   54.82 +    def setUp(self):
   54.83 +        PlacelessSetup.setUp(self)
   54.84 +        zcml.load_config('meta.zcml', Products.Five)
   54.85 +        zcml.load_config('permissions.zcml', Products.Five)
   54.86 +
   54.87 +
   54.88 +class BodyAdapterTestCase(_AdapterTestCaseBase):
   54.89 +
   54.90 +    def test_z3interfaces(self):
   54.91 +        verifyClass(IBody, self._getTargetClass())
   54.92 +
   54.93 +    def test_body_get(self):
   54.94 +        self._populate(self._obj)
   54.95 +        context = DummySetupEnviron()
   54.96 +        adapted = zapi.getMultiAdapter((self._obj, context), IBody)
   54.97 +        self.assertEqual(adapted.body, self._BODY)
   54.98 +
   54.99 +    def test_body_set(self):
  54.100 +        context = DummySetupEnviron()
  54.101 +        adapted = zapi.getMultiAdapter((self._obj, context), IBody)
  54.102 +        adapted.body = self._BODY
  54.103 +        self._verifyImport(self._obj)
  54.104 +        self.assertEqual(adapted.body, self._BODY)
  54.105 +
  54.106 +        # now in update mode
  54.107 +        context._should_purge = False
  54.108 +        adapted = zapi.getMultiAdapter((self._obj, context), IBody)
  54.109 +        adapted.body = self._BODY
  54.110 +        self._verifyImport(self._obj)
  54.111 +        self.assertEqual(adapted.body, self._BODY)
  54.112 +
  54.113 +        # and again in update mode
  54.114 +        adapted = zapi.getMultiAdapter((self._obj, context), IBody)
  54.115 +        adapted.body = self._BODY
  54.116 +        self._verifyImport(self._obj)
  54.117 +        self.assertEqual(adapted.body, self._BODY)
  54.118 +
  54.119 +class NodeAdapterTestCase(_AdapterTestCaseBase):
  54.120 +
  54.121 +    def test_z3interfaces(self):
  54.122 +        verifyClass(INode, self._getTargetClass())
  54.123 +
  54.124 +    def test_node_get(self):
  54.125 +        self._populate(self._obj)
  54.126 +        context = DummySetupEnviron()
  54.127 +        adapted = zapi.getMultiAdapter((self._obj, context), INode)
  54.128 +        self.assertEqual(adapted.node.toprettyxml(' '), self._XML)
  54.129 +
  54.130 +    def test_node_set(self):
  54.131 +        context = DummySetupEnviron()
  54.132 +        adapted = zapi.getMultiAdapter((self._obj, context), INode)
  54.133 +        adapted.node = parseString(self._XML).documentElement
  54.134 +        self._verifyImport(self._obj)
  54.135 +        self.assertEqual(adapted.node.toprettyxml(' '), self._XML)
  54.136 +
  54.137 +        # now in update mode
  54.138 +        context._should_purge = False
  54.139 +        adapted = zapi.getMultiAdapter((self._obj, context), INode)
  54.140 +        adapted.node = parseString(self._XML).documentElement
  54.141 +        self._verifyImport(self._obj)
  54.142 +        self.assertEqual(adapted.node.toprettyxml(' '), self._XML)
  54.143 +
  54.144 +        # and again in update mode
  54.145 +        adapted = zapi.getMultiAdapter((self._obj, context), INode)
  54.146 +        adapted.node = parseString(self._XML).documentElement
  54.147 +        self._verifyImport(self._obj)
  54.148 +        self.assertEqual(adapted.node.toprettyxml(' '), self._XML)
    55.1 new file mode 100644
    55.2 --- /dev/null
    55.3 +++ b/tests/__init__.py
    55.4 @@ -0,0 +1,16 @@
    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 tests.
   55.18 +
   55.19 +$Id: __init__.py 38575 2005-09-24 09:01:32Z yuppie $
   55.20 +"""
    56.1 new file mode 100644
    56.2 --- /dev/null
    56.3 +++ b/tests/common.py
    56.4 @@ -0,0 +1,246 @@
    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 +""" GenericSetup product:  unit test utilities.
   56.18 +
   56.19 +$Id: common.py 40715 2005-12-12 10:33:40Z yuppie $
   56.20 +"""
   56.21 +
   56.22 +import os
   56.23 +import shutil
   56.24 +from tarfile import TarFile
   56.25 +
   56.26 +from Acquisition import Implicit
   56.27 +from Testing.ZopeTestCase import ZopeTestCase
   56.28 +from zope.interface import implements
   56.29 +
   56.30 +from Products.GenericSetup.interfaces import IExportContext
   56.31 +from Products.GenericSetup.interfaces import IImportContext
   56.32 +from Products.GenericSetup.testing import DummyLogger
   56.33 +
   56.34 +
   56.35 +class OmnipotentUser(Implicit):
   56.36 +    """ Omnipotent User for unit testing purposes.
   56.37 +    """
   56.38 +    def getId(self):
   56.39 +        return 'all_powerful_Oz'
   56.40 +
   56.41 +    getUserName = getId
   56.42 +
   56.43 +    def getRoles(self):
   56.44 +        return ('Manager',)
   56.45 +
   56.46 +    def allowed(self, object, object_roles=None):
   56.47 +        return 1
   56.48 +
   56.49 +    def getRolesInContext(self, object):
   56.50 +        return ('Manager',)
   56.51 +
   56.52 +class SecurityRequestTest(ZopeTestCase):
   56.53 +    def setUp(self):
   56.54 +        from AccessControl.SecurityManagement import newSecurityManager
   56.55 +        ZopeTestCase.setUp(self)
   56.56 +        self.root = self.app
   56.57 +        newSecurityManager(None, OmnipotentUser().__of__(self.app.acl_users))
   56.58 +
   56.59 +    def tearDown(self):
   56.60 +        from AccessControl.SecurityManagement import noSecurityManager
   56.61 +        noSecurityManager()
   56.62 +        ZopeTestCase.tearDown(self)
   56.63 +
   56.64 +class DOMComparator:
   56.65 +
   56.66 +    def _compareDOM( self, found_text, expected_text, debug=False ):
   56.67 +
   56.68 +        found_lines = [ x.strip() for x in found_text.splitlines() ]
   56.69 +        found_text = '\n'.join( filter( None, found_lines ) )
   56.70 +
   56.71 +        expected_lines = [ x.strip() for x in expected_text.splitlines() ]
   56.72 +        expected_text = '\n'.join( filter( None, expected_lines ) )
   56.73 +
   56.74 +        from xml.dom.minidom import parseString
   56.75 +        found = parseString( found_text )
   56.76 +        expected = parseString( expected_text )
   56.77 +        fxml = found.toxml()
   56.78 +        exml = expected.toxml()
   56.79 +
   56.80 +        if fxml != exml:
   56.81 +
   56.82 +            if debug:
   56.83 +                zipped = zip( fxml, exml )
   56.84 +                diff = [ ( i, zipped[i][0], zipped[i][1] )
   56.85 +                        for i in range( len( zipped ) )
   56.86 +                        if zipped[i][0] != zipped[i][1]
   56.87 +                    ]
   56.88 +                import pdb; pdb.set_trace()
   56.89 +
   56.90 +            print 'Found:'
   56.91 +            print fxml
   56.92 +            print
   56.93 +            print 'Expected:'
   56.94 +            print exml
   56.95 +            print
   56.96 +
   56.97 +        self.assertEqual( found.toxml(), expected.toxml() )
   56.98 +
   56.99 +class BaseRegistryTests( SecurityRequestTest, DOMComparator ):
  56.100 +
  56.101 +    def _makeOne( self, *args, **kw ):
  56.102 +
  56.103 +        # Derived classes must implement _getTargetClass
  56.104 +        return self._getTargetClass()( *args, **kw )
  56.105 +
  56.106 +def _clearTestDirectory( root_path ):
  56.107 +
  56.108 +    if os.path.exists( root_path ):
  56.109 +        shutil.rmtree( root_path )
  56.110 +
  56.111 +def _makeTestFile( filename, root_path, contents ):
  56.112 +
  56.113 +    path, filename = os.path.split( filename )
  56.114 +
  56.115 +    subdir = os.path.join( root_path, path )
  56.116 +
  56.117 +    if not os.path.exists( subdir ):
  56.118 +        os.makedirs( subdir )
  56.119 +
  56.120 +    fqpath = os.path.join( subdir, filename )
  56.121 +
  56.122 +    file = open( fqpath, 'wb' )
  56.123 +    file.write( contents )
  56.124 +    file.close()
  56.125 +    return fqpath
  56.126 +
  56.127 +class FilesystemTestBase( SecurityRequestTest ):
  56.128 +
  56.129 +    def _makeOne( self, *args, **kw ):
  56.130 +
  56.131 +        return self._getTargetClass()( *args, **kw )
  56.132 +
  56.133 +    def setUp( self ):
  56.134 +
  56.135 +        SecurityRequestTest.setUp( self )
  56.136 +        self._clearTempDir()
  56.137 +
  56.138 +    def tearDown( self ):
  56.139 +
  56.140 +        self._clearTempDir()
  56.141 +        SecurityRequestTest.tearDown( self )
  56.142 +
  56.143 +    def _clearTempDir( self ):
  56.144 +
  56.145 +        _clearTestDirectory( self._PROFILE_PATH )
  56.146 +
  56.147 +    def _makeFile( self, filename, contents ):
  56.148 +
  56.149 +        return _makeTestFile( filename, self._PROFILE_PATH, contents )
  56.150 +
  56.151 +
  56.152 +class TarballTester( DOMComparator ):
  56.153 +
  56.154 +    def _verifyTarballContents( self, fileish, toc_list, when=None ):
  56.155 +
  56.156 +        fileish.seek( 0L )
  56.157 +        tarfile = TarFile.open( 'foo.tar.gz', fileobj=fileish, mode='r:gz' )
  56.158 +        items = tarfile.getnames()
  56.159 +        items.sort()
  56.160 +        toc_list.sort()
  56.161 +
  56.162 +        self.assertEqual( len( items ), len( toc_list ) )
  56.163 +        for i in range( len( items ) ):
  56.164 +            self.assertEqual( items[ i ], toc_list[ i ] )
  56.165 +
  56.166 +        if when is not None:
  56.167 +            for tarinfo in tarfile:
  56.168 +                self.failIf( tarinfo.mtime < when )
  56.169 +
  56.170 +    def _verifyTarballEntry( self, fileish, entry_name, data ):
  56.171 +
  56.172 +        fileish.seek( 0L )
  56.173 +        tarfile = TarFile.open( 'foo.tar.gz', fileobj=fileish, mode='r:gz' )
  56.174 +        extract = tarfile.extractfile( entry_name )
  56.175 +        found = extract.read()
  56.176 +        self.assertEqual( found, data )
  56.177 +
  56.178 +    def _verifyTarballEntryXML( self, fileish, entry_name, data ):
  56.179 +
  56.180 +        fileish.seek( 0L )
  56.181 +        tarfile = TarFile.open( 'foo.tar.gz', fileobj=fileish, mode='r:gz' )
  56.182 +        extract = tarfile.extractfile( entry_name )
  56.183 +        found = extract.read()
  56.184 +        self._compareDOM( found, data )
  56.185 +
  56.186 +
  56.187 +class DummyExportContext:
  56.188 +
  56.189 +    implements(IExportContext)
  56.190 +
  56.191 +    def __init__( self, site, tool=None ):
  56.192 +        self._site = site
  56.193 +        self._tool = tool
  56.194 +        self._wrote = []
  56.195 +        self._notes = []
  56.196 +
  56.197 +    def getSite( self ):
  56.198 +        return self._site
  56.199 +
  56.200 +    def getSetupTool( self ):
  56.201 +        return self._tool
  56.202 +
  56.203 +    def getLogger(self, name):
  56.204 +        return DummyLogger(name, self._notes)
  56.205 +
  56.206 +    def writeDataFile( self, filename, text, content_type, subdir=None ):
  56.207 +        if subdir is not None:
  56.208 +            filename = '%s/%s' % ( subdir, filename )
  56.209 +        self._wrote.append( ( filename, text, content_type ) )
  56.210 +
  56.211 +
  56.212 +class DummyImportContext:
  56.213 +
  56.214 +    implements(IImportContext)
  56.215 +
  56.216 +    def __init__( self, site, purge=True, encoding=None, tool=None ):
  56.217 +        self._site = site
  56.218 +        self._tool = tool
  56.219 +        self._purge = purge
  56.220 +        self._encoding = encoding
  56.221 +        self._files = {}
  56.222 +        self._notes = []
  56.223 +
  56.224 +    def getSite( self ):
  56.225 +        return self._site
  56.226 +
  56.227 +    def getSetupTool( self ):
  56.228 +        return self._tool
  56.229 +
  56.230 +    def getEncoding( self ):
  56.231 +        return self._encoding
  56.232 +
  56.233 +    def getLogger(self, name):
  56.234 +        return DummyLogger(name, self._notes)
  56.235 +
  56.236 +    def readDataFile( self, filename, subdir=None ):
  56.237 +
  56.238 +        if subdir is not None:
  56.239 +            filename = '/'.join( (subdir, filename) )
  56.240 +
  56.241 +        return self._files.get( filename )
  56.242 +
  56.243 +    def shouldPurge( self ):
  56.244 +
  56.245 +        return self._purge
  56.246 +
  56.247 +
  56.248 +def dummy_handler( context ):
  56.249 +
  56.250 +    pass
    57.1 new file mode 100644
    57.2 --- /dev/null
    57.3 +++ b/tests/conformance.py
    57.4 @@ -0,0 +1,163 @@
    57.5 +##############################################################################
    57.6 +#
    57.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    57.8 +#
    57.9 +# This software is subject to the provisions of the Zope Public License,
   57.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   57.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   57.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   57.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   57.14 +# FOR A PARTICULAR PURPOSE.
   57.15 +#
   57.16 +##############################################################################
   57.17 +""" Base classes for testing interface conformance.
   57.18 +
   57.19 +Derived testcase classes should define '_getTargetClass()', which must
   57.20 +return the class being tested for conformance.
   57.21 +
   57.22 +$Id: conformance.py 40140 2005-11-15 18:53:19Z tseaver $
   57.23 +"""
   57.24 +
   57.25 +class ConformsToISetupContext:
   57.26 +
   57.27 +    def test_ISetupContext_conformance( self ):
   57.28 +
   57.29 +        from Products.GenericSetup.interfaces import ISetupContext
   57.30 +        from zope.interface.verify import verifyClass
   57.31 +
   57.32 +        verifyClass( ISetupContext, self._getTargetClass() )
   57.33 +
   57.34 +class ConformsToIImportContext:
   57.35 +
   57.36 +    def test_IImportContext_conformance( self ):
   57.37 +
   57.38 +        from Products.GenericSetup.interfaces import IImportContext
   57.39 +        from zope.interface.verify import verifyClass
   57.40 +
   57.41 +        verifyClass( IImportContext, self._getTargetClass() )
   57.42 +
   57.43 +class ConformsToIExportContext:
   57.44 +
   57.45 +    def test_IExportContext_conformance( self ):
   57.46 +
   57.47 +        from Products.GenericSetup.interfaces import IExportContext
   57.48 +        from zope.interface.verify import verifyClass
   57.49 +
   57.50 +        verifyClass( IExportContext, self._getTargetClass() )
   57.51 +
   57.52 +class ConformsToIStepRegistry:
   57.53 +
   57.54 +    def test_IStepRegistry_conformance( self ):
   57.55 +
   57.56 +        from Products.GenericSetup.interfaces import IStepRegistry
   57.57 +        from zope.interface.verify import verifyClass
   57.58 +
   57.59 +        verifyClass( IStepRegistry, self._getTargetClass() )
   57.60 +
   57.61 +class ConformsToIImportStepRegistry:
   57.62 +
   57.63 +    def test_IImportStepRegistry_conformance( self ):
   57.64 +
   57.65 +        from Products.GenericSetup.interfaces import IImportStepRegistry
   57.66 +        from zope.interface.verify import verifyClass
   57.67 +
   57.68 +        verifyClass( IImportStepRegistry, self._getTargetClass() )
   57.69 +
   57.70 +class ConformsToIExportStepRegistry:
   57.71 +
   57.72 +    def test_IExportStepRegistry_conformance( self ):
   57.73 +
   57.74 +        from Products.GenericSetup.interfaces import IExportStepRegistry
   57.75 +        from zope.interface.verify import verifyClass
   57.76 +
   57.77 +        verifyClass( IExportStepRegistry, self._getTargetClass() )
   57.78 +
   57.79 +class ConformsToIToolsetRegistry:
   57.80 +
   57.81 +    def test_IToolsetRegistry_conformance( self ):
   57.82 +
   57.83 +        from Products.GenericSetup.interfaces import IToolsetRegistry
   57.84 +        from zope.interface.verify import verifyClass
   57.85 +
   57.86 +        verifyClass( IToolsetRegistry, self._getTargetClass() )
   57.87 +
   57.88 +class ConformsToIProfileRegistry:
   57.89 +
   57.90 +    def test_IProfileRegistry_conformance( self ):
   57.91 +
   57.92 +        from Products.GenericSetup.interfaces import IProfileRegistry
   57.93 +        from zope.interface.verify import verifyClass
   57.94 +
   57.95 +        verifyClass( IProfileRegistry, self._getTargetClass() )
   57.96 +
   57.97 +class ConformsToISetupTool:
   57.98 +
   57.99 +    def test_ISetupTool_conformance( self ):
  57.100 +
  57.101 +        from Products.GenericSetup.interfaces import ISetupTool
  57.102 +        from zope.interface.verify import verifyClass
  57.103 +
  57.104 +        verifyClass( ISetupTool, self._getTargetClass() )
  57.105 +
  57.106 +class ConformsToIContentFactory:
  57.107 +
  57.108 +    def test_conforms_to_IContentFactory(self):
  57.109 +
  57.110 +        from Products.GenericSetup.interfaces import IContentFactory
  57.111 +        from zope.interface.verify import verifyClass
  57.112 +
  57.113 +        verifyClass( IContentFactory, self._getTargetClass() )
  57.114 +
  57.115 +class ConformsToIContentFactoryName:
  57.116 +
  57.117 +    def test_conforms_to_IContentFactory(self):
  57.118 +
  57.119 +        from Products.GenericSetup.interfaces import IContentFactoryName
  57.120 +        from zope.interface.verify import verifyClass
  57.121 +
  57.122 +        verifyClass( IContentFactoryName, self._getTargetClass() )
  57.123 +
  57.124 +class ConformsToIFilesystemExporter:
  57.125 +
  57.126 +    def test_conforms_to_IFilesystemExporter(self):
  57.127 +
  57.128 +        from Products.GenericSetup.interfaces import IFilesystemExporter
  57.129 +        from zope.interface.verify import verifyClass
  57.130 +
  57.131 +        verifyClass( IFilesystemExporter, self._getTargetClass() )
  57.132 +
  57.133 +class ConformsToIFilesystemImporter:
  57.134 +
  57.135 +    def test_conforms_to_IFilesystemImporter(self):
  57.136 +
  57.137 +        from Products.GenericSetup.interfaces import IFilesystemImporter
  57.138 +        from zope.interface.verify import verifyClass
  57.139 +
  57.140 +        verifyClass( IFilesystemImporter, self._getTargetClass() )
  57.141 +
  57.142 +class ConformsToIINIAware:
  57.143 +
  57.144 +    def test_conforms_to_IINIAware(self):
  57.145 +
  57.146 +        from Products.GenericSetup.interfaces import IINIAware
  57.147 +        from zope.interface.verify import verifyClass
  57.148 +
  57.149 +        verifyClass (IINIAware, self._getTargetClass() )
  57.150 +
  57.151 +class ConformsToICSVAware:
  57.152 +
  57.153 +    def test_conforms_to_ICSVAware(self):
  57.154 +
  57.155 +        from Products.GenericSetup.interfaces import ICSVAware
  57.156 +        from zope.interface.verify import verifyClass
  57.157 +
  57.158 +        verifyClass( ICSVAware, self._getTargetClass() )
  57.159 +
  57.160 +class ConformsToIDAVAware:
  57.161 +
  57.162 +    def test_conforms_to_IDAVAware(self):
  57.163 +
  57.164 +        from Products.GenericSetup.interfaces import IDAVAware
  57.165 +        from zope.interface.verify import verifyClass
  57.166 +
  57.167 +        verifyClass( IDAVAware, self._getTargetClass() )
    58.1 new file mode 100644
    58.2 --- /dev/null
    58.3 +++ b/tests/default_profile/export_steps.xml
    58.4 @@ -0,0 +1,8 @@
    58.5 +<?xml version="1.0"?>
    58.6 +<export-steps>
    58.7 + <export-step id="one"
    58.8 +                handler="Products.GenericSetup.tests.common.dummy_handler"
    58.9 +                title="One Step">
   58.10 +  One small step
   58.11 + </export-step>
   58.12 +</export-steps>
    59.1 new file mode 100644
    59.2 --- /dev/null
    59.3 +++ b/tests/default_profile/import_steps.xml
    59.4 @@ -0,0 +1,9 @@
    59.5 +<?xml version="1.0"?>
    59.6 +<import-steps>
    59.7 + <import-step id="one"
    59.8 +             version="1"
    59.9 +             handler="Products.GenericSetup.tests.common.dummy_handler"
   59.10 +             title="One Step">
   59.11 +  One small step
   59.12 + </import-step>
   59.13 +</import-steps>
    60.1 new file mode 100644
    60.2 --- /dev/null
    60.3 +++ b/tests/default_profile/profile.ini
    60.4 @@ -0,0 +1,2 @@
    60.5 +[Metadata]
    60.6 +Title=Unit Test Profile Data
    61.1 new file mode 100644
    61.2 --- /dev/null
    61.3 +++ b/tests/default_profile/toolset.xml
    61.4 @@ -0,0 +1,6 @@
    61.5 +<?xml version="1.0"?>
    61.6 +<tool-setup>
    61.7 + <forbidden tool_id="doomed" />
    61.8 + <required tool_id="mandatory" class="path.to.one" />
    61.9 + <required tool_id="obligatory" class="path.to.another" />
   61.10 +</tool-setup>
    62.1 new file mode 100644
    62.2 --- /dev/null
    62.3 +++ b/tests/faux_objects.py
    62.4 @@ -0,0 +1,82 @@
    62.5 +""" Simple, importable content classes.
    62.6 +
    62.7 +$Id: faux_objects.py 40140 2005-11-15 18:53:19Z tseaver $
    62.8 +"""
    62.9 +from OFS.Folder import Folder
   62.10 +from OFS.PropertyManager import PropertyManager
   62.11 +from OFS.SimpleItem import SimpleItem
   62.12 +from zope.interface import implements
   62.13 +
   62.14 +try:
   62.15 +    from OFS.interfaces import IObjectManager
   62.16 +    from OFS.interfaces import ISimpleItem
   62.17 +    from OFS.interfaces import IPropertyManager
   62.18 +except ImportError: # BBB
   62.19 +    from Products.Five.interfaces import IObjectManager
   62.20 +    from Products.Five.interfaces import ISimpleItem
   62.21 +    from Products.Five.interfaces import IPropertyManager
   62.22 +
   62.23 +class TestSimpleItem(SimpleItem):
   62.24 +    implements(ISimpleItem)
   62.25 +
   62.26 +class TestSimpleItemWithProperties(SimpleItem, PropertyManager):
   62.27 +    implements(ISimpleItem, IPropertyManager)
   62.28 +
   62.29 +KNOWN_CSV = """\
   62.30 +one,two,three
   62.31 +four,five,six
   62.32 +"""
   62.33 +
   62.34 +from Products.GenericSetup.interfaces import ICSVAware
   62.35 +class TestCSVAware(SimpleItem):
   62.36 +    implements(ICSVAware)
   62.37 +    _was_put = None
   62.38 +    _csv = KNOWN_CSV
   62.39 +
   62.40 +    def as_csv(self):
   62.41 +        return self._csv
   62.42 +
   62.43 +    def put_csv(self, text):
   62.44 +        self._was_put = text
   62.45 +
   62.46 +KNOWN_INI = """\
   62.47 +[DEFAULT]
   62.48 +title = %s
   62.49 +description = %s
   62.50 +"""
   62.51 +
   62.52 +from Products.GenericSetup.interfaces import IINIAware
   62.53 +class TestINIAware(SimpleItem):
   62.54 +    implements(IINIAware)
   62.55 +    _was_put = None
   62.56 +    title = 'INI title'
   62.57 +    description = 'INI description'
   62.58 +
   62.59 +    def as_ini(self):
   62.60 +        return KNOWN_INI % (self.title, self.description)
   62.61 +
   62.62 +    def put_ini(self, text):
   62.63 +        self._was_put = text
   62.64 +
   62.65 +KNOWN_DAV = """\
   62.66 +Title: %s
   62.67 +Description: %s
   62.68 +
   62.69 +%s
   62.70 +"""
   62.71 +
   62.72 +from Products.GenericSetup.interfaces import IDAVAware
   62.73 +class TestDAVAware(SimpleItem):
   62.74 +    implements(IDAVAware)
   62.75 +    _was_put = None
   62.76 +    title = 'DAV title'
   62.77 +    description = 'DAV description'
   62.78 +    body = 'DAV body'
   62.79 +
   62.80 +    def manage_FTPget(self):
   62.81 +        return KNOWN_DAV % (self.title, self.description, self.body)
   62.82 +
   62.83 +    def PUT(self, REQUEST, RESPONSE):
   62.84 +        self._was_put = REQUEST.get('BODY', '')
   62.85 +        stream = REQUEST.get('BODYFILE', None)
   62.86 +        self._was_put_as_read = stream.read()
    63.1 new file mode 100644
    63.2 index 0000000000000000000000000000000000000000..a28a1350ebef24c6f6a393faf5056c07d119cf44
    63.3 GIT binary patch
    63.4 literal 840
    63.5 zc$@)91GoH%P)<h;3K|Lk000e1NJLTq002Ay001or1^@s6@^%PJ00006VoOIv0RI60
    63.6 z0RN!9r;`8x010qNS#tmY3labT3lag+-G2N4000McNliru(h46BC=K~o-Npa_0@X=G
    63.7 zK~#90?O4%q+b|4$>U9jNb12zE-9zmhO7?K}kTZvzJ<tyU&=W}8O)r*nDc_yX6bXR@
    63.8 z2!eP5Dk>@}zBZ)O-vN*bkaX`AU5504jLSdJ6a+c|J%Bb0905E4I4%Fp<82rE5s)50
    63.9 z0-ER2U{X(X)Hmo(_WJ7pnax|D8<GH#O9EqVh#VH9|60i>g7nM!&kIPwU<o_kFXz_V
   63.10 zbC6GKzsseqGwnHB+#H;1(Ix9VnXWN6$_y3R>bq*Nbop+|L4P_C8_b4SB-r9a*5sB&
   63.11 z%|Yc;km<;%QO5_;J8063PO|r|9le-s&BL3%iHr&%BlA{=MTex|7?N{mL`P3yPL2)<
   63.12 zMkN~K_uhN-;!Z>*mdt$NJQ+m?J0=?MixzTZi~ydq;AC%)>w9rME54OF6&-+=?PNHH
   63.13 z@uPaU2k=H?J%A@_zfk*C^=Sw<KXm*T=~Z1ataJKxY0p8|W(|pSY$Er2*;W|<JOa(z
   63.14 zGEwuYO4#65jj}!pEwn)MlMFcNf*(}IcIgD*x3*2bT^8?QZn@!Eb+Kc3tX=wio}=19
   63.15 zo0c1zC^1=bGDEnn=f$5jggMMzCuLvL31H34;X0IwjsS(X-2g6EN`8g+Dk>_@L;C#8
   63.16 z0`N-b)^qO$FW;#2^8_|*E1sLpKVO&oxXGv%_Hs{++#@^gnuSX>s$WUx3VE)Mi~vlb
   63.17 z!qSq`uPd(NBC!Z%>`hT(A^!D)cfQwMr|`?_-B}H_uM%2UOTzk9B@&CE<K~bd_pYC1
   63.18 zCJ$z0q;p(IT&j}`#KQXnz1$sKK>_gMovSlo>y*sE5?Kkv0?2jV1sOFQk+Y}3uZ_!k
   63.19 z$VMH^jb*Fr-*JXKya%FesA0B59{`>4XrJ$vF&w}Ni0N>dKrF-<S{vru6Qu;=WMW!(
   63.20 z#L2sz=b6Sr0dtmEr2Hn>4Sv1c`#jwezH@f(er01TDk>@}Dk>@}Dk>`eBm4or%ud3<
   63.21 Sil1%(0000<MNUMnLSTZVwtlDp
   63.22 
    64.1 new file mode 100644
    64.2 --- /dev/null
    64.3 +++ b/tests/test_content.py
    64.4 @@ -0,0 +1,897 @@
    64.5 +##############################################################################
    64.6 +#
    64.7 +# Copyright (c) 2005 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 +"""Filesystem exporter / importer adapter unit tests.
   64.18 +
   64.19 +$Id: test_content.py 40234 2005-11-18 21:22:13Z tseaver $
   64.20 +"""
   64.21 +
   64.22 +import unittest
   64.23 +import Testing
   64.24 +import Zope2
   64.25 +Zope2.startup()
   64.26 +
   64.27 +from csv import reader
   64.28 +from ConfigParser import ConfigParser
   64.29 +from StringIO import StringIO
   64.30 +
   64.31 +try:
   64.32 +    from OFS.interfaces import IObjectManager
   64.33 +    from OFS.interfaces import ISimpleItem
   64.34 +    from OFS.interfaces import IPropertyManager
   64.35 +except ImportError: # BBB
   64.36 +    from Products.Five.interfaces import IObjectManager
   64.37 +    from Products.Five.interfaces import ISimpleItem
   64.38 +    from Products.Five.interfaces import IPropertyManager
   64.39 +
   64.40 +from Products.GenericSetup.testing import PlacelessSetup
   64.41 +from Products.GenericSetup.tests.common import DummyExportContext
   64.42 +from Products.GenericSetup.tests.common import DummyImportContext
   64.43 +
   64.44 +from conformance import ConformsToIINIAware
   64.45 +from conformance import ConformsToIFilesystemExporter
   64.46 +from conformance import ConformsToIFilesystemImporter
   64.47 +
   64.48 +class SimpleINIAwareTests(unittest.TestCase, ConformsToIINIAware):
   64.49 +
   64.50 +    def _getTargetClass(self):
   64.51 +        from Products.GenericSetup.content import SimpleINIAware
   64.52 +        return SimpleINIAware
   64.53 +
   64.54 +    def test_as_ini_no_properties(self):
   64.55 +        context = _makePropertied('no_properties')
   64.56 +        context._properties = ()
   64.57 +        adapter = self._getTargetClass()(context)
   64.58 +        text = adapter.as_ini()
   64.59 +        parser = ConfigParser()
   64.60 +        parser.readfp(StringIO(text))
   64.61 +        self.failIf(parser.sections())
   64.62 +        default_options = parser.defaults()
   64.63 +        self.assertEqual(len(default_options), 0)
   64.64 +
   64.65 +    def test_as_ini_string_property(self):
   64.66 +        TITLE = 'String Property'
   64.67 +        DESCR = 'Another property'
   64.68 +        context = _makePropertied('string_property')
   64.69 +        context.title = TITLE
   64.70 +        context._setProperty('description', DESCR)
   64.71 +        adapter = self._getTargetClass()(context)
   64.72 +        text = adapter.as_ini()
   64.73 +        parser = ConfigParser()
   64.74 +        parser.readfp(StringIO(text))
   64.75 +        self.failIf(parser.sections())
   64.76 +        default_options = parser.defaults()
   64.77 +        self.assertEqual(len(default_options), 2)
   64.78 +        self.assertEqual(default_options['title'].strip(), TITLE)
   64.79 +        self.assertEqual(default_options['description'].strip(), DESCR)
   64.80 +
   64.81 +    def test_as_ini_other_properties(self):
   64.82 +        from DateTime.DateTime import DateTime
   64.83 +        INTPROP = 42
   64.84 +        FLOATPROP = 3.1415926
   64.85 +        DATESTR = '2005-11-07T12:00:00.000Z'
   64.86 +        context = _makePropertied('string_property')
   64.87 +        context._properties = ()
   64.88 +        context._setProperty('int_prop', INTPROP, 'int')
   64.89 +        context._setProperty('float_prop', FLOATPROP, 'float')
   64.90 +        context._setProperty('date_prop', DateTime(DATESTR), 'date')
   64.91 +        adapter = self._getTargetClass()(context)
   64.92 +        text = adapter.as_ini()
   64.93 +        parser = ConfigParser()
   64.94 +        parser.readfp(StringIO(text))
   64.95 +        self.failIf(parser.sections())
   64.96 +        default_options = parser.defaults()
   64.97 +        self.assertEqual(len(default_options), 3)
   64.98 +        self.assertEqual(default_options['int_prop'], str(INTPROP))
   64.99 +        self.assertEqual(default_options['float_prop'], str(FLOATPROP))
  64.100 +        self.assertEqual(default_options['date_prop'], str(DateTime(DATESTR)))
  64.101 +
  64.102 +    def test_put_ini_empty(self):
  64.103 +        context = _makePropertied('empty_ini')
  64.104 +        adapter = self._getTargetClass()(context)
  64.105 +        context._properties = ()
  64.106 +        self.failIf(context.propertyItems())
  64.107 +        adapter.put_ini('')
  64.108 +        self.failIf(context.propertyItems())
  64.109 +
  64.110 +    def test_put_ini_with_values_stripped(self):
  64.111 +        context = _makePropertied('empty_ini')
  64.112 +        adapter = self._getTargetClass()(context)
  64.113 +        adapter.put_ini('[DEFAULT]\ntitle = Foo \ndescription = bar ')
  64.114 +        props = context.propdict()
  64.115 +        self.assertEqual(len(props), 2)
  64.116 +        self.failUnless('title' in props)
  64.117 +        self.failUnless('description' in props)
  64.118 +        self.assertEqual(context.title, 'Foo')
  64.119 +        self.assertEqual(context.description, 'bar')
  64.120 +
  64.121 +    def test_put_ini_other_properties(self):
  64.122 +        from DateTime.DateTime import DateTime
  64.123 +        INTPROP = 42
  64.124 +        FLOATPROP = 3.1415926
  64.125 +        DATESTR = '2005-11-07T12:00:00.000Z'
  64.126 +        DATESTR2 = '2005-11-09T12:00:00.000Z'
  64.127 +        context = _makePropertied('string_property')
  64.128 +        context._properties = ()
  64.129 +        context._setProperty('int_prop', INTPROP, 'int')
  64.130 +        context._setProperty('float_prop', FLOATPROP, 'float')
  64.131 +        context._setProperty('date_prop', DateTime(DATESTR), 'date')
  64.132 +        adapter = self._getTargetClass()(context)
  64.133 +        adapter.put_ini('''\
  64.134 +[DEFAULT]
  64.135 +int_prop = 13 
  64.136 +\nfloat_prop = 2.818
  64.137 +\ndate_prop = %s''' % DATESTR2)
  64.138 +        self.assertEqual(len(context.propertyIds()), 3)
  64.139 +        self.assertEqual(context.int_prop, 13)
  64.140 +        self.assertEqual(context.float_prop, 2.818)
  64.141 +        self.assertEqual(context.date_prop, DateTime(DATESTR2))
  64.142 +
  64.143 +class FolderishExporterImporterTests(PlacelessSetup,
  64.144 +                                     unittest.TestCase,
  64.145 +                                    ):
  64.146 +
  64.147 +    def _getExporter(self):
  64.148 +        from Products.GenericSetup.content import exportSiteStructure
  64.149 +        return exportSiteStructure
  64.150 +
  64.151 +    def _getImporter(self):
  64.152 +        from Products.GenericSetup.content import importSiteStructure
  64.153 +        return importSiteStructure
  64.154 +
  64.155 +    def _makeSetupTool(self):
  64.156 +        from Products.GenericSetup.tool import SetupTool
  64.157 +        return SetupTool('portal_setup')
  64.158 +
  64.159 +    def _setUpAdapters(self):
  64.160 +        from OFS.Folder import Folder
  64.161 +        from zope.app.tests import ztapi
  64.162 +        #from OFS.Image import File
  64.163 +
  64.164 +        from Products.GenericSetup.interfaces import IFilesystemExporter
  64.165 +        from Products.GenericSetup.interfaces import IFilesystemImporter
  64.166 +        from Products.GenericSetup.interfaces import ICSVAware
  64.167 +        from Products.GenericSetup.interfaces import IINIAware
  64.168 +        from Products.GenericSetup.interfaces import IDAVAware
  64.169 +
  64.170 +        from Products.GenericSetup.content import \
  64.171 +             SimpleINIAware
  64.172 +        from Products.GenericSetup.content import \
  64.173 +             FolderishExporterImporter
  64.174 +        from Products.GenericSetup.content import \
  64.175 +             CSVAwareFileAdapter
  64.176 +        from Products.GenericSetup.content import \
  64.177 +             INIAwareFileAdapter
  64.178 +        from Products.GenericSetup.content import \
  64.179 +             DAVAwareFileAdapter
  64.180 +
  64.181 +        ztapi.provideAdapter(IObjectManager,
  64.182 +                             IFilesystemExporter,
  64.183 +                             FolderishExporterImporter,
  64.184 +                            )
  64.185 +
  64.186 +        ztapi.provideAdapter(IObjectManager,
  64.187 +                             IFilesystemImporter,
  64.188 +                             FolderishExporterImporter,
  64.189 +                            )
  64.190 +
  64.191 +        ztapi.provideAdapter(IPropertyManager,
  64.192 +                             IINIAware,
  64.193 +                             SimpleINIAware,
  64.194 +                            )
  64.195 +
  64.196 +        ztapi.provideAdapter(ICSVAware,
  64.197 +                             IFilesystemExporter,
  64.198 +                             CSVAwareFileAdapter,
  64.199 +                            )
  64.200 +
  64.201 +        ztapi.provideAdapter(ICSVAware,
  64.202 +                             IFilesystemImporter,
  64.203 +                             CSVAwareFileAdapter,
  64.204 +                            )
  64.205 +
  64.206 +        ztapi.provideAdapter(IINIAware,
  64.207 +                             IFilesystemExporter,
  64.208 +                             INIAwareFileAdapter,
  64.209 +                            )
  64.210 +
  64.211 +        ztapi.provideAdapter(IINIAware,
  64.212 +                             IFilesystemImporter,
  64.213 +                             INIAwareFileAdapter,
  64.214 +                            )
  64.215 +
  64.216 +        ztapi.provideAdapter(IDAVAware,
  64.217 +                             IFilesystemExporter,
  64.218 +                             DAVAwareFileAdapter,
  64.219 +                            )
  64.220 +
  64.221 +        ztapi.provideAdapter(IDAVAware,
  64.222 +                             IFilesystemImporter,
  64.223 +                             DAVAwareFileAdapter,
  64.224 +                            )
  64.225 +
  64.226 +
  64.227 +    def test_export_empty_site(self):
  64.228 +        self._setUpAdapters()
  64.229 +        site = _makeFolder('site')
  64.230 +        site.title = 'test_export_empty_site'
  64.231 +        site.description = 'Testing export of an empty site.'
  64.232 +        context = DummyExportContext(site)
  64.233 +        exporter = self._getExporter()
  64.234 +        exporter(context)
  64.235 +
  64.236 +        self.assertEqual(len(context._wrote), 2)
  64.237 +        filename, text, content_type = context._wrote[0]
  64.238 +        self.assertEqual(filename, 'structure/.objects')
  64.239 +        self.assertEqual(content_type, 'text/comma-separated-values')
  64.240 +
  64.241 +        objects = [x for x in reader(StringIO(text))]
  64.242 +        self.assertEqual(len(objects), 0)
  64.243 +
  64.244 +        filename, text, content_type = context._wrote[1]
  64.245 +        self.assertEqual(filename, 'structure/.properties')
  64.246 +        self.assertEqual(content_type, 'text/plain')
  64.247 +
  64.248 +        parser = ConfigParser()
  64.249 +        parser.readfp(StringIO(text))
  64.250 +
  64.251 +        defaults = parser.defaults()
  64.252 +        self.assertEqual(len(defaults), 1)
  64.253 +        self.assertEqual(defaults['title'], site.title)
  64.254 +
  64.255 +    def test_export_empty_site_with_setup_tool(self):
  64.256 +        self._setUpAdapters()
  64.257 +        site = _makeFolder('site')
  64.258 +        site._setObject('setup_tool', self._makeSetupTool())
  64.259 +        site._updateProperty('title', 'test_export_empty_site_with_setup_tool')
  64.260 +        site._setProperty('description',
  64.261 +                          'Testing export of an empty site with setup tool.')
  64.262 +        context = DummyExportContext(site)
  64.263 +        exporter = self._getExporter()
  64.264 +        exporter(context)
  64.265 +
  64.266 +        self.assertEqual(len(context._wrote), 2)
  64.267 +        filename, text, content_type = context._wrote[0]
  64.268 +        self.assertEqual(filename, 'structure/.objects')
  64.269 +        self.assertEqual(content_type, 'text/comma-separated-values')
  64.270 +
  64.271 +        objects = [x for x in reader(StringIO(text))]
  64.272 +        self.assertEqual(len(objects), 0)
  64.273 +
  64.274 +        filename, text, content_type = context._wrote[1]
  64.275 +        self.assertEqual(filename, 'structure/.properties')
  64.276 +        self.assertEqual(content_type, 'text/plain')
  64.277 +
  64.278 +        parser = ConfigParser()
  64.279 +        parser.readfp(StringIO(text))
  64.280 +
  64.281 +        defaults = parser.defaults()
  64.282 +        self.assertEqual(len(defaults), 2)
  64.283 +        self.assertEqual(defaults['title'], site.title)
  64.284 +        self.assertEqual(defaults['description'], site.description)
  64.285 +
  64.286 +    def test_export_site_with_non_exportable_simple_items(self):
  64.287 +        from Products.GenericSetup.utils import _getDottedName
  64.288 +        self._setUpAdapters()
  64.289 +        ITEM_IDS = ('foo', 'bar', 'baz')
  64.290 +
  64.291 +        site = _makeFolder('site')
  64.292 +        site.title = 'AAA'
  64.293 +        site._setProperty('description', 'BBB')
  64.294 +        item = _makeItem('aside')
  64.295 +        dotted = _getDottedName(item.__class__)
  64.296 +        for id in ITEM_IDS:
  64.297 +            site._setObject(id, _makeItem(id))
  64.298 +
  64.299 +        context = DummyExportContext(site)
  64.300 +        exporter = self._getExporter()
  64.301 +        exporter(context)
  64.302 +
  64.303 +        self.assertEqual(len(context._wrote), 2)
  64.304 +        filename, text, content_type = context._wrote[0]
  64.305 +        self.assertEqual(filename, 'structure/.objects')
  64.306 +        self.assertEqual(content_type, 'text/comma-separated-values')
  64.307 +
  64.308 +        objects = [x for x in reader(StringIO(text))]
  64.309 +        self.assertEqual(len(objects), 3)
  64.310 +        for index in range(len(ITEM_IDS)):
  64.311 +            self.assertEqual(objects[index][0], ITEM_IDS[index])
  64.312 +            self.assertEqual(objects[index][1], dotted)
  64.313 +
  64.314 +        filename, text, content_type = context._wrote[1]
  64.315 +        self.assertEqual(filename, 'structure/.properties')
  64.316 +        self.assertEqual(content_type, 'text/plain')
  64.317 +        parser = ConfigParser()
  64.318 +        parser.readfp(StringIO(text))
  64.319 +
  64.320 +        defaults = parser.defaults()
  64.321 +        self.assertEqual(len(defaults), 2)
  64.322 +        self.assertEqual(defaults['title'], 'AAA')
  64.323 +        self.assertEqual(defaults['description'], 'BBB')
  64.324 +
  64.325 +    def test_export_site_with_exportable_simple_items(self):
  64.326 +        from Products.GenericSetup.utils import _getDottedName
  64.327 +        self._setUpAdapters()
  64.328 +        ITEM_IDS = ('foo', 'bar', 'baz')
  64.329 +
  64.330 +        site = _makeFolder('site')
  64.331 +        site.title = 'AAA'
  64.332 +        site._setProperty('description', 'BBB')
  64.333 +        aware = _makeINIAware('aside')
  64.334 +        dotted = _getDottedName(aware.__class__)
  64.335 +        for id in ITEM_IDS:
  64.336 +            site._setObject(id, _makeINIAware(id))
  64.337 +
  64.338 +        context = DummyExportContext(site)
  64.339 +        exporter = self._getExporter()
  64.340 +        exporter(context)
  64.341 +
  64.342 +        self.assertEqual(len(context._wrote), 2 + len(ITEM_IDS))
  64.343 +        filename, text, content_type = context._wrote[0]
  64.344 +        self.assertEqual(filename, 'structure/.objects')
  64.345 +        self.assertEqual(content_type, 'text/comma-separated-values')
  64.346 +
  64.347 +        objects = [x for x in reader(StringIO(text))]
  64.348 +        self.assertEqual(len(objects), 3)
  64.349 +        for index in range(len(ITEM_IDS)):
  64.350 +            self.assertEqual(objects[index][0], ITEM_IDS[index])
  64.351 +            self.assertEqual(objects[index][1], dotted)
  64.352 +
  64.353 +            filename, text, content_type = context._wrote[index+2]
  64.354 +            self.assertEqual(filename, 'structure/%s.ini' % ITEM_IDS[index])
  64.355 +            object = site._getOb(ITEM_IDS[index])
  64.356 +            self.assertEqual(text.strip(),
  64.357 +                             object.as_ini().strip())
  64.358 +            self.assertEqual(content_type, 'text/plain')
  64.359 +
  64.360 +        filename, text, content_type = context._wrote[1]
  64.361 +        self.assertEqual(filename, 'structure/.properties')
  64.362 +        self.assertEqual(content_type, 'text/plain')
  64.363 +        parser = ConfigParser()
  64.364 +        parser.readfp(StringIO(text))
  64.365 +
  64.366 +        defaults = parser.defaults()
  64.367 +        self.assertEqual(len(defaults), 2)
  64.368 +        self.assertEqual(defaults['title'], 'AAA')
  64.369 +        self.assertEqual(defaults['description'], 'BBB')
  64.370 +
  64.371 +    def test_export_site_with_subfolders(self):
  64.372 +        from Products.GenericSetup.utils import _getDottedName
  64.373 +        self._setUpAdapters()
  64.374 +        FOLDER_IDS = ('foo', 'bar', 'baz')
  64.375 +
  64.376 +        site = _makeFolder('site')
  64.377 +        site.title = 'AAA'
  64.378 +        site._setProperty('description', 'BBB')
  64.379 +        aside = _makeFolder('aside')
  64.380 +        dotted = _getDottedName(aside.__class__)
  64.381 +        for id in FOLDER_IDS:
  64.382 +            folder = _makeFolder(id)
  64.383 +            folder.title = 'Title: %s' % id
  64.384 +            site._setObject(id, folder)
  64.385 +
  64.386 +        context = DummyExportContext(site)
  64.387 +        exporter = self._getExporter()
  64.388 +        exporter(context)
  64.389 +
  64.390 +        self.assertEqual(len(context._wrote), 2 + (2 *len(FOLDER_IDS)))
  64.391 +        filename, text, content_type = context._wrote[0]
  64.392 +        self.assertEqual(filename, 'structure/.objects')
  64.393 +        self.assertEqual(content_type, 'text/comma-separated-values')
  64.394 +
  64.395 +        objects = [x for x in reader(StringIO(text))]
  64.396 +        self.assertEqual(len(objects), 3)
  64.397 +
  64.398 +        for index in range(len(FOLDER_IDS)):
  64.399 +            id = FOLDER_IDS[index]
  64.400 +            self.assertEqual(objects[index][0], id)
  64.401 +            self.assertEqual(objects[index][1], dotted)
  64.402 +
  64.403 +            filename, text, content_type = context._wrote[2 + (2 * index)]
  64.404 +            self.assertEqual(filename, '/'.join(('structure', id, '.objects')))
  64.405 +            self.assertEqual(content_type, 'text/comma-separated-values')
  64.406 +            subobjects = [x for x in reader(StringIO(text))]
  64.407 +            self.assertEqual(len(subobjects), 0)
  64.408 +
  64.409 +            filename, text, content_type = context._wrote[2 + (2 * index) + 1]
  64.410 +            self.assertEqual(filename,
  64.411 +                             '/'.join(('structure', id, '.properties')))
  64.412 +            self.assertEqual(content_type, 'text/plain')
  64.413 +            parser = ConfigParser()
  64.414 +            parser.readfp(StringIO(text))
  64.415 +
  64.416 +            defaults = parser.defaults()
  64.417 +            self.assertEqual(len(defaults), 1)
  64.418 +            self.assertEqual(defaults['title'], 'Title: %s' % id)
  64.419 +
  64.420 +        filename, text, content_type = context._wrote[1]
  64.421 +        self.assertEqual(filename, 'structure/.properties')
  64.422 +        self.assertEqual(content_type, 'text/plain')
  64.423 +
  64.424 +        parser = ConfigParser()
  64.425 +        parser.readfp(StringIO(text))
  64.426 +
  64.427 +        defaults = parser.defaults()
  64.428 +        self.assertEqual(len(defaults), 2)
  64.429 +        self.assertEqual(defaults['title'], 'AAA')
  64.430 +        self.assertEqual(defaults['description'], 'BBB')
  64.431 +
  64.432 +    def test_export_site_with_csvaware(self):
  64.433 +        from Products.GenericSetup.utils import _getDottedName
  64.434 +        self._setUpAdapters()
  64.435 +
  64.436 +        site = _makeFolder('site')
  64.437 +        site.title = 'test_export_site_with_csvaware'
  64.438 +        site._setProperty('description',
  64.439 +                          'Testing export of an site with CSV-aware content.')
  64.440 +
  64.441 +        aware = _makeCSVAware('aware')
  64.442 +        site._setObject('aware', aware)
  64.443 +
  64.444 +        context = DummyExportContext(site)
  64.445 +        exporter = self._getExporter()
  64.446 +        exporter(context)
  64.447 +
  64.448 +        self.assertEqual(len(context._wrote), 3)
  64.449 +        filename, text, content_type = context._wrote[0]
  64.450 +        self.assertEqual(filename, 'structure/.objects')
  64.451 +        self.assertEqual(content_type, 'text/comma-separated-values')
  64.452 +
  64.453 +        objects = [x for x in reader(StringIO(text))]
  64.454 +        self.assertEqual(len(objects), 1)
  64.455 +        self.assertEqual(objects[0][0], 'aware')
  64.456 +        self.assertEqual(objects[0][1], _getDottedName(aware.__class__))
  64.457 +
  64.458 +        filename, text, content_type = context._wrote[1]
  64.459 +        self.assertEqual(filename, 'structure/.properties')
  64.460 +        self.assertEqual(content_type, 'text/plain')
  64.461 +
  64.462 +        parser = ConfigParser()
  64.463 +        parser.readfp(StringIO(text))
  64.464 +
  64.465 +        defaults = parser.defaults()
  64.466 +        self.assertEqual(len(defaults), 2)
  64.467 +        self.assertEqual(defaults['title'], site.title)
  64.468 +        self.assertEqual(defaults['description'], site.description)
  64.469 +
  64.470 +        filename, text, content_type = context._wrote[2]
  64.471 +        self.assertEqual(filename, 'structure/aware.csv')
  64.472 +        self.assertEqual(content_type, 'text/comma-separated-values')
  64.473 +        rows = [x for x in reader(StringIO(text))]
  64.474 +        self.assertEqual(len(rows), 2)
  64.475 +        self.assertEqual(rows[0][0], 'one')
  64.476 +        self.assertEqual(rows[0][1], 'two')
  64.477 +        self.assertEqual(rows[0][2], 'three')
  64.478 +        self.assertEqual(rows[1][0], 'four')
  64.479 +        self.assertEqual(rows[1][1], 'five')
  64.480 +        self.assertEqual(rows[1][2], 'six')
  64.481 +
  64.482 +    def test_import_empty_site(self):
  64.483 +        self._setUpAdapters()
  64.484 +        site = _makeFolder('site')
  64.485 +        context = DummyImportContext(site)
  64.486 +        context._files['structure/.objects'] = ''
  64.487 +        importer = self._getImporter()
  64.488 +        self.assertEqual(len(site.objectIds()), 0)
  64.489 +        importer(context)
  64.490 +        self.assertEqual(len(site.objectIds()), 0)
  64.491 +
  64.492 +    def test_import_empty_site_with_setup_tool(self):
  64.493 +        self._setUpAdapters()
  64.494 +        site = _makeFolder('site')
  64.495 +        site._setObject('setup_tool', self._makeSetupTool())
  64.496 +        context = DummyImportContext(site)
  64.497 +        importer = self._getImporter()
  64.498 +
  64.499 +        self.assertEqual(len(site.objectIds()), 1)
  64.500 +        self.assertEqual(site.objectIds()[0], 'setup_tool')
  64.501 +        importer(context)
  64.502 +        self.assertEqual(len(site.objectIds()), 1)
  64.503 +        self.assertEqual(site.objectIds()[0], 'setup_tool')
  64.504 +
  64.505 +    def test_import_site_with_subfolders(self):
  64.506 +        from Products.GenericSetup.utils import _getDottedName
  64.507 +        self._setUpAdapters()
  64.508 +        FOLDER_IDS = ('foo', 'bar', 'baz')
  64.509 +
  64.510 +        site = _makeFolder('site')
  64.511 +        dotted = _getDottedName(site.__class__)
  64.512 +
  64.513 +        context = DummyImportContext(site)
  64.514 +
  64.515 +        for id in FOLDER_IDS:
  64.516 +            context._files['structure/%s/.objects' % id] = ''
  64.517 +            context._files['structure/%s/.properties' % id] = (
  64.518 +                _PROPERTIES_TEMPLATE % id )
  64.519 +
  64.520 +        _ROOT_OBJECTS = '\n'.join(['%s,%s' % (id, dotted)
  64.521 +                                        for id in FOLDER_IDS])
  64.522 +
  64.523 +        context._files['structure/.objects'] = _ROOT_OBJECTS
  64.524 +        context._files['structure/.properties'] = (
  64.525 +                _PROPERTIES_TEMPLATE % 'Test Site')
  64.526 +
  64.527 +        importer = self._getImporter()
  64.528 +        importer(context)
  64.529 +
  64.530 +        content = site.objectValues()
  64.531 +        self.assertEqual(len(content), len(FOLDER_IDS))
  64.532 +
  64.533 +    def test_import_site_with_subitems(self):
  64.534 +        from Products.GenericSetup.utils import _getDottedName
  64.535 +        from faux_objects import KNOWN_INI
  64.536 +        from faux_objects import TestINIAware
  64.537 +        dotted = _getDottedName(TestINIAware)
  64.538 +        self._setUpAdapters()
  64.539 +        ITEM_IDS = ('foo', 'bar', 'baz')
  64.540 +
  64.541 +        site = _makeFolder('site')
  64.542 +
  64.543 +        context = DummyImportContext(site)
  64.544 +        # We want to add 'baz' to 'foo', without losing 'bar'
  64.545 +        context._files['structure/.objects'] = '\n'.join(
  64.546 +                            ['%s,%s' % (x, dotted) for x in ITEM_IDS])
  64.547 +        for index in range(len(ITEM_IDS)):
  64.548 +            id = ITEM_IDS[index]
  64.549 +            context._files[
  64.550 +                    'structure/%s.ini' % id] = KNOWN_INI % ('Title: %s' % id,
  64.551 +                                                            'xyzzy',
  64.552 +                                                           )
  64.553 +        importer = self._getImporter()
  64.554 +        importer(context)
  64.555 +
  64.556 +        after = site.objectIds()
  64.557 +        self.assertEqual(len(after), len(ITEM_IDS))
  64.558 +        for found_id, expected_id in zip(after, ITEM_IDS):
  64.559 +            self.assertEqual(found_id, expected_id)
  64.560 +
  64.561 +    def test_import_site_with_subitem_unknown_portal_type(self):
  64.562 +        from faux_objects import KNOWN_INI
  64.563 +        self._setUpAdapters()
  64.564 +        ITEM_IDS = ('foo', 'bar', 'baz')
  64.565 +
  64.566 +        site = _makeFolder('site')
  64.567 +
  64.568 +        context = DummyImportContext(site)
  64.569 +        # We want to add 'baz' to 'foo', without losing 'bar'
  64.570 +        context._files['structure/.objects'] = '\n'.join(
  64.571 +                                ['%s,Unknown Type' % x for x in ITEM_IDS])
  64.572 +        for index in range(len(ITEM_IDS)):
  64.573 +            id = ITEM_IDS[index]
  64.574 +            context._files[
  64.575 +                    'structure/%s.ini' % id] = KNOWN_INI % ('Title: %s' % id,
  64.576 +                                                            'xyzzy',
  64.577 +                                                           )
  64.578 +
  64.579 +        importer = self._getImporter()
  64.580 +        importer(context)
  64.581 +
  64.582 +        after = site.objectIds()
  64.583 +        self.assertEqual(len(after), 0)
  64.584 +        self.assertEqual(len(context._notes), len(ITEM_IDS))
  64.585 +        for level, component, message in context._notes:
  64.586 +            self.assertEqual(component, 'SFWA')
  64.587 +            self.failUnless(message.startswith("Couldn't make"))
  64.588 +
  64.589 +    def test_import_site_with_subitems_and_no_preserve(self):
  64.590 +        self._setUpAdapters()
  64.591 +        ITEM_IDS = ('foo', 'bar', 'baz')
  64.592 +
  64.593 +        site = _makeFolder('site')
  64.594 +        for id in ITEM_IDS:
  64.595 +            site._setObject(id, _makeItem(id))
  64.596 +
  64.597 +        context = DummyImportContext(site)
  64.598 +        # We want to add 'baz' to 'foo', without losing 'bar'
  64.599 +        context._files['structure/.objects'] = ''
  64.600 +
  64.601 +        importer = self._getImporter()
  64.602 +        importer(context)
  64.603 +
  64.604 +        self.assertEqual(len(site.objectIds()), 0)
  64.605 +
  64.606 +    def test_import_site_with_subitemss_and_preserve(self):
  64.607 +        self._setUpAdapters()
  64.608 +        ITEM_IDS = ('foo', 'bar', 'baz')
  64.609 +
  64.610 +        site = _makeFolder('site')
  64.611 +        for id in ITEM_IDS:
  64.612 +            site._setObject(id, _makeItem(id))
  64.613 +
  64.614 +        context = DummyImportContext(site)
  64.615 +        # We want to add 'baz' to 'foo', without losing 'bar'
  64.616 +        context._files['structure/.objects'] = ''
  64.617 +        context._files['structure/.preserve'] = '*'
  64.618 +
  64.619 +        importer = self._getImporter()
  64.620 +        importer(context)
  64.621 +
  64.622 +        after = site.objectIds()
  64.623 +        self.assertEqual(len(after), len(ITEM_IDS))
  64.624 +        for i in range(len(ITEM_IDS)):
  64.625 +            self.assertEqual(after[i], ITEM_IDS[i])
  64.626 +
  64.627 +    def test_import_site_with_subitemss_and_preserve_partial(self):
  64.628 +        self._setUpAdapters()
  64.629 +        ITEM_IDS = ('foo', 'bar', 'baz')
  64.630 +
  64.631 +        site = _makeFolder('site')
  64.632 +        for id in ITEM_IDS:
  64.633 +            site._setObject(id, _makeItem(id))
  64.634 +
  64.635 +        context = DummyImportContext(site)
  64.636 +        # We want to add 'baz' to 'foo', without losing 'bar'
  64.637 +        context._files['structure/.objects'] = ''
  64.638 +        context._files['structure/.preserve'] = 'b*'
  64.639 +
  64.640 +        importer = self._getImporter()
  64.641 +        importer(context)
  64.642 +
  64.643 +        after = site.objectIds()
  64.644 +        self.assertEqual(len(after), 2)
  64.645 +        self.assertEqual(after[0], 'bar')
  64.646 +        self.assertEqual(after[1], 'baz')
  64.647 +
  64.648 +    def test_import_site_with_subfolders_and_preserve(self):
  64.649 +        from Products.GenericSetup.utils import _getDottedName
  64.650 +        self._setUpAdapters()
  64.651 +
  64.652 +        site = _makeFolder('site')
  64.653 +        site._setObject('foo', _makeFolder('foo'))
  64.654 +        foo = site._getOb('foo')
  64.655 +        foo._setObject('bar', _makeFolder('bar'))
  64.656 +        bar = foo._getOb('bar')
  64.657 +
  64.658 +        context = DummyImportContext(site)
  64.659 +        # We want to add 'baz' to 'foo', without losing 'bar'
  64.660 +        context._files['structure/.objects'
  64.661 +                      ] = 'foo,%s' % _getDottedName(foo.__class__)
  64.662 +        context._files['structure/.preserve'] = '*'
  64.663 +        context._files['structure/foo/.objects'
  64.664 +                      ] = 'baz,%s' % _getDottedName(bar.__class__)
  64.665 +        context._files['structure/foo/.preserve'] = '*'
  64.666 +        context._files['structure/foo/baz/.objects'] = ''
  64.667 +
  64.668 +        importer = self._getImporter()
  64.669 +        importer(context)
  64.670 +
  64.671 +        self.assertEqual(len(site.objectIds()), 1)
  64.672 +        self.assertEqual(site.objectIds()[0], 'foo')
  64.673 +
  64.674 +        self.assertEqual(len(foo.objectIds()), 2, site.foo.objectIds())
  64.675 +        self.assertEqual(foo.objectIds()[0], 'bar')
  64.676 +        self.assertEqual(foo.objectIds()[1], 'baz')
  64.677 +
  64.678 +
  64.679 +class Test_globpattern(unittest.TestCase):
  64.680 +
  64.681 +    NAMELIST = ('foo', 'bar', 'baz', 'bam', 'qux', 'quxx', 'quxxx')
  64.682 +
  64.683 +    def _checkResults(self, globpattern, namelist, expected):
  64.684 +        from Products.GenericSetup.content import _globtest
  64.685 +        found = _globtest(globpattern, namelist)
  64.686 +        self.assertEqual(len(found), len(expected))
  64.687 +        for found_item, expected_item in zip(found, expected):
  64.688 +            self.assertEqual(found_item, expected_item)
  64.689 +
  64.690 +    def test_star(self):
  64.691 +        self._checkResults('*', self.NAMELIST, self.NAMELIST)
  64.692 +
  64.693 +    def test_simple(self):
  64.694 +        self._checkResults('b*', self.NAMELIST,
  64.695 +                            [x for x in self.NAMELIST if x.startswith('b')])
  64.696 +
  64.697 +    def test_multiple(self):
  64.698 +        self._checkResults('b*\n*x', self.NAMELIST,
  64.699 +                            [x for x in self.NAMELIST
  64.700 +                                if x.startswith('b') or x.endswith('x')])
  64.701 +
  64.702 +
  64.703 +class CSVAwareFileAdapterTests(unittest.TestCase,
  64.704 +                               ConformsToIFilesystemExporter,
  64.705 +                               ConformsToIFilesystemImporter,
  64.706 +                              ):
  64.707 +
  64.708 +    def _getTargetClass(self):
  64.709 +        from Products.GenericSetup.content import CSVAwareFileAdapter
  64.710 +        return CSVAwareFileAdapter
  64.711 +
  64.712 +    def _makeOne(self, context, *args, **kw):
  64.713 +        return self._getTargetClass()(context, *args, **kw)
  64.714 +
  64.715 +    def test_export_with_known_CSV(self):
  64.716 +        from faux_objects import KNOWN_CSV
  64.717 +        sheet = _makeCSVAware('config')
  64.718 +
  64.719 +        adapter = self._makeOne(sheet)
  64.720 +        context = DummyExportContext(None)
  64.721 +        adapter.export(context, 'subpath/to/sheet')
  64.722 +
  64.723 +        self.assertEqual(len(context._wrote), 1)
  64.724 +        filename, text, content_type = context._wrote[0]
  64.725 +        self.assertEqual(filename, 'subpath/to/sheet/config.csv')
  64.726 +        self.assertEqual(content_type, 'text/comma-separated-values')
  64.727 +
  64.728 +        self.assertEqual(text.strip(), KNOWN_CSV.strip())
  64.729 +
  64.730 +    def test_import_with_known_CSV(self):
  64.731 +        ORIG_CSV = """\
  64.732 +one,two,three
  64.733 +four,five,six
  64.734 +"""
  64.735 +        NEW_CSV = """\
  64.736 +four,five,six
  64.737 +one,two,three
  64.738 +"""
  64.739 +        sheet = _makeCSVAware('config', ORIG_CSV)
  64.740 +
  64.741 +        adapter = self._makeOne(sheet)
  64.742 +        context = DummyImportContext(None)
  64.743 +        context._files['subpath/to/sheet/config.csv'] = NEW_CSV
  64.744 +        adapter.import_(context, 'subpath/to/sheet')
  64.745 +
  64.746 +        self.assertEqual(sheet._was_put.getvalue().strip(), NEW_CSV.strip())
  64.747 +
  64.748 +
  64.749 +_PROPERTIES_TEMPLATE = """
  64.750 +[DEFAULT]
  64.751 +Title = %s
  64.752 +Description = This is a test
  64.753 +"""
  64.754 +
  64.755 +class INIAwareFileAdapterTests(unittest.TestCase,
  64.756 +                               ConformsToIFilesystemExporter,
  64.757 +                               ConformsToIFilesystemImporter,
  64.758 +                               ):
  64.759 +
  64.760 +    def _getTargetClass(self):
  64.761 +        from Products.GenericSetup.content import INIAwareFileAdapter
  64.762 +        return INIAwareFileAdapter
  64.763 +
  64.764 +    def _makeOne(self, context, *args, **kw):
  64.765 +        return self._getTargetClass()(context, *args, **kw)
  64.766 +
  64.767 +    def test_export_ini_file(self):
  64.768 +        ini_file = _makeINIAware('ini_file.html')
  64.769 +        adapter = self._makeOne(ini_file)
  64.770 +        context = DummyExportContext(None)
  64.771 +        adapter.export(context, 'subpath/to')
  64.772 +
  64.773 +        self.assertEqual(len(context._wrote), 1)
  64.774 +        filename, text, content_type = context._wrote[0]
  64.775 +        self.assertEqual(filename, 'subpath/to/ini_file.html.ini')
  64.776 +        self.assertEqual(content_type, 'text/plain')
  64.777 +
  64.778 +        self.assertEqual(text.strip(), ini_file.as_ini().strip())
  64.779 +
  64.780 +    def test_import_ini_file(self):
  64.781 +        from faux_objects import KNOWN_INI
  64.782 +        ini_file = _makeINIAware('ini_file.html')
  64.783 +        adapter = self._makeOne(ini_file)
  64.784 +        context = DummyImportContext(None)
  64.785 +        context._files['subpath/to/ini_file.html.ini'] = (
  64.786 +                        KNOWN_INI % ('Title: ini_file', 'abc'))
  64.787 +
  64.788 +        adapter.import_(context, 'subpath/to')
  64.789 +        text = ini_file._was_put
  64.790 +        parser = ConfigParser()
  64.791 +        parser.readfp(StringIO(text))
  64.792 +        self.assertEqual(parser.get('DEFAULT', 'title'), 'Title: ini_file')
  64.793 +        self.assertEqual(parser.get('DEFAULT', 'description'), 'abc')
  64.794 +
  64.795 +
  64.796 +class DAVAwareFileAdapterTests(unittest.TestCase,
  64.797 +                               ConformsToIFilesystemExporter,
  64.798 +                               ConformsToIFilesystemImporter,
  64.799 +                               ):
  64.800 +
  64.801 +    def _getTargetClass(self):
  64.802 +        from Products.GenericSetup.content import DAVAwareFileAdapter
  64.803 +        return DAVAwareFileAdapter
  64.804 +
  64.805 +    def _makeOne(self, context, *args, **kw):
  64.806 +        return self._getTargetClass()(context, *args, **kw)
  64.807 +
  64.808 +    def test_export_dav_file(self):
  64.809 +        dav_file = _makeDAVAware('dav_file.html')
  64.810 +        adapter = self._makeOne(dav_file)
  64.811 +        context = DummyExportContext(None)
  64.812 +        adapter.export(context, 'subpath/to')
  64.813 +
  64.814 +        self.assertEqual(len(context._wrote), 1)
  64.815 +        filename, text, content_type = context._wrote[0]
  64.816 +        self.assertEqual(filename, 'subpath/to/dav_file.html')
  64.817 +        self.assertEqual(content_type, 'text/plain')
  64.818 +        self.assertEqual(text.strip(), dav_file.manage_FTPget().strip())
  64.819 +
  64.820 +    def test_import_dav_file(self):
  64.821 +        from faux_objects import KNOWN_DAV
  64.822 +        VALUES = ('Title: dav_file', 'Description: abc', 'body goes here')
  64.823 +        dav_file = _makeDAVAware('dav_file.html')
  64.824 +        adapter = self._makeOne(dav_file)
  64.825 +        context = DummyImportContext(None)
  64.826 +        context._files['subpath/to/dav_file.html'] = KNOWN_DAV % VALUES
  64.827 +
  64.828 +        adapter.import_(context, 'subpath/to')
  64.829 +        text = dav_file._was_put == KNOWN_DAV % VALUES
  64.830 +
  64.831 +
  64.832 +def _makePropertied(id):
  64.833 +    from faux_objects import TestSimpleItemWithProperties
  64.834 +
  64.835 +    propertied = TestSimpleItemWithProperties()
  64.836 +    propertied._setId(id)
  64.837 +
  64.838 +    return propertied
  64.839 +
  64.840 +def _makeCSVAware(id, csv=None):
  64.841 +    from faux_objects import TestCSVAware
  64.842 +
  64.843 +    aware = TestCSVAware()
  64.844 +    aware._setId(id)
  64.845 +    if csv is not None:
  64.846 +        aware._csv = csv
  64.847 +
  64.848 +    return aware
  64.849 +
  64.850 +
  64.851 +def _makeINIAware(id):
  64.852 +    from faux_objects import TestINIAware
  64.853 +
  64.854 +    aware = TestINIAware()
  64.855 +    aware._setId(id)
  64.856 +
  64.857 +    return aware
  64.858 +
  64.859 +
  64.860 +def _makeDAVAware(id):
  64.861 +    from faux_objects import TestDAVAware
  64.862 +
  64.863 +    aware = TestDAVAware()
  64.864 +    aware._setId(id)
  64.865 +
  64.866 +    return aware
  64.867 +
  64.868 +
  64.869 +def _makeItem(id):
  64.870 +    from faux_objects import TestSimpleItem
  64.871 +
  64.872 +    aware = TestSimpleItem()
  64.873 +    aware._setId(id)
  64.874 +
  64.875 +    return aware
  64.876 +
  64.877 +
  64.878 +def _makeFolder(id):
  64.879 +    from OFS.Folder import Folder
  64.880 +    from zope.interface import directlyProvides
  64.881 +    from zope.interface import providedBy
  64.882 +
  64.883 +    folder = Folder(id)
  64.884 +    directlyProvides(folder, providedBy(folder)
  64.885 +                             + IObjectManager + IPropertyManager)
  64.886 +
  64.887 +    return folder
  64.888 +
  64.889 +
  64.890 +def test_suite():
  64.891 +    suite = unittest.TestSuite()
  64.892 +    suite.addTest(unittest.makeSuite(SimpleINIAwareTests))
  64.893 +    suite.addTest(unittest.makeSuite(FolderishExporterImporterTests))
  64.894 +    suite.addTest(unittest.makeSuite(Test_globpattern))
  64.895 +    suite.addTest(unittest.makeSuite(CSVAwareFileAdapterTests))
  64.896 +    suite.addTest(unittest.makeSuite(INIAwareFileAdapterTests))
  64.897 +    suite.addTest(unittest.makeSuite(DAVAwareFileAdapterTests))
  64.898 +    return suite
  64.899 +
  64.900 +if __name__ == '__main__':
  64.901 +    unittest.main(defaultTest='test_suite')
    65.1 new file mode 100644
    65.2 --- /dev/null
    65.3 +++ b/tests/test_context.py
    65.4 @@ -0,0 +1,1444 @@
    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 import / export contexts.
   65.18 +
   65.19 +$Id: test_context.py 41502 2006-01-30 17:48:12Z efge $
   65.20 +"""
   65.21 +
   65.22 +import unittest
   65.23 +import Testing
   65.24 +
   65.25 +import logging
   65.26 +import os
   65.27 +import time
   65.28 +from StringIO import StringIO
   65.29 +from tarfile import TarFile
   65.30 +from tarfile import TarInfo
   65.31 +
   65.32 +from DateTime.DateTime import DateTime
   65.33 +from OFS.Folder import Folder
   65.34 +from OFS.Image import File
   65.35 +
   65.36 +from common import FilesystemTestBase
   65.37 +from common import SecurityRequestTest
   65.38 +from common import TarballTester
   65.39 +from common import _makeTestFile
   65.40 +from conformance import ConformsToISetupContext
   65.41 +from conformance import ConformsToIImportContext
   65.42 +from conformance import ConformsToIExportContext
   65.43 +
   65.44 +
   65.45 +class DummySite( Folder ):
   65.46 +
   65.47 +    pass
   65.48 +
   65.49 +class DummyTool( Folder ):
   65.50 +
   65.51 +    pass
   65.52 +
   65.53 +
   65.54 +class DirectoryImportContextTests( FilesystemTestBase
   65.55 +                                 , ConformsToISetupContext
   65.56 +                                 , ConformsToIImportContext
   65.57 +                                 ):
   65.58 +
   65.59 +    _PROFILE_PATH = '/tmp/ICTTexts'
   65.60 +
   65.61 +    def _getTargetClass( self ):
   65.62 +
   65.63 +        from Products.GenericSetup.context import DirectoryImportContext
   65.64 +        return DirectoryImportContext
   65.65 +
   65.66 +    def test_getLogger( self ):
   65.67 +
   65.68 +        site = DummySite( 'site' ).__of__( self.root )
   65.69 +        ctx = self._makeOne( site, self._PROFILE_PATH )
   65.70 +        self.assertEqual( len( ctx.listNotes() ), 0 )
   65.71 +
   65.72 +        logger = ctx.getLogger('foo')
   65.73 +        logger.info('bar')
   65.74 +
   65.75 +        self.assertEqual( len( ctx.listNotes() ), 1 )
   65.76 +        level, component, message = ctx.listNotes()[0]
   65.77 +        self.assertEqual( level, logging.INFO )
   65.78 +        self.assertEqual( component, 'foo' )
   65.79 +        self.assertEqual( message, 'bar' )
   65.80 +
   65.81 +        ctx.clearNotes()
   65.82 +        self.assertEqual( len( ctx.listNotes() ), 0 )
   65.83 +
   65.84 +    def test_readDataFile_nonesuch( self ):
   65.85 +
   65.86 +        FILENAME = 'nonesuch.txt'
   65.87 +
   65.88 +        site = DummySite( 'site' ).__of__( self.root )
   65.89 +        ctx = self._makeOne( site, self._PROFILE_PATH )
   65.90 +
   65.91 +        self.assertEqual( ctx.readDataFile( FILENAME ), None )
   65.92 +
   65.93 +    def test_readDataFile_simple( self ):
   65.94 +
   65.95 +        from string import printable
   65.96 +
   65.97 +        FILENAME = 'simple.txt'
   65.98 +        self._makeFile( FILENAME, printable )
   65.99 +
  65.100 +        site = DummySite( 'site' ).__of__( self.root )
  65.101 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.102 +
  65.103 +        self.assertEqual( ctx.readDataFile( FILENAME ), printable )
  65.104 +
  65.105 +    def test_readDataFile_subdir( self ):
  65.106 +
  65.107 +        from string import printable
  65.108 +
  65.109 +        FILENAME = 'subdir/nested.txt'
  65.110 +        self._makeFile( FILENAME, printable )
  65.111 +
  65.112 +        site = DummySite( 'site' ).__of__( self.root )
  65.113 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.114 +
  65.115 +        self.assertEqual( ctx.readDataFile( FILENAME ), printable )
  65.116 +
  65.117 +    def test_getLastModified_nonesuch( self ):
  65.118 +
  65.119 +        FILENAME = 'nonesuch.txt'
  65.120 +
  65.121 +        site = DummySite( 'site' ).__of__( self.root )
  65.122 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.123 +
  65.124 +        self.assertEqual( ctx.getLastModified( FILENAME ), None )
  65.125 +
  65.126 +    def test_getLastModified_simple( self ):
  65.127 +
  65.128 +        from string import printable
  65.129 +
  65.130 +        FILENAME = 'simple.txt'
  65.131 +        fqpath = self._makeFile( FILENAME, printable )
  65.132 +        timestamp = os.path.getmtime( fqpath )
  65.133 +
  65.134 +        site = DummySite( 'site' ).__of__( self.root )
  65.135 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.136 +
  65.137 +        lm = ctx.getLastModified( FILENAME )
  65.138 +        self.failUnless( isinstance( lm, DateTime ) )
  65.139 +        self.assertEqual( lm, timestamp )
  65.140 +
  65.141 +    def test_getLastModified_subdir( self ):
  65.142 +
  65.143 +        from string import printable
  65.144 +
  65.145 +        SUBDIR = 'subdir'
  65.146 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.147 +        fqpath = self._makeFile( FILENAME, printable )
  65.148 +        timestamp = os.path.getmtime( fqpath )
  65.149 +
  65.150 +        site = DummySite( 'site' ).__of__( self.root )
  65.151 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.152 +
  65.153 +        lm = ctx.getLastModified( FILENAME )
  65.154 +        self.failUnless( isinstance( lm, DateTime ) )
  65.155 +        self.assertEqual( lm, timestamp )
  65.156 +
  65.157 +    def test_getLastModified_directory( self ):
  65.158 +
  65.159 +        from string import printable
  65.160 +
  65.161 +        SUBDIR = 'subdir'
  65.162 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.163 +        fqpath = self._makeFile( FILENAME, printable )
  65.164 +        path, file = os.path.split( fqpath )
  65.165 +        timestamp = os.path.getmtime( path )
  65.166 +
  65.167 +        site = DummySite( 'site' ).__of__( self.root )
  65.168 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.169 +
  65.170 +        lm = ctx.getLastModified( SUBDIR )
  65.171 +        self.failUnless( isinstance( lm, DateTime ) )
  65.172 +        self.assertEqual( lm, timestamp )
  65.173 +
  65.174 +    def test_isDirectory_nonesuch( self ):
  65.175 +
  65.176 +        FILENAME = 'nonesuch.txt'
  65.177 +
  65.178 +        site = DummySite( 'site' ).__of__( self.root )
  65.179 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.180 +
  65.181 +        self.assertEqual( ctx.isDirectory( FILENAME ), None )
  65.182 +
  65.183 +    def test_isDirectory_simple( self ):
  65.184 +
  65.185 +        from string import printable
  65.186 +
  65.187 +        FILENAME = 'simple.txt'
  65.188 +        fqpath = self._makeFile( FILENAME, printable )
  65.189 +
  65.190 +        site = DummySite( 'site' ).__of__( self.root )
  65.191 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.192 +
  65.193 +        self.assertEqual( ctx.isDirectory( FILENAME ), False )
  65.194 +
  65.195 +    def test_isDirectory_nested( self ):
  65.196 +
  65.197 +        from string import printable
  65.198 +
  65.199 +        SUBDIR = 'subdir'
  65.200 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.201 +        fqpath = self._makeFile( FILENAME, printable )
  65.202 +
  65.203 +        site = DummySite( 'site' ).__of__( self.root )
  65.204 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.205 +
  65.206 +        self.assertEqual( ctx.isDirectory( FILENAME ), False )
  65.207 +
  65.208 +    def test_isDirectory_directory( self ):
  65.209 +
  65.210 +        from string import printable
  65.211 +
  65.212 +        SUBDIR = 'subdir'
  65.213 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.214 +        fqpath = self._makeFile( FILENAME, printable )
  65.215 +
  65.216 +        site = DummySite( 'site' ).__of__( self.root )
  65.217 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.218 +
  65.219 +        self.assertEqual( ctx.isDirectory( SUBDIR ), True )
  65.220 +
  65.221 +    def test_listDirectory_nonesuch( self ):
  65.222 +
  65.223 +        FILENAME = 'nonesuch.txt'
  65.224 +
  65.225 +        site = DummySite( 'site' ).__of__( self.root )
  65.226 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.227 +
  65.228 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
  65.229 +
  65.230 +    def test_listDirectory_root( self ):
  65.231 +
  65.232 +        from string import printable
  65.233 +
  65.234 +        site = DummySite( 'site' ).__of__( self.root )
  65.235 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.236 +
  65.237 +        FILENAME = 'simple.txt'
  65.238 +        self._makeFile( FILENAME, printable )
  65.239 +
  65.240 +        self.assertEqual( len( ctx.listDirectory( None ) ), 1 )
  65.241 +        self.failUnless( FILENAME in ctx.listDirectory( None ) )
  65.242 +
  65.243 +    def test_listDirectory_simple( self ):
  65.244 +
  65.245 +        from string import printable
  65.246 +
  65.247 +        FILENAME = 'simple.txt'
  65.248 +        self._makeFile( FILENAME, printable )
  65.249 +
  65.250 +        site = DummySite( 'site' ).__of__( self.root )
  65.251 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.252 +
  65.253 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
  65.254 +
  65.255 +    def test_listDirectory_nested( self ):
  65.256 +
  65.257 +        from string import printable
  65.258 +
  65.259 +        SUBDIR = 'subdir'
  65.260 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.261 +        self._makeFile( FILENAME, printable )
  65.262 +
  65.263 +        site = DummySite( 'site' ).__of__( self.root )
  65.264 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.265 +
  65.266 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
  65.267 +
  65.268 +    def test_listDirectory_single( self ):
  65.269 +
  65.270 +        from string import printable
  65.271 +
  65.272 +        SUBDIR = 'subdir'
  65.273 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.274 +        self._makeFile( FILENAME, printable )
  65.275 +
  65.276 +        site = DummySite( 'site' ).__of__( self.root )
  65.277 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.278 +
  65.279 +        names = ctx.listDirectory( SUBDIR )
  65.280 +        self.assertEqual( len( names ), 1 )
  65.281 +        self.failUnless( 'nested.txt' in names )
  65.282 +
  65.283 +    def test_listDirectory_multiple( self ):
  65.284 +
  65.285 +        from string import printable
  65.286 +        SUBDIR = 'subdir'
  65.287 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.288 +        self._makeFile( FILENAME, printable )
  65.289 +        self._makeFile( os.path.join( SUBDIR, 'another.txt' ), 'ABC' )
  65.290 +
  65.291 +        site = DummySite( 'site' ).__of__( self.root )
  65.292 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.293 +
  65.294 +        names = ctx.listDirectory( SUBDIR )
  65.295 +        self.assertEqual( len( names ), 2 )
  65.296 +        self.failUnless( 'nested.txt' in names )
  65.297 +        self.failUnless( 'another.txt' in names )
  65.298 +
  65.299 +    def test_listDirectory_skip_implicit( self ):
  65.300 +
  65.301 +        from string import printable
  65.302 +        SUBDIR = 'subdir'
  65.303 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.304 +        self._makeFile( FILENAME, printable )
  65.305 +        self._makeFile( os.path.join( SUBDIR, 'another.txt' ), 'ABC' )
  65.306 +        self._makeFile( os.path.join( SUBDIR, 'another.txt~' ), '123' )
  65.307 +        self._makeFile( os.path.join( SUBDIR, 'CVS/skip.txt' ), 'DEF' )
  65.308 +        self._makeFile( os.path.join( SUBDIR, '.svn/skip.txt' ), 'GHI' )
  65.309 +
  65.310 +        site = DummySite( 'site' ).__of__( self.root )
  65.311 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.312 +
  65.313 +        names = ctx.listDirectory( SUBDIR )
  65.314 +        self.assertEqual( len( names ), 2 )
  65.315 +        self.failUnless( 'nested.txt' in names )
  65.316 +        self.failUnless( 'another.txt' in names )
  65.317 +        self.failIf( 'another.txt~' in names )
  65.318 +        self.failIf( 'CVS' in names )
  65.319 +        self.failIf( '.svn' in names )
  65.320 +
  65.321 +    def test_listDirectory_skip_explicit( self ):
  65.322 +
  65.323 +        from string import printable
  65.324 +        SUBDIR = 'subdir'
  65.325 +        FILENAME = os.path.join( SUBDIR, 'nested.txt' )
  65.326 +        self._makeFile( FILENAME, printable )
  65.327 +        self._makeFile( os.path.join( SUBDIR, 'another.txt' ), 'ABC' )
  65.328 +        self._makeFile( os.path.join( SUBDIR, 'another.bak' ), '123' )
  65.329 +        self._makeFile( os.path.join( SUBDIR, 'CVS/skip.txt' ), 'DEF' )
  65.330 +        self._makeFile( os.path.join( SUBDIR, '.svn/skip.txt' ), 'GHI' )
  65.331 +
  65.332 +        site = DummySite( 'site' ).__of__( self.root )
  65.333 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.334 +
  65.335 +        names = ctx.listDirectory(SUBDIR, skip=('nested.txt',),
  65.336 +                                  skip_suffixes=('.bak',))
  65.337 +        self.assertEqual( len( names ), 3 )
  65.338 +        self.failIf( 'nested.txt' in names )
  65.339 +        self.failIf( 'nested.bak' in names )
  65.340 +        self.failUnless( 'another.txt' in names )
  65.341 +        self.failUnless( 'CVS' in names )
  65.342 +        self.failUnless( '.svn' in names )
  65.343 +
  65.344 +
  65.345 +class DirectoryExportContextTests( FilesystemTestBase
  65.346 +                                 , ConformsToISetupContext
  65.347 +                                 , ConformsToIExportContext
  65.348 +                                 ):
  65.349 +
  65.350 +    _PROFILE_PATH = '/tmp/ECTTexts'
  65.351 +
  65.352 +    def _getTargetClass( self ):
  65.353 +
  65.354 +        from Products.GenericSetup.context import DirectoryExportContext
  65.355 +        return DirectoryExportContext
  65.356 +
  65.357 +    def test_getLogger( self ):
  65.358 +
  65.359 +        site = DummySite( 'site' ).__of__( self.root )
  65.360 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.361 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  65.362 +
  65.363 +        logger = ctx.getLogger('foo')
  65.364 +        logger.info('bar')
  65.365 +
  65.366 +        self.assertEqual( len( ctx.listNotes() ), 1 )
  65.367 +        level, component, message = ctx.listNotes()[0]
  65.368 +        self.assertEqual( level, logging.INFO )
  65.369 +        self.assertEqual( component, 'foo' )
  65.370 +        self.assertEqual( message, 'bar' )
  65.371 +
  65.372 +        ctx.clearNotes()
  65.373 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  65.374 +
  65.375 +    def test_writeDataFile_simple( self ):
  65.376 +
  65.377 +        from string import printable, digits
  65.378 +        FILENAME = 'simple.txt'
  65.379 +        fqname = self._makeFile( FILENAME, printable )
  65.380 +
  65.381 +        site = DummySite( 'site' ).__of__( self.root )
  65.382 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.383 +
  65.384 +        ctx.writeDataFile( FILENAME, digits, 'text/plain' )
  65.385 +
  65.386 +        self.assertEqual( open( fqname, 'rb' ).read(), digits )
  65.387 +
  65.388 +    def test_writeDataFile_new_subdir( self ):
  65.389 +
  65.390 +        from string import printable, digits
  65.391 +        SUBDIR = 'subdir'
  65.392 +        FILENAME = 'nested.txt'
  65.393 +        fqname = os.path.join( self._PROFILE_PATH, SUBDIR, FILENAME )
  65.394 +
  65.395 +        site = DummySite( 'site' ).__of__( self.root )
  65.396 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.397 +
  65.398 +        ctx.writeDataFile( FILENAME, digits, 'text/plain', SUBDIR )
  65.399 +
  65.400 +        self.assertEqual( open( fqname, 'rb' ).read(), digits )
  65.401 +
  65.402 +    def test_writeDataFile_overwrite( self ):
  65.403 +
  65.404 +        from string import printable, digits
  65.405 +        SUBDIR = 'subdir'
  65.406 +        FILENAME = 'nested.txt'
  65.407 +        fqname = self._makeFile( os.path.join( SUBDIR, FILENAME )
  65.408 +                               , printable )
  65.409 +
  65.410 +        site = DummySite( 'site' ).__of__( self.root )
  65.411 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.412 +
  65.413 +        ctx.writeDataFile( FILENAME, digits, 'text/plain', SUBDIR )
  65.414 +
  65.415 +        self.assertEqual( open( fqname, 'rb' ).read(), digits )
  65.416 +
  65.417 +    def test_writeDataFile_existing_subdir( self ):
  65.418 +
  65.419 +        from string import printable, digits
  65.420 +        SUBDIR = 'subdir'
  65.421 +        FILENAME = 'nested.txt'
  65.422 +        self._makeFile( os.path.join( SUBDIR, 'another.txt' ), printable )
  65.423 +        fqname = os.path.join( self._PROFILE_PATH, SUBDIR, FILENAME )
  65.424 +
  65.425 +        site = DummySite( 'site' ).__of__( self.root )
  65.426 +        ctx = self._makeOne( site, self._PROFILE_PATH )
  65.427 +
  65.428 +        ctx.writeDataFile( FILENAME, digits, 'text/plain', SUBDIR )
  65.429 +
  65.430 +        self.assertEqual( open( fqname, 'rb' ).read(), digits )
  65.431 +
  65.432 +
  65.433 +class TarballImportContextTests( SecurityRequestTest
  65.434 +                               , ConformsToISetupContext
  65.435 +                               , ConformsToIImportContext
  65.436 +                               ):
  65.437 +
  65.438 +    def _getTargetClass( self ):
  65.439 +
  65.440 +        from Products.GenericSetup.context import TarballImportContext
  65.441 +        return TarballImportContext
  65.442 +
  65.443 +    def _makeOne( self, file_dict={}, mod_time=None, *args, **kw ):
  65.444 +
  65.445 +        archive_stream = StringIO()
  65.446 +        archive = TarFile.open('test.tar.gz', 'w:gz', archive_stream)
  65.447 +
  65.448 +        def _addOneMember(path, data, modtime):
  65.449 +            stream = StringIO(v)
  65.450 +            info = TarInfo(k)
  65.451 +            info.size = len(v)
  65.452 +            info.mtime = mod_time
  65.453 +            archive.addfile(info, stream)
  65.454 +
  65.455 +        def _addMember(path, data, modtime):
  65.456 +            from tarfile import DIRTYPE
  65.457 +            elements = path.split('/')
  65.458 +            parents = filter(None, [elements[x] for x in range(len(elements))])
  65.459 +            for parent in parents:
  65.460 +                info = TarInfo()
  65.461 +                info.name = parent
  65.462 +                info.size = 0
  65.463 +                info.mtime = mod_time
  65.464 +                info.type = DIRTYPE
  65.465 +                archive.addfile(info, StringIO())
  65.466 +            _addOneMember(path, data, modtime)
  65.467 +
  65.468 +        file_items = file_dict.items() or [('dummy', '')] # empty archive barfs
  65.469 +
  65.470 +        if mod_time is None:
  65.471 +            mod_time = time.time()
  65.472 +
  65.473 +        for k, v in file_items:
  65.474 +            _addMember(k, v, mod_time)
  65.475 +
  65.476 +        archive.close()
  65.477 +        bits = archive_stream.getvalue()
  65.478 +
  65.479 +        site = DummySite( 'site' ).__of__( self.root )
  65.480 +        site._setObject( 'setup_tool', Folder( 'setup_tool' ) )
  65.481 +        tool = site._getOb( 'setup_tool' )
  65.482 +
  65.483 +        ctx = self._getTargetClass()( tool, bits, *args, **kw )
  65.484 +
  65.485 +        return site, tool, ctx.__of__( tool )
  65.486 +
  65.487 +    def test_getLogger( self ):
  65.488 +
  65.489 +        site, tool, ctx = self._makeOne()
  65.490 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  65.491 +
  65.492 +        logger = ctx.getLogger('foo')
  65.493 +        logger.info('bar')
  65.494 +
  65.495 +        self.assertEqual( len( ctx.listNotes() ), 1 )
  65.496 +        level, component, message = ctx.listNotes()[0]
  65.497 +        self.assertEqual( level, logging.INFO )
  65.498 +        self.assertEqual( component, 'foo' )
  65.499 +        self.assertEqual( message, 'bar' )
  65.500 +
  65.501 +        ctx.clearNotes()
  65.502 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  65.503 +
  65.504 +    def test_ctorparms( self ):
  65.505 +
  65.506 +        ENCODING = 'latin-1'
  65.507 +        site, tool, ctx = self._makeOne( encoding=ENCODING
  65.508 +                                       , should_purge=True
  65.509 +                                       )
  65.510 +
  65.511 +        self.assertEqual( ctx.getEncoding(), ENCODING )
  65.512 +        self.assertEqual( ctx.shouldPurge(), True )
  65.513 +
  65.514 +    def test_empty( self ):
  65.515 +
  65.516 +        site, tool, ctx = self._makeOne()
  65.517 +
  65.518 +        self.assertEqual( ctx.getSite(), site )
  65.519 +        self.assertEqual( ctx.getEncoding(), None )
  65.520 +        self.assertEqual( ctx.shouldPurge(), False )
  65.521 +
  65.522 +        # These methods are all specified to return 'None' for non-existing
  65.523 +        # paths / entities
  65.524 +        self.assertEqual( ctx.isDirectory( 'nonesuch/path' ), None )
  65.525 +        self.assertEqual( ctx.listDirectory( 'nonesuch/path' ), None )
  65.526 +
  65.527 +    def test_readDataFile_nonesuch( self ):
  65.528 +
  65.529 +        FILENAME = 'nonesuch.txt'
  65.530 +
  65.531 +        site, tool, ctx = self._makeOne()
  65.532 +
  65.533 +        self.assertEqual( ctx.readDataFile( FILENAME ), None )
  65.534 +        self.assertEqual( ctx.readDataFile( FILENAME, 'subdir' ), None )
  65.535 +
  65.536 +    def test_readDataFile_simple( self ):
  65.537 +
  65.538 +        from string import printable
  65.539 +
  65.540 +        FILENAME = 'simple.txt'
  65.541 +
  65.542 +        site, tool, ctx = self._makeOne( { FILENAME: printable } )
  65.543 +
  65.544 +        self.assertEqual( ctx.readDataFile( FILENAME ), printable )
  65.545 +
  65.546 +    def test_readDataFile_subdir( self ):
  65.547 +
  65.548 +        from string import printable
  65.549 +
  65.550 +        FILENAME = 'subdir.txt'
  65.551 +        SUBDIR = 'subdir'
  65.552 +
  65.553 +        site, tool, ctx = self._makeOne( { '%s/%s' % (SUBDIR, FILENAME):
  65.554 +                                            printable } )
  65.555 +
  65.556 +        self.assertEqual( ctx.readDataFile( FILENAME, SUBDIR ), printable )
  65.557 +
  65.558 +    def test_getLastModified_nonesuch( self ):
  65.559 +
  65.560 +        FILENAME = 'nonesuch.txt'
  65.561 +
  65.562 +        site, tool, ctx = self._makeOne()
  65.563 +
  65.564 +        self.assertEqual( ctx.getLastModified( FILENAME ), None )
  65.565 +
  65.566 +    def test_getLastModified_simple( self ):
  65.567 +
  65.568 +        from string import printable
  65.569 +
  65.570 +        FILENAME = 'simple.txt'
  65.571 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
  65.572 +
  65.573 +        site, tool, ctx = self._makeOne( { FILENAME : printable }
  65.574 +                                       , mod_time=WHEN )
  65.575 +
  65.576 +        self.assertEqual( ctx.getLastModified( FILENAME ), WHEN )
  65.577 +
  65.578 +    def test_getLastModified_subdir( self ):
  65.579 +
  65.580 +        from string import printable
  65.581 +
  65.582 +        FILENAME = 'subdir.txt'
  65.583 +        SUBDIR = 'subdir'
  65.584 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  65.585 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
  65.586 +
  65.587 +        site, tool, ctx = self._makeOne( { PATH: printable }
  65.588 +                                       , mod_time=WHEN )
  65.589 +
  65.590 +        self.assertEqual( ctx.getLastModified( PATH ), WHEN )
  65.591 +
  65.592 +    def test_getLastModified_directory( self ):
  65.593 +
  65.594 +        from string import printable
  65.595 +
  65.596 +        FILENAME = 'subdir.txt'
  65.597 +        SUBDIR = 'subdir'
  65.598 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  65.599 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
  65.600 +
  65.601 +        site, tool, ctx = self._makeOne( { PATH: printable }
  65.602 +                                       , mod_time=WHEN
  65.603 +                                       )
  65.604 +
  65.605 +        self.assertEqual( ctx.getLastModified( SUBDIR ), WHEN )
  65.606 +
  65.607 +    def test_isDirectory_nonesuch( self ):
  65.608 +
  65.609 +        FILENAME = 'nonesuch.txt'
  65.610 +
  65.611 +        site, tool, ctx = self._makeOne()
  65.612 +
  65.613 +        self.assertEqual( ctx.isDirectory( FILENAME ), None )
  65.614 +
  65.615 +    def test_isDirectory_simple( self ):
  65.616 +
  65.617 +        from string import printable
  65.618 +
  65.619 +        FILENAME = 'simple.txt'
  65.620 +
  65.621 +        site, tool, ctx = self._makeOne( { FILENAME: printable } )
  65.622 +
  65.623 +        self.assertEqual( ctx.isDirectory( FILENAME ), False )
  65.624 +
  65.625 +    def test_isDirectory_nested( self ):
  65.626 +
  65.627 +        from string import printable
  65.628 +
  65.629 +        SUBDIR = 'subdir'
  65.630 +        FILENAME = 'nested.txt'
  65.631 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  65.632 +
  65.633 +        site, tool, ctx = self._makeOne( { PATH: printable } )
  65.634 +
  65.635 +        self.assertEqual( ctx.isDirectory( PATH ), False )
  65.636 +
  65.637 +    def test_isDirectory_subdir( self ):
  65.638 +
  65.639 +        from string import printable
  65.640 +
  65.641 +        SUBDIR = 'subdir'
  65.642 +        FILENAME = 'nested.txt'
  65.643 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  65.644 +
  65.645 +        site, tool, ctx = self._makeOne( { PATH: printable } )
  65.646 +
  65.647 +        self.assertEqual( ctx.isDirectory( SUBDIR ), True )
  65.648 +
  65.649 +    def test_listDirectory_nonesuch( self ):
  65.650 +
  65.651 +        SUBDIR = 'nonesuch/path'
  65.652 +
  65.653 +        site, tool, ctx = self._makeOne()
  65.654 +
  65.655 +        self.assertEqual( ctx.listDirectory( SUBDIR ), None )
  65.656 +
  65.657 +    def test_listDirectory_root( self ):
  65.658 +
  65.659 +        from string import printable
  65.660 +
  65.661 +        FILENAME = 'simple.txt'
  65.662 +
  65.663 +        site, tool, ctx = self._makeOne( { FILENAME: printable } )
  65.664 +
  65.665 +        self.assertEqual( len( ctx.listDirectory( None ) ), 1 )
  65.666 +        self.failUnless( FILENAME in ctx.listDirectory( None ) )
  65.667 +
  65.668 +    def test_listDirectory_simple( self ):
  65.669 +
  65.670 +        from string import printable
  65.671 +
  65.672 +        FILENAME = 'simple.txt'
  65.673 +
  65.674 +        site, tool, ctx = self._makeOne( { FILENAME: printable } )
  65.675 +
  65.676 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
  65.677 +
  65.678 +    def test_listDirectory_nested( self ):
  65.679 +
  65.680 +        from string import printable
  65.681 +
  65.682 +        SUBDIR = 'subdir'
  65.683 +        FILENAME = 'nested.txt'
  65.684 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  65.685 +
  65.686 +        site, tool, ctx = self._makeOne( { PATH: printable } )
  65.687 +
  65.688 +        self.assertEqual( ctx.listDirectory( PATH ), None )
  65.689 +
  65.690 +    def test_listDirectory_single( self ):
  65.691 +
  65.692 +        from string import printable
  65.693 +
  65.694 +        SUBDIR = 'subdir'
  65.695 +        FILENAME = 'nested.txt'
  65.696 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
  65.697 +
  65.698 +        site, tool, ctx = self._makeOne( { PATH: printable } )
  65.699 +
  65.700 +        names = ctx.listDirectory( SUBDIR )
  65.701 +        self.assertEqual( len( names ), 1 )
  65.702 +        self.failUnless( FILENAME in names )
  65.703 +
  65.704 +    def test_listDirectory_multiple( self ):
  65.705 +
  65.706 +        from string import printable, uppercase
  65.707 +
  65.708 +        SUBDIR = 'subdir'
  65.709 +        FILENAME1 = 'nested.txt'
  65.710 +        PATH1 = '%s/%s' % ( SUBDIR, FILENAME1 )
  65.711 +        FILENAME2 = 'another.txt'
  65.712 +        PATH2 = '%s/%s' % ( SUBDIR, FILENAME2 )
  65.713 +
  65.714 +        site, tool, ctx = self._makeOne( { PATH1: printable
  65.715 +                                         , PATH2: uppercase
  65.716 +                                         } )
  65.717 +                                             
  65.718 +        names = ctx.listDirectory( SUBDIR )
  65.719 +        self.assertEqual( len( names ), 2 )
  65.720 +        self.failUnless( FILENAME1 in names )
  65.721 +        self.failUnless( FILENAME2 in names )
  65.722 +
  65.723 +    def test_listDirectory_skip( self ):
  65.724 +
  65.725 +        from string import printable, uppercase
  65.726 +
  65.727 +        SUBDIR = 'subdir'
  65.728 +        FILENAME1 = 'nested.txt'
  65.729 +        PATH1 = '%s/%s' % ( SUBDIR, FILENAME1 )
  65.730 +        FILENAME2 = 'another.txt'
  65.731 +        PATH2 = '%s/%s' % ( SUBDIR, FILENAME2 )
  65.732 +        FILENAME3 = 'another.bak'
  65.733 +        PATH3 = '%s/%s' % ( SUBDIR, FILENAME3 )
  65.734 +
  65.735 +        site, tool, ctx = self._makeOne( { PATH1: printable
  65.736 +                                         , PATH2: uppercase
  65.737 +                                         , PATH3: 'xyz'
  65.738 +                                         } )
  65.739 +
  65.740 +        names = ctx.listDirectory(SUBDIR, skip=(FILENAME1,),
  65.741 +                                  skip_suffixes=('.bak',))
  65.742 +        self.assertEqual( len( names ), 1 )
  65.743 +        self.failIf( FILENAME1 in names )
  65.744 +        self.failUnless( FILENAME2 in names )
  65.745 +        self.failIf( FILENAME3 in names )
  65.746 +
  65.747 +
  65.748 +class TarballExportContextTests( FilesystemTestBase
  65.749 +                               , TarballTester
  65.750 +                               , ConformsToISetupContext
  65.751 +                               , ConformsToIExportContext
  65.752 +                               ):
  65.753 +
  65.754 +    _PROFILE_PATH = '/tmp/TECT_tests'
  65.755 +
  65.756 +    def _getTargetClass( self ):
  65.757 +
  65.758 +        from Products.GenericSetup.context import TarballExportContext
  65.759 +        return TarballExportContext
  65.760 +
  65.761 +    def test_getLogger( self ):
  65.762 +
  65.763 +        site = DummySite( 'site' ).__of__( self.root )
  65.764 +        ctx = self._getTargetClass()( site )
  65.765 +
  65.766 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  65.767 +
  65.768 +        logger = ctx.getLogger('foo')
  65.769 +        logger.info('bar')
  65.770 +
  65.771 +        self.assertEqual( len( ctx.listNotes() ), 1 )
  65.772 +        level, component, message = ctx.listNotes()[0]
  65.773 +        self.assertEqual( level, logging.INFO )
  65.774 +        self.assertEqual( component, 'foo' )
  65.775 +        self.assertEqual( message, 'bar' )
  65.776 +
  65.777 +        ctx.clearNotes()
  65.778 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  65.779 +
  65.780 +    def test_writeDataFile_simple( self ):
  65.781 +
  65.782 +        from string import printable
  65.783 +        now = long( time.time() )
  65.784 +
  65.785 +        site = DummySite( 'site' ).__of__( self.root )
  65.786 +        ctx = self._getTargetClass()( site )
  65.787 +
  65.788 +        ctx.writeDataFile( 'foo.txt', printable, 'text/plain' )
  65.789 +
  65.790 +        fileish = StringIO( ctx.getArchive() )
  65.791 +
  65.792 +        self._verifyTarballContents( fileish, [ 'foo.txt' ], now )
  65.793 +        self._verifyTarballEntry( fileish, 'foo.txt', printable )
  65.794 +
  65.795 +    def test_writeDataFile_multiple( self ):
  65.796 +
  65.797 +        from string import printable
  65.798 +        from string import digits
  65.799 +
  65.800 +        site = DummySite( 'site' ).__of__( self.root )
  65.801 +        ctx = self._getTargetClass()( site )
  65.802 +
  65.803 +        ctx.writeDataFile( 'foo.txt', printable, 'text/plain' )
  65.804 +        ctx.writeDataFile( 'bar.txt', digits, 'text/plain' )
  65.805 +
  65.806 +        fileish = StringIO( ctx.getArchive() )
  65.807 +
  65.808 +        self._verifyTarballContents( fileish, [ 'foo.txt', 'bar.txt' ] )
  65.809 +        self._verifyTarballEntry( fileish, 'foo.txt', printable )
  65.810 +        self._verifyTarballEntry( fileish, 'bar.txt', digits )
  65.811 +
  65.812 +    def test_writeDataFile_subdir( self ):
  65.813 +
  65.814 +        from string import printable
  65.815 +        from string import digits
  65.816 +
  65.817 +        site = DummySite( 'site' ).__of__( self.root )
  65.818 +        ctx = self._getTargetClass()( site )
  65.819 +
  65.820 +        ctx.writeDataFile( 'foo.txt', printable, 'text/plain' )
  65.821 +        ctx.writeDataFile( 'bar/baz.txt', digits, 'text/plain' )
  65.822 +
  65.823 +        fileish = StringIO( ctx.getArchive() )
  65.824 +
  65.825 +        self._verifyTarballContents( fileish, [ 'foo.txt', 'bar/baz.txt' ] )
  65.826 +        self._verifyTarballEntry( fileish, 'foo.txt', printable )
  65.827 +        self._verifyTarballEntry( fileish, 'bar/baz.txt', digits )
  65.828 +
  65.829 +
  65.830 +class SnapshotExportContextTests( SecurityRequestTest
  65.831 +                                , ConformsToISetupContext
  65.832 +                                , ConformsToIExportContext
  65.833 +                                ):
  65.834 +
  65.835 +    def _getTargetClass( self ):
  65.836 +
  65.837 +        from Products.GenericSetup.context import SnapshotExportContext
  65.838 +        return SnapshotExportContext
  65.839 +
  65.840 +    def _makeOne( self, *args, **kw ):
  65.841 +
  65.842 +        return self._getTargetClass()( *args, **kw )
  65.843 +
  65.844 +    def test_getLogger( self ):
  65.845 +
  65.846 +        site = DummySite( 'site' ).__of__( self.root )
  65.847 +        site.setup_tool = DummyTool( 'setup_tool' )
  65.848 +        tool = site.setup_tool
  65.849 +        ctx = self._makeOne( tool, 'simple' )
  65.850 +
  65.851 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  65.852 +
  65.853 +        logger = ctx.getLogger('foo')
  65.854 +        logger.info('bar')
  65.855 +
  65.856 +        self.assertEqual( len( ctx.listNotes() ), 1 )
  65.857 +        level, component, message = ctx.listNotes()[0]
  65.858 +        self.assertEqual( level, logging.INFO )
  65.859 +        self.assertEqual( component, 'foo' )
  65.860 +        self.assertEqual( message, 'bar' )
  65.861 +
  65.862 +        ctx.clearNotes()
  65.863 +        self.assertEqual( len( ctx.listNotes() ), 0 )
  65.864 +
  65.865 +    def test_writeDataFile_simple_image( self ):
  65.866 +
  65.867 +        from OFS.Image import Image
  65.868 +        FILENAME = 'simple.txt'
  65.869 +        CONTENT_TYPE = 'image/png'
  65.870 +        png_filename = os.path.join( os.path.split( __file__ )[0]
  65.871 +                                   , 'simple.png' )
  65.872 +        png_file = open( png_filename, 'rb' )
  65.873 +        png_data = png_file.read()
  65.874 +        png_file.close()
  65.875 +
  65.876 +        site = DummySite( 'site' ).__of__( self.root )
  65.877 +        site.setup_tool = DummyTool( 'setup_tool' )
  65.878 +        tool = site.setup_tool
  65.879 +        ctx = self._makeOne( tool, 'simple' )
  65.880 +
  65.881 +        ctx.writeDataFile( FILENAME, png_data, CONTENT_TYPE )
  65.882 +
  65.883 +        snapshot = tool.snapshots._getOb( 'simple' )
  65.884 +
  65.885 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  65.886 +        self.failUnless( FILENAME in snapshot.objectIds() )
  65.887 +
  65.888 +        fileobj = snapshot._getOb( FILENAME )
  65.889 +
  65.890 +        self.assertEqual( fileobj.getId(), FILENAME )
  65.891 +        self.assertEqual( fileobj.meta_type, Image.meta_type )
  65.892 +        self.assertEqual( fileobj.getContentType(), CONTENT_TYPE )
  65.893 +        self.assertEqual( fileobj.data, png_data )
  65.894 +
  65.895 +    def test_writeDataFile_simple_plain_text( self ):
  65.896 +
  65.897 +        from string import digits
  65.898 +        from OFS.Image import File
  65.899 +        FILENAME = 'simple.txt'
  65.900 +        CONTENT_TYPE = 'text/plain'
  65.901 +
  65.902 +        site = DummySite( 'site' ).__of__( self.root )
  65.903 +        site.setup_tool = DummyTool( 'setup_tool' )
  65.904 +        tool = site.setup_tool
  65.905 +        ctx = self._makeOne( tool, 'simple' )
  65.906 +
  65.907 +        ctx.writeDataFile( FILENAME, digits, CONTENT_TYPE )
  65.908 +
  65.909 +        snapshot = tool.snapshots._getOb( 'simple' )
  65.910 +
  65.911 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  65.912 +        self.failUnless( FILENAME in snapshot.objectIds() )
  65.913 +
  65.914 +        fileobj = snapshot._getOb( FILENAME )
  65.915 +
  65.916 +        self.assertEqual( fileobj.getId(), FILENAME )
  65.917 +        self.assertEqual( fileobj.meta_type, File.meta_type )
  65.918 +        self.assertEqual( fileobj.getContentType(), CONTENT_TYPE )
  65.919 +        self.assertEqual( str( fileobj ), digits )
  65.920 +
  65.921 +    def test_writeDataFile_simple_plain_text_unicode( self ):
  65.922 +
  65.923 +        from string import digits
  65.924 +        from OFS.Image import File
  65.925 +        FILENAME = 'simple.txt'
  65.926 +        CONTENT_TYPE = 'text/plain'
  65.927 +        CONTENT = u'Unicode, with non-ASCII: %s.' % unichr(150)
  65.928 +
  65.929 +        site = DummySite( 'site' ).__of__( self.root )
  65.930 +        site.setup_tool = DummyTool( 'setup_tool' )
  65.931 +        tool = site.setup_tool
  65.932 +        ctx = self._makeOne( tool, 'simple', 'latin_1' )
  65.933 +
  65.934 +        ctx.writeDataFile( FILENAME, CONTENT, CONTENT_TYPE )
  65.935 +
  65.936 +        snapshot = tool.snapshots._getOb( 'simple' )
  65.937 +
  65.938 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  65.939 +        self.failUnless( FILENAME in snapshot.objectIds() )
  65.940 +
  65.941 +        fileobj = snapshot._getOb( FILENAME )
  65.942 +
  65.943 +        self.assertEqual( fileobj.getId(), FILENAME )
  65.944 +        self.assertEqual( fileobj.meta_type, File.meta_type )
  65.945 +        self.assertEqual( fileobj.getContentType(), CONTENT_TYPE )
  65.946 +        saved = fileobj.manage_FTPget().decode('latin_1')
  65.947 +        self.assertEqual( saved, CONTENT )
  65.948 +
  65.949 +    def test_writeDataFile_simple_xml( self ):
  65.950 +
  65.951 +        from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
  65.952 +        FILENAME = 'simple.xml'
  65.953 +        CONTENT_TYPE = 'text/xml'
  65.954 +        _XML = """<?xml version="1.0"?><simple />"""
  65.955 +
  65.956 +        site = DummySite( 'site' ).__of__( self.root )
  65.957 +        site.setup_tool = DummyTool( 'setup_tool' )
  65.958 +        tool = site.setup_tool
  65.959 +        ctx = self._makeOne( tool, 'simple' )
  65.960 +
  65.961 +        ctx.writeDataFile( FILENAME, _XML, CONTENT_TYPE )
  65.962 +
  65.963 +        snapshot = tool.snapshots._getOb( 'simple' )
  65.964 +
  65.965 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  65.966 +        self.failUnless( FILENAME in snapshot.objectIds() )
  65.967 +
  65.968 +        template = snapshot._getOb( FILENAME )
  65.969 +
  65.970 +        self.assertEqual( template.getId(), FILENAME )
  65.971 +        self.assertEqual( template.meta_type, ZopePageTemplate.meta_type )
  65.972 +        self.assertEqual( template.read(), _XML )
  65.973 +        self.failIf( template.html() )
  65.974 +
  65.975 +    def test_writeDataFile_unicode_xml( self ):
  65.976 +
  65.977 +        from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
  65.978 +        FILENAME = 'simple.xml'
  65.979 +        CONTENT_TYPE = 'text/xml'
  65.980 +        _XML = u"""<?xml version="1.0"?><simple />"""
  65.981 +
  65.982 +        site = DummySite( 'site' ).__of__( self.root )
  65.983 +        site.setup_tool = DummyTool( 'setup_tool' )
  65.984 +        tool = site.setup_tool
  65.985 +        ctx = self._makeOne( tool, 'simple' )
  65.986 +
  65.987 +        ctx.writeDataFile( FILENAME, _XML, CONTENT_TYPE )
  65.988 +
  65.989 +        snapshot = tool.snapshots._getOb( 'simple' )
  65.990 +
  65.991 +        self.assertEqual( len( snapshot.objectIds() ), 1 )
  65.992 +        self.failUnless( FILENAME in snapshot.objectIds() )
  65.993 +
  65.994 +        template = snapshot._getOb( FILENAME )
  65.995 +
  65.996 +        self.assertEqual( template.getId(), FILENAME )
  65.997 +        self.assertEqual( template.meta_type, ZopePageTemplate.meta_type )
  65.998 +        self.assertEqual( template.read(), _XML )
  65.999 +        self.failIf( template.html() )
 65.1000 +
 65.1001 +    def test_writeDataFile_subdir_dtml( self ):
 65.1002 +
 65.1003 +        from OFS.DTMLDocument import DTMLDocument
 65.1004 +        FILENAME = 'simple.dtml'
 65.1005 +        CONTENT_TYPE = 'text/html'
 65.1006 +        _HTML = """<html><body><h1>HTML</h1></body></html>"""
 65.1007 +
 65.1008 +        site = DummySite( 'site' ).__of__( self.root )
 65.1009 +        site.setup_tool = DummyTool( 'setup_tool' )
 65.1010 +        tool = site.setup_tool
 65.1011 +        ctx = self._makeOne( tool, 'simple' )
 65.1012 +
 65.1013 +        ctx.writeDataFile( FILENAME, _HTML, CONTENT_TYPE, 'sub1' )
 65.1014 +
 65.1015 +        snapshot = tool.snapshots._getOb( 'simple' )
 65.1016 +        sub1 = snapshot._getOb( 'sub1' )
 65.1017 +
 65.1018 +        self.assertEqual( len( sub1.objectIds() ), 1 )
 65.1019 +        self.failUnless( FILENAME in sub1.objectIds() )
 65.1020 +
 65.1021 +        template = sub1._getOb( FILENAME )
 65.1022 +
 65.1023 +        self.assertEqual( template.getId(), FILENAME )
 65.1024 +        self.assertEqual( template.meta_type, DTMLDocument.meta_type )
 65.1025 +        self.assertEqual( template.read(), _HTML )
 65.1026 +
 65.1027 +        ctx.writeDataFile( 'sub1/%s2' % FILENAME, _HTML, CONTENT_TYPE)
 65.1028 +        self.assertEqual( len( sub1.objectIds() ), 2 )
 65.1029 +
 65.1030 +    def test_writeDataFile_nested_subdirs_html( self ):
 65.1031 +
 65.1032 +        from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
 65.1033 +        FILENAME = 'simple.html'
 65.1034 +        CONTENT_TYPE = 'text/html'
 65.1035 +        _HTML = """<html><body><h1>HTML</h1></body></html>"""
 65.1036 +
 65.1037 +        site = DummySite( 'site' ).__of__( self.root )
 65.1038 +        site.setup_tool = DummyTool( 'setup_tool' )
 65.1039 +        tool = site.setup_tool
 65.1040 +        ctx = self._makeOne( tool, 'simple' )
 65.1041 +
 65.1042 +        ctx.writeDataFile( FILENAME, _HTML, CONTENT_TYPE, 'sub1/sub2' )
 65.1043 +
 65.1044 +        snapshot = tool.snapshots._getOb( 'simple' )
 65.1045 +        sub1 = snapshot._getOb( 'sub1' )
 65.1046 +        sub2 = sub1._getOb( 'sub2' )
 65.1047 +
 65.1048 +        self.assertEqual( len( sub2.objectIds() ), 1 )
 65.1049 +        self.failUnless( FILENAME in sub2.objectIds() )
 65.1050 +
 65.1051 +        template = sub2._getOb( FILENAME )
 65.1052 +
 65.1053 +        self.assertEqual( template.getId(), FILENAME )
 65.1054 +        self.assertEqual( template.meta_type, ZopePageTemplate.meta_type )
 65.1055 +        self.assertEqual( template.read(), _HTML )
 65.1056 +        self.failUnless( template.html() )
 65.1057 +
 65.1058 +    def test_writeDataFile_multiple( self ):
 65.1059 +
 65.1060 +        from string import printable
 65.1061 +        from string import digits
 65.1062 +
 65.1063 +        site = DummySite( 'site' ).__of__( self.root )
 65.1064 +        site.setup_tool = DummyTool( 'setup_tool' )
 65.1065 +        tool = site.setup_tool
 65.1066 +        ctx = self._makeOne( tool, 'multiple' )
 65.1067 +
 65.1068 +        ctx.writeDataFile( 'foo.txt', printable, 'text/plain' )
 65.1069 +        ctx.writeDataFile( 'bar.txt', digits, 'text/plain' )
 65.1070 +
 65.1071 +        snapshot = tool.snapshots._getOb( 'multiple' )
 65.1072 +
 65.1073 +        self.assertEqual( len( snapshot.objectIds() ), 2 )
 65.1074 +
 65.1075 +        for id in [ 'foo.txt', 'bar.txt' ]:
 65.1076 +            self.failUnless( id in snapshot.objectIds() )
 65.1077 +
 65.1078 +
 65.1079 +class SnapshotImportContextTests( SecurityRequestTest
 65.1080 +                                , ConformsToISetupContext
 65.1081 +                                , ConformsToIImportContext
 65.1082 +                                ):
 65.1083 +
 65.1084 +    def _getTargetClass( self ):
 65.1085 +
 65.1086 +        from Products.GenericSetup.context import SnapshotImportContext
 65.1087 +        return SnapshotImportContext
 65.1088 +
 65.1089 +    def _makeOne( self, context_id, *args, **kw ):
 65.1090 +
 65.1091 +        site = DummySite( 'site' ).__of__( self.root )
 65.1092 +        site._setObject( 'setup_tool', Folder( 'setup_tool' ) )
 65.1093 +        tool = site._getOb( 'setup_tool' )
 65.1094 +
 65.1095 +        tool._setObject( 'snapshots', Folder( 'snapshots' ) )
 65.1096 +        tool.snapshots._setObject( context_id, Folder( context_id ) )
 65.1097 +
 65.1098 +        ctx = self._getTargetClass()( tool, context_id, *args, **kw )
 65.1099 +
 65.1100 +        return site, tool, ctx.__of__( tool )
 65.1101 +
 65.1102 +    def _makeFile( self
 65.1103 +                 , tool
 65.1104 +                 , snapshot_id
 65.1105 +                 , filename
 65.1106 +                 , contents
 65.1107 +                 , content_type='text/plain'
 65.1108 +                 , mod_time=None
 65.1109 +                 , subdir=None
 65.1110 +                 ):
 65.1111 +
 65.1112 +        snapshots = tool._getOb( 'snapshots' )
 65.1113 +        folder = snapshot = snapshots._getOb( snapshot_id )
 65.1114 +
 65.1115 +        if subdir is not None:
 65.1116 +
 65.1117 +            for element in subdir.split( '/' ):
 65.1118 +
 65.1119 +                try:
 65.1120 +                    folder = folder._getOb( element )
 65.1121 +                except AttributeError:
 65.1122 +                    folder._setObject( element, Folder( element ) )
 65.1123 +                    folder = folder._getOb( element )
 65.1124 +
 65.1125 +        file = File( filename, '', contents, content_type )
 65.1126 +        folder._setObject( filename, file )
 65.1127 +
 65.1128 +        if mod_time is not None:
 65.1129 +
 65.1130 +            def __faux_mod_time():
 65.1131 +                return mod_time
 65.1132 +
 65.1133 +            folder.bobobase_modification_time = \
 65.1134 +            file.bobobase_modification_time = __faux_mod_time
 65.1135 +
 65.1136 +        return folder._getOb( filename )
 65.1137 +
 65.1138 +    def test_getLogger( self ):
 65.1139 +
 65.1140 +        SNAPSHOT_ID = 'note'
 65.1141 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1142 +
 65.1143 +        self.assertEqual( len( ctx.listNotes() ), 0 )
 65.1144 +
 65.1145 +        logger = ctx.getLogger('foo')
 65.1146 +        logger.info('bar')
 65.1147 +
 65.1148 +        self.assertEqual( len( ctx.listNotes() ), 1 )
 65.1149 +        level, component, message = ctx.listNotes()[0]
 65.1150 +        self.assertEqual( level, logging.INFO )
 65.1151 +        self.assertEqual( component, 'foo' )
 65.1152 +        self.assertEqual( message, 'bar' )
 65.1153 +
 65.1154 +        ctx.clearNotes()
 65.1155 +        self.assertEqual( len( ctx.listNotes() ), 0 )
 65.1156 +
 65.1157 +    def test_ctorparms( self ):
 65.1158 +
 65.1159 +        SNAPSHOT_ID = 'ctorparms'
 65.1160 +        ENCODING = 'latin-1'
 65.1161 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID
 65.1162 +                                       , encoding=ENCODING
 65.1163 +                                       , should_purge=True
 65.1164 +                                       )
 65.1165 +
 65.1166 +        self.assertEqual( ctx.getEncoding(), ENCODING )
 65.1167 +        self.assertEqual( ctx.shouldPurge(), True )
 65.1168 +
 65.1169 +    def test_empty( self ):
 65.1170 +
 65.1171 +        SNAPSHOT_ID = 'empty'
 65.1172 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1173 +
 65.1174 +        self.assertEqual( ctx.getSite(), site )
 65.1175 +        self.assertEqual( ctx.getEncoding(), None )
 65.1176 +        self.assertEqual( ctx.shouldPurge(), False )
 65.1177 +
 65.1178 +        # These methods are all specified to return 'None' for non-existing
 65.1179 +        # paths / entities
 65.1180 +        self.assertEqual( ctx.isDirectory( 'nonesuch/path' ), None )
 65.1181 +        self.assertEqual( ctx.listDirectory( 'nonesuch/path' ), None )
 65.1182 +
 65.1183 +    def test_readDataFile_nonesuch( self ):
 65.1184 +
 65.1185 +        SNAPSHOT_ID = 'readDataFile_nonesuch'
 65.1186 +        FILENAME = 'nonesuch.txt'
 65.1187 +
 65.1188 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1189 +
 65.1190 +        self.assertEqual( ctx.readDataFile( FILENAME ), None )
 65.1191 +        self.assertEqual( ctx.readDataFile( FILENAME, 'subdir' ), None )
 65.1192 +
 65.1193 +    def test_readDataFile_simple( self ):
 65.1194 +
 65.1195 +        from string import printable
 65.1196 +
 65.1197 +        SNAPSHOT_ID = 'readDataFile_simple'
 65.1198 +        FILENAME = 'simple.txt'
 65.1199 +
 65.1200 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1201 +        self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable )
 65.1202 +
 65.1203 +        self.assertEqual( ctx.readDataFile( FILENAME ), printable )
 65.1204 +
 65.1205 +    def test_readDataFile_subdir( self ):
 65.1206 +
 65.1207 +        from string import printable
 65.1208 +
 65.1209 +        SNAPSHOT_ID = 'readDataFile_subdir'
 65.1210 +        FILENAME = 'subdir.txt'
 65.1211 +        SUBDIR = 'subdir'
 65.1212 +
 65.1213 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1214 +        self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 65.1215 +                      , subdir=SUBDIR )
 65.1216 +
 65.1217 +        self.assertEqual( ctx.readDataFile( FILENAME, SUBDIR ), printable )
 65.1218 +        self.assertEqual( ctx.readDataFile( '%s/%s' % (SUBDIR, FILENAME) ),
 65.1219 +                                            printable )
 65.1220 +
 65.1221 +    def test_getLastModified_nonesuch( self ):
 65.1222 +
 65.1223 +        SNAPSHOT_ID = 'getLastModified_nonesuch'
 65.1224 +        FILENAME = 'nonesuch.txt'
 65.1225 +
 65.1226 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1227 +
 65.1228 +        self.assertEqual( ctx.getLastModified( FILENAME ), None )
 65.1229 +
 65.1230 +    def test_getLastModified_simple( self ):
 65.1231 +
 65.1232 +        from string import printable
 65.1233 +
 65.1234 +        SNAPSHOT_ID = 'getLastModified_simple'
 65.1235 +        FILENAME = 'simple.txt'
 65.1236 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
 65.1237 +
 65.1238 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1239 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 65.1240 +                             , mod_time=WHEN )
 65.1241 +
 65.1242 +        self.assertEqual( ctx.getLastModified( FILENAME ), WHEN )
 65.1243 +
 65.1244 +    def test_getLastModified_subdir( self ):
 65.1245 +
 65.1246 +        from string import printable
 65.1247 +
 65.1248 +        SNAPSHOT_ID = 'getLastModified_subdir'
 65.1249 +        FILENAME = 'subdir.txt'
 65.1250 +        SUBDIR = 'subdir'
 65.1251 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
 65.1252 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
 65.1253 +
 65.1254 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1255 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 65.1256 +                             , mod_time=WHEN, subdir=SUBDIR )
 65.1257 +
 65.1258 +        self.assertEqual( ctx.getLastModified( PATH ), WHEN )
 65.1259 +
 65.1260 +    def test_getLastModified_directory( self ):
 65.1261 +
 65.1262 +        from string import printable
 65.1263 +
 65.1264 +        SNAPSHOT_ID = 'readDataFile_subdir'
 65.1265 +        FILENAME = 'subdir.txt'
 65.1266 +        SUBDIR = 'subdir'
 65.1267 +        WHEN = DateTime( '2004-01-01T00:00:00Z' )
 65.1268 +
 65.1269 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1270 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 65.1271 +                             , mod_time=WHEN, subdir=SUBDIR )
 65.1272 +
 65.1273 +        self.assertEqual( ctx.getLastModified( SUBDIR ), WHEN )
 65.1274 +
 65.1275 +    def test_isDirectory_nonesuch( self ):
 65.1276 +
 65.1277 +        SNAPSHOT_ID = 'isDirectory_nonesuch'
 65.1278 +        FILENAME = 'nonesuch.txt'
 65.1279 +
 65.1280 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1281 +
 65.1282 +        self.assertEqual( ctx.isDirectory( FILENAME ), None )
 65.1283 +
 65.1284 +    def test_isDirectory_simple( self ):
 65.1285 +
 65.1286 +        from string import printable
 65.1287 +
 65.1288 +        SNAPSHOT_ID = 'isDirectory_simple'
 65.1289 +        FILENAME = 'simple.txt'
 65.1290 +
 65.1291 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1292 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable )
 65.1293 +
 65.1294 +        self.assertEqual( ctx.isDirectory( FILENAME ), False )
 65.1295 +
 65.1296 +    def test_isDirectory_nested( self ):
 65.1297 +
 65.1298 +        from string import printable
 65.1299 +
 65.1300 +        SNAPSHOT_ID = 'isDirectory_nested'
 65.1301 +        SUBDIR = 'subdir'
 65.1302 +        FILENAME = 'nested.txt'
 65.1303 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
 65.1304 +
 65.1305 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1306 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 65.1307 +                             , subdir=SUBDIR )
 65.1308 +
 65.1309 +        self.assertEqual( ctx.isDirectory( PATH ), False )
 65.1310 +
 65.1311 +    def test_isDirectory_subdir( self ):
 65.1312 +
 65.1313 +        from string import printable
 65.1314 +
 65.1315 +        SNAPSHOT_ID = 'isDirectory_subdir'
 65.1316 +        SUBDIR = 'subdir'
 65.1317 +        FILENAME = 'nested.txt'
 65.1318 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
 65.1319 +
 65.1320 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1321 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 65.1322 +                             , subdir=SUBDIR )
 65.1323 +
 65.1324 +        self.assertEqual( ctx.isDirectory( SUBDIR ), True )
 65.1325 +
 65.1326 +    def test_listDirectory_nonesuch( self ):
 65.1327 +
 65.1328 +        SNAPSHOT_ID = 'listDirectory_nonesuch'
 65.1329 +        SUBDIR = 'nonesuch/path'
 65.1330 +
 65.1331 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1332 +
 65.1333 +        self.assertEqual( ctx.listDirectory( SUBDIR ), None )
 65.1334 +
 65.1335 +    def test_listDirectory_root( self ):
 65.1336 +
 65.1337 +        from string import printable
 65.1338 +
 65.1339 +        SNAPSHOT_ID = 'listDirectory_root'
 65.1340 +        FILENAME = 'simple.txt'
 65.1341 +
 65.1342 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1343 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable )
 65.1344 +
 65.1345 +        self.assertEqual( len( ctx.listDirectory( None ) ), 1 )
 65.1346 +        self.failUnless( FILENAME in ctx.listDirectory( None ) )
 65.1347 +
 65.1348 +    def test_listDirectory_simple( self ):
 65.1349 +
 65.1350 +        from string import printable
 65.1351 +
 65.1352 +        SNAPSHOT_ID = 'listDirectory_simple'
 65.1353 +        FILENAME = 'simple.txt'
 65.1354 +
 65.1355 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1356 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable )
 65.1357 +
 65.1358 +        self.assertEqual( ctx.listDirectory( FILENAME ), None )
 65.1359 +
 65.1360 +    def test_listDirectory_nested( self ):
 65.1361 +
 65.1362 +        from string import printable
 65.1363 +
 65.1364 +        SNAPSHOT_ID = 'listDirectory_nested'
 65.1365 +        SUBDIR = 'subdir'
 65.1366 +        FILENAME = 'nested.txt'
 65.1367 +        PATH = '%s/%s' % ( SUBDIR, FILENAME )
 65.1368 +
 65.1369 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1370 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 65.1371 +                             , subdir=SUBDIR )
 65.1372 +
 65.1373 +        self.assertEqual( ctx.listDirectory( PATH ), None )
 65.1374 +
 65.1375 +    def test_listDirectory_single( self ):
 65.1376 +
 65.1377 +        from string import printable
 65.1378 +
 65.1379 +        SNAPSHOT_ID = 'listDirectory_nested'
 65.1380 +        SUBDIR = 'subdir'
 65.1381 +        FILENAME = 'nested.txt'
 65.1382 +
 65.1383 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1384 +        file = self._makeFile( tool, SNAPSHOT_ID, FILENAME, printable
 65.1385 +                             , subdir=SUBDIR )
 65.1386 +
 65.1387 +        names = ctx.listDirectory( SUBDIR )
 65.1388 +        self.assertEqual( len( names ), 1 )
 65.1389 +        self.failUnless( FILENAME in names )
 65.1390 +
 65.1391 +    def test_listDirectory_multiple( self ):
 65.1392 +
 65.1393 +        from string import printable, uppercase
 65.1394 +
 65.1395 +        SNAPSHOT_ID = 'listDirectory_nested'
 65.1396 +        SUBDIR = 'subdir'
 65.1397 +        FILENAME1 = 'nested.txt'
 65.1398 +        FILENAME2 = 'another.txt'
 65.1399 +
 65.1400 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1401 +        file1 = self._makeFile( tool, SNAPSHOT_ID, FILENAME1, printable
 65.1402 +                              , subdir=SUBDIR )
 65.1403 +        file2 = self._makeFile( tool, SNAPSHOT_ID, FILENAME2, uppercase
 65.1404 +                              , subdir=SUBDIR )
 65.1405 +
 65.1406 +        names = ctx.listDirectory( SUBDIR )
 65.1407 +        self.assertEqual( len( names ), 2 )
 65.1408 +        self.failUnless( FILENAME1 in names )
 65.1409 +        self.failUnless( FILENAME2 in names )
 65.1410 +
 65.1411 +    def test_listDirectory_skip( self ):
 65.1412 +
 65.1413 +        from string import printable, uppercase
 65.1414 +
 65.1415 +        SNAPSHOT_ID = 'listDirectory_nested'
 65.1416 +        SUBDIR = 'subdir'
 65.1417 +        FILENAME1 = 'nested.txt'
 65.1418 +        FILENAME2 = 'another.txt'
 65.1419 +        FILENAME3 = 'another.bak'
 65.1420 +
 65.1421 +        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
 65.1422 +        file1 = self._makeFile( tool, SNAPSHOT_ID, FILENAME1, printable
 65.1423 +                              , subdir=SUBDIR )
 65.1424 +        file2 = self._makeFile( tool, SNAPSHOT_ID, FILENAME2, uppercase
 65.1425 +                              , subdir=SUBDIR )
 65.1426 +        file3 = self._makeFile( tool, SNAPSHOT_ID, FILENAME3, 'abc'
 65.1427 +                              , subdir=SUBDIR )
 65.1428 +
 65.1429 +        names = ctx.listDirectory(SUBDIR, skip=(FILENAME1,),
 65.1430 +                                  skip_suffixes=('.bak',))
 65.1431 +        self.assertEqual( len( names ), 1 )
 65.1432 +        self.failIf( FILENAME1 in names )
 65.1433 +        self.failUnless( FILENAME2 in names )
 65.1434 +        self.failIf( FILENAME3 in names )
 65.1435 +
 65.1436 +
 65.1437 +def test_suite():
 65.1438 +    return unittest.TestSuite((
 65.1439 +        unittest.makeSuite( DirectoryImportContextTests ),
 65.1440 +        unittest.makeSuite( DirectoryExportContextTests ),
 65.1441 +        unittest.makeSuite( TarballImportContextTests ),
 65.1442 +        unittest.makeSuite( TarballExportContextTests ),
 65.1443 +        unittest.makeSuite( SnapshotExportContextTests ),
 65.1444 +        unittest.makeSuite( SnapshotImportContextTests ),
 65.1445 +        ))
 65.1446 +
 65.1447 +if __name__ == '__main__':
 65.1448 +    unittest.main(defaultTest='test_suite')
    66.1 new file mode 100644
    66.2 --- /dev/null
    66.3 +++ b/tests/test_differ.py
    66.4 @@ -0,0 +1,406 @@
    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 +""" Unit tests for differ module.
   66.18 +
   66.19 +$Id: test_differ.py 38578 2005-09-24 09:34:34Z 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 OFS.Image import File
   66.29 +
   66.30 +from DateTime.DateTime import DateTime
   66.31 +
   66.32 +from common import SecurityRequestTest
   66.33 +
   66.34 +class DummySite( Folder ):
   66.35 +
   66.36 +    pass
   66.37 +
   66.38 +
   66.39 +class Test_unidiff( unittest.TestCase ):
   66.40 +
   66.41 +    def test_unidiff_both_text( self ):
   66.42 +
   66.43 +        from Products.GenericSetup.differ import unidiff
   66.44 +
   66.45 +        diff_lines = unidiff( ONE_FOUR, ZERO_FOUR )
   66.46 +        diff_text = '\n'.join( diff_lines )
   66.47 +        self.assertEqual( diff_text, DIFF_TEXT )
   66.48 +
   66.49 +    def test_unidiff_both_lines( self ):
   66.50 +
   66.51 +        from Products.GenericSetup.differ import unidiff
   66.52 +
   66.53 +        diff_lines = unidiff( ONE_FOUR.splitlines(), ZERO_FOUR.splitlines() )
   66.54 +        diff_text = '\n'.join( diff_lines )
   66.55 +        self.assertEqual( diff_text, DIFF_TEXT )
   66.56 +
   66.57 +    def test_unidiff_mixed( self ):
   66.58 +
   66.59 +        from Products.GenericSetup.differ import unidiff
   66.60 +
   66.61 +        diff_lines = unidiff( ONE_FOUR, ZERO_FOUR.splitlines() )
   66.62 +        diff_text = '\n'.join( diff_lines )
   66.63 +        self.assertEqual( diff_text, DIFF_TEXT )
   66.64 +
   66.65 +    def test_unidiff_ignore_blanks( self ):
   66.66 +
   66.67 +        from Products.GenericSetup.differ import unidiff
   66.68 +
   66.69 +        double_spaced = ONE_FOUR.replace( '\n', '\n\n' )
   66.70 +        diff_lines = unidiff( double_spaced
   66.71 +                            , ZERO_FOUR.splitlines()
   66.72 +                            , ignore_blanks=True
   66.73 +                            )
   66.74 +
   66.75 +        diff_text = '\n'.join( diff_lines )
   66.76 +        self.assertEqual( diff_text, DIFF_TEXT )
   66.77 +
   66.78 +ZERO_FOUR = """\
   66.79 +zero
   66.80 +one
   66.81 +tree
   66.82 +four
   66.83 +"""
   66.84 +
   66.85 +ONE_FOUR = """\
   66.86 +one
   66.87 +two
   66.88 +three
   66.89 +four
   66.90 +"""
   66.91 +
   66.92 +DIFF_TEXT = """\
   66.93 +--- original None
   66.94 ++++ modified None
   66.95 +@@ -1,4 +1,4 @@
   66.96 ++zero
   66.97 + one
   66.98 +-two
   66.99 +-three
  66.100 ++tree
  66.101 + four\
  66.102 +"""
  66.103 +
  66.104 +class ConfigDiffTests( SecurityRequestTest ):
  66.105 +
  66.106 +    site = None
  66.107 +    tool = None
  66.108 +
  66.109 +    def _getTargetClass( self ):
  66.110 +
  66.111 +        from Products.GenericSetup.differ import ConfigDiff
  66.112 +        return ConfigDiff
  66.113 +
  66.114 +    def _makeOne( self, lhs, rhs, *args, **kw ):
  66.115 +
  66.116 +        return self._getTargetClass()( lhs, rhs, *args, **kw )
  66.117 +
  66.118 +    def _makeSite( self ):
  66.119 +
  66.120 +        if self.site is not None:
  66.121 +            return
  66.122 +
  66.123 +        site = self.site = DummySite( 'site' ).__of__( self.root )
  66.124 +        site._setObject( 'setup_tool', Folder( 'setup_tool' ) )
  66.125 +        self.tool = tool = site._getOb( 'setup_tool' )
  66.126 +
  66.127 +        tool._setObject( 'snapshots', Folder( 'snapshots' ) )
  66.128 +
  66.129 +    def _makeContext( self, context_id ):
  66.130 +
  66.131 +        from Products.GenericSetup.context import SnapshotImportContext
  66.132 +
  66.133 +        self._makeSite()
  66.134 +
  66.135 +        if context_id not in self.tool.snapshots.objectIds():
  66.136 +            self.tool.snapshots._setObject( context_id, Folder( context_id ) )
  66.137 +
  66.138 +        ctx = SnapshotImportContext( self.tool, context_id )
  66.139 +
  66.140 +        return ctx.__of__( self.tool )
  66.141 +
  66.142 +    def _makeDirectory( self, snapshot_id, subdir ):
  66.143 +
  66.144 +        self._makeSite()
  66.145 +        folder = self.tool.snapshots._getOb( snapshot_id )
  66.146 +
  66.147 +        for element in subdir.split( '/' ):
  66.148 +
  66.149 +            try:
  66.150 +                folder = folder._getOb( element )
  66.151 +            except AttributeError:
  66.152 +                folder._setObject( element, Folder( element ) )
  66.153 +                folder = folder._getOb( element )
  66.154 +
  66.155 +        return folder
  66.156 +
  66.157 +    def _makeFile( self
  66.158 +                 , snapshot_id
  66.159 +                 , filename
  66.160 +                 , contents
  66.161 +                 , content_type='text/plain'
  66.162 +                 , mod_time=None
  66.163 +                 , subdir=None
  66.164 +                 ):
  66.165 +
  66.166 +        self._makeSite()
  66.167 +        snapshots = self.tool.snapshots
  66.168 +        snapshot = snapshots._getOb( snapshot_id )
  66.169 +
  66.170 +        if subdir is not None:
  66.171 +            folder = self._makeDirectory( snapshot_id, subdir )
  66.172 +        else:
  66.173 +            folder = snapshot
  66.174 +
  66.175 +        file = File( filename, '', contents, content_type )
  66.176 +        folder._setObject( filename, file )
  66.177 +
  66.178 +        if mod_time is not None:
  66.179 +
  66.180 +            def __faux_mod_time():
  66.181 +                return mod_time
  66.182 +
  66.183 +            folder.bobobase_modification_time = \
  66.184 +            file.bobobase_modification_time = __faux_mod_time
  66.185 +
  66.186 +        return folder._getOb( filename )
  66.187 +
  66.188 +    def test_compare_empties( self ):
  66.189 +
  66.190 +        lhs = self._makeContext( 'lhs' )
  66.191 +        rhs = self._makeContext( 'rhs' )
  66.192 +
  66.193 +        cd = self._makeOne( lhs, rhs )
  66.194 +
  66.195 +        diffs = cd.compare()
  66.196 +
  66.197 +        self.assertEqual( diffs, '' )
  66.198 +
  66.199 +    def test_compare_identical( self ):
  66.200 +
  66.201 +        lhs = self._makeContext( 'lhs' )
  66.202 +        rhs = self._makeContext( 'rhs' )
  66.203 +
  66.204 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF' )
  66.205 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub' )
  66.206 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF' )
  66.207 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub' )
  66.208 +
  66.209 +        cd = self._makeOne( lhs, rhs )
  66.210 +
  66.211 +        diffs = cd.compare()
  66.212 +
  66.213 +        self.assertEqual( diffs, '' )
  66.214 +
  66.215 +    def test_compare_changed_file( self ):
  66.216 +
  66.217 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  66.218 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  66.219 +
  66.220 +        lhs = self._makeContext( 'lhs' )
  66.221 +        rhs = self._makeContext( 'rhs' )
  66.222 +
  66.223 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ', mod_time=BEFORE )
  66.224 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub' )
  66.225 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nQRST', mod_time=AFTER )
  66.226 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub' )
  66.227 +
  66.228 +        cd = self._makeOne( lhs, rhs )
  66.229 +
  66.230 +        diffs = cd.compare()
  66.231 +
  66.232 +        self.assertEqual( diffs, TEST_TXT_DIFFS % ( BEFORE, AFTER ) )
  66.233 +
  66.234 +    def test_compare_changed_file_ignore_blanks( self ):
  66.235 +
  66.236 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  66.237 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  66.238 +
  66.239 +        lhs = self._makeContext( 'lhs' )
  66.240 +        rhs = self._makeContext( 'rhs' )
  66.241 +
  66.242 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ', mod_time=BEFORE )
  66.243 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\n\n\nWXYZ', mod_time=AFTER )
  66.244 +
  66.245 +        cd = self._makeOne( lhs, rhs, ignore_blanks=True )
  66.246 +
  66.247 +        diffs = cd.compare()
  66.248 +
  66.249 +        self.assertEqual( diffs, '' )
  66.250 +
  66.251 +    def test_compare_changed_file_explicit_skip( self ):
  66.252 +
  66.253 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  66.254 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  66.255 +
  66.256 +        lhs = self._makeContext( 'lhs' )
  66.257 +        rhs = self._makeContext( 'rhs' )
  66.258 +
  66.259 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ', subdir='skipme'
  66.260 +                      , mod_time=BEFORE )
  66.261 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub' )
  66.262 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nQRST', subdir='skipme'
  66.263 +                      , mod_time=AFTER )
  66.264 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub' )
  66.265 +
  66.266 +        cd = self._makeOne( lhs, rhs, skip=[ 'skipme' ] )
  66.267 +
  66.268 +        diffs = cd.compare()
  66.269 +
  66.270 +        self.assertEqual( diffs, '' )
  66.271 +
  66.272 +    def test_compare_changed_file_implicit_skip( self ):
  66.273 +
  66.274 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  66.275 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  66.276 +
  66.277 +        lhs = self._makeContext( 'lhs' )
  66.278 +        rhs = self._makeContext( 'rhs' )
  66.279 +
  66.280 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ', subdir='CVS'
  66.281 +                      , mod_time=BEFORE )
  66.282 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='.svn'
  66.283 +                      , mod_time=BEFORE )
  66.284 +
  66.285 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nQRST', subdir='CVS'
  66.286 +                      , mod_time=AFTER )
  66.287 +        self._makeFile( 'rhs', 'again.txt', 'MNOPQR', subdir='.svn'
  66.288 +                      , mod_time=AFTER )
  66.289 +
  66.290 +        cd = self._makeOne( lhs, rhs )
  66.291 +
  66.292 +        diffs = cd.compare()
  66.293 +
  66.294 +        self.assertEqual( diffs, '' )
  66.295 +
  66.296 +    def test_compare_added_file_no_missing_as_empty( self ):
  66.297 +
  66.298 +        lhs = self._makeContext( 'lhs' )
  66.299 +        rhs = self._makeContext( 'rhs' )
  66.300 +
  66.301 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ' )
  66.302 +        self._makeDirectory( 'lhs', subdir='sub' )
  66.303 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nWXYZ' )
  66.304 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub' )
  66.305 +
  66.306 +        cd = self._makeOne( lhs, rhs )
  66.307 +
  66.308 +        diffs = cd.compare()
  66.309 +
  66.310 +        self.assertEqual( diffs, ADDED_FILE_DIFFS_NO_MAE )
  66.311 +
  66.312 +    def test_compare_added_file_missing_as_empty( self ):
  66.313 +
  66.314 +        AFTER = DateTime( '2004-02-29T23:59:59Z' )
  66.315 +        lhs = self._makeContext( 'lhs' )
  66.316 +        rhs = self._makeContext( 'rhs' )
  66.317 +
  66.318 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ' )
  66.319 +        self._makeDirectory( 'lhs', subdir='sub' )
  66.320 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nWXYZ' )
  66.321 +        self._makeFile( 'rhs', 'again.txt', 'GHIJKL', subdir='sub'
  66.322 +                      , mod_time=AFTER )
  66.323 +
  66.324 +        cd = self._makeOne( lhs, rhs, missing_as_empty=True )
  66.325 +
  66.326 +        diffs = cd.compare()
  66.327 +
  66.328 +        self.assertEqual( diffs, ADDED_FILE_DIFFS_MAE % AFTER )
  66.329 +
  66.330 +    def test_compare_removed_file_no_missing_as_empty( self ):
  66.331 +
  66.332 +        lhs = self._makeContext( 'lhs' )
  66.333 +        rhs = self._makeContext( 'rhs' )
  66.334 +
  66.335 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ' )
  66.336 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub' )
  66.337 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nWXYZ' )
  66.338 +        self._makeDirectory( 'rhs', subdir='sub' )
  66.339 +
  66.340 +        cd = self._makeOne( lhs, rhs )
  66.341 +
  66.342 +        diffs = cd.compare()
  66.343 +
  66.344 +        self.assertEqual( diffs, REMOVED_FILE_DIFFS_NO_MAE )
  66.345 +
  66.346 +    def test_compare_removed_file_missing_as_empty( self ):
  66.347 +
  66.348 +        BEFORE = DateTime( '2004-01-01T00:00:00Z' )
  66.349 +        lhs = self._makeContext( 'lhs' )
  66.350 +        rhs = self._makeContext( 'rhs' )
  66.351 +
  66.352 +        self._makeFile( 'lhs', 'test.txt', 'ABCDEF\nWXYZ' )
  66.353 +        self._makeFile( 'lhs', 'again.txt', 'GHIJKL', subdir='sub'
  66.354 +                      , mod_time=BEFORE )
  66.355 +        self._makeFile( 'rhs', 'test.txt', 'ABCDEF\nWXYZ' )
  66.356 +        self._makeDirectory( 'rhs', subdir='sub' )
  66.357 +
  66.358 +        cd = self._makeOne( lhs, rhs, missing_as_empty=True )
  66.359 +
  66.360 +        diffs = cd.compare()
  66.361 +
  66.362 +        self.assertEqual( diffs, REMOVED_FILE_DIFFS_MAE % BEFORE )
  66.363 +
  66.364 +
  66.365 +TEST_TXT_DIFFS = """\
  66.366 +Index: test.txt
  66.367 +===================================================================
  66.368 +--- test.txt %s
  66.369 ++++ test.txt %s
  66.370 +@@ -1,2 +1,2 @@
  66.371 + ABCDEF
  66.372 +-WXYZ
  66.373 ++QRST\
  66.374 +"""
  66.375 +
  66.376 +ADDED_FILE_DIFFS_NO_MAE = """\
  66.377 +** File sub/again.txt added
  66.378 +"""
  66.379 +
  66.380 +ADDED_FILE_DIFFS_MAE = """\
  66.381 +Index: sub/again.txt
  66.382 +===================================================================
  66.383 +--- sub/again.txt 0
  66.384 ++++ sub/again.txt %s
  66.385 +@@ -1,0 +1,1 @@
  66.386 ++GHIJKL\
  66.387 +"""
  66.388 +
  66.389 +REMOVED_FILE_DIFFS_NO_MAE = """\
  66.390 +** File sub/again.txt removed
  66.391 +"""
  66.392 +
  66.393 +REMOVED_FILE_DIFFS_MAE = """\
  66.394 +Index: sub/again.txt
  66.395 +===================================================================
  66.396 +--- sub/again.txt %s
  66.397 ++++ sub/again.txt 0
  66.398 +@@ -1,1 +1,0 @@
  66.399 +-GHIJKL\
  66.400 +"""
  66.401 +
  66.402 +
  66.403 +def test_suite():
  66.404 +    return unittest.TestSuite((
  66.405 +        unittest.makeSuite( Test_unidiff ),
  66.406 +        unittest.makeSuite( ConfigDiffTests ),
  66.407 +        ))
  66.408 +
  66.409 +if __name__ == '__main__':
  66.410 +    unittest.main(defaultTest='test_suite')
    67.1 new file mode 100644
    67.2 --- /dev/null
    67.3 +++ b/tests/test_registry.py
    67.4 @@ -0,0 +1,1174 @@
    67.5 +##############################################################################
    67.6 +#
    67.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    67.8 +#
    67.9 +# This software is subject to the provisions of the Zope Public License,
   67.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   67.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   67.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   67.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   67.14 +# FOR A PARTICULAR PURPOSE.
   67.15 +#
   67.16 +##############################################################################
   67.17 +""" Registry unit tests.
   67.18 +
   67.19 +$Id: test_registry.py 40299 2005-11-21 16:49:38Z tseaver $
   67.20 +"""
   67.21 +
   67.22 +import unittest
   67.23 +import Testing
   67.24 +import Zope2
   67.25 +Zope2.startup()
   67.26 +
   67.27 +from OFS.Folder import Folder
   67.28 +from Products.GenericSetup.tests.common import BaseRegistryTests
   67.29 +from Products.GenericSetup import EXTENSION
   67.30 +from zope.interface import Interface
   67.31 +
   67.32 +from conformance import ConformsToIStepRegistry
   67.33 +from conformance import ConformsToIImportStepRegistry
   67.34 +from conformance import ConformsToIExportStepRegistry
   67.35 +from conformance import ConformsToIToolsetRegistry
   67.36 +from conformance import ConformsToIProfileRegistry
   67.37 +
   67.38 +
   67.39 +#==============================================================================
   67.40 +#   Dummy handlers
   67.41 +#==============================================================================
   67.42 +def ONE_FUNC( context ): pass
   67.43 +def TWO_FUNC( context ): pass
   67.44 +def THREE_FUNC( context ): pass
   67.45 +def FOUR_FUNC( context ): pass
   67.46 +
   67.47 +ONE_FUNC_NAME = '%s.%s' % ( __name__, ONE_FUNC.__name__ )
   67.48 +TWO_FUNC_NAME = '%s.%s' % ( __name__, TWO_FUNC.__name__ )
   67.49 +THREE_FUNC_NAME = '%s.%s' % ( __name__, THREE_FUNC.__name__ )
   67.50 +FOUR_FUNC_NAME = '%s.%s' % ( __name__, FOUR_FUNC.__name__ )
   67.51 +
   67.52 +
   67.53 +#==============================================================================
   67.54 +#   SSR tests
   67.55 +#==============================================================================
   67.56 +class ImportStepRegistryTests( BaseRegistryTests
   67.57 +                             , ConformsToIStepRegistry
   67.58 +                             , ConformsToIImportStepRegistry
   67.59 +                             ):
   67.60 +
   67.61 +    def _getTargetClass( self ):
   67.62 +
   67.63 +        from Products.GenericSetup.registry import ImportStepRegistry
   67.64 +        return ImportStepRegistry
   67.65 +
   67.66 +    def test_empty( self ):
   67.67 +
   67.68 +        registry = self._makeOne()
   67.69 +
   67.70 +        self.assertEqual( len( registry.listSteps() ), 0 )
   67.71 +        self.assertEqual( len( registry.listStepMetadata() ), 0 )
   67.72 +        self.assertEqual( len( registry.sortSteps() ), 0 )
   67.73 +
   67.74 +    def test_getStep_nonesuch( self ):
   67.75 +
   67.76 +        registry = self._makeOne()
   67.77 +
   67.78 +        self.assertEqual( registry.getStep( 'nonesuch' ), None )
   67.79 +        self.assertEqual( registry.getStep( 'nonesuch' ), None )
   67.80 +        default = object()
   67.81 +        self.failUnless( registry.getStepMetadata( 'nonesuch'
   67.82 +                                                 , default ) is default )
   67.83 +        self.failUnless( registry.getStep( 'nonesuch', default ) is default )
   67.84 +        self.failUnless( registry.getStepMetadata( 'nonesuch'
   67.85 +                                                 , default ) is default )
   67.86 +
   67.87 +    def test_getStep_defaulted( self ):
   67.88 +
   67.89 +        registry = self._makeOne()
   67.90 +        default = object()
   67.91 +
   67.92 +        self.failUnless( registry.getStep( 'nonesuch', default ) is default )
   67.93 +        self.assertEqual( registry.getStepMetadata( 'nonesuch', {} ), {} )
   67.94 +
   67.95 +    def test_registerStep_docstring( self ):
   67.96 +
   67.97 +        def func_with_doc( site ):
   67.98 +            """This is the first line.
   67.99 +
  67.100 +            This is the second line.
  67.101 +            """
  67.102 +        FUNC_NAME = '%s.%s' % ( __name__, func_with_doc.__name__ )
  67.103 +
  67.104 +        registry = self._makeOne()
  67.105 +
  67.106 +        registry.registerStep( id='docstring'
  67.107 +                             , version='1'
  67.108 +                             , handler=func_with_doc
  67.109 +                             , dependencies=()
  67.110 +                             )
  67.111 +
  67.112 +        info = registry.getStepMetadata( 'docstring' )
  67.113 +        self.assertEqual( info[ 'id' ], 'docstring' )
  67.114 +        self.assertEqual( info[ 'handler' ], FUNC_NAME )
  67.115 +        self.assertEqual( info[ 'dependencies' ], () )
  67.116 +        self.assertEqual( info[ 'title' ], 'This is the first line.' )
  67.117 +        self.assertEqual( info[ 'description' ] , 'This is the second line.' )
  67.118 +
  67.119 +    def test_registerStep_docstring_override( self ):
  67.120 +
  67.121 +        def func_with_doc( site ):
  67.122 +            """This is the first line.
  67.123 +
  67.124 +            This is the second line.
  67.125 +            """
  67.126 +        FUNC_NAME = '%s.%s' % ( __name__, func_with_doc.__name__ )
  67.127 +
  67.128 +        registry = self._makeOne()
  67.129 +
  67.130 +        registry.registerStep( id='docstring'
  67.131 +                             , version='1'
  67.132 +                             , handler=func_with_doc
  67.133 +                             , dependencies=()
  67.134 +                             , title='Title'
  67.135 +                             )
  67.136 +
  67.137 +        info = registry.getStepMetadata( 'docstring' )
  67.138 +        self.assertEqual( info[ 'id' ], 'docstring' )
  67.139 +        self.assertEqual( info[ 'handler' ], FUNC_NAME )
  67.140 +        self.assertEqual( info[ 'dependencies' ], () )
  67.141 +        self.assertEqual( info[ 'title' ], 'Title' )
  67.142 +        self.assertEqual( info[ 'description' ] , 'This is the second line.' )
  67.143 +
  67.144 +    def test_registerStep_single( self ):
  67.145 +
  67.146 +        registry = self._makeOne()
  67.147 +
  67.148 +        registry.registerStep( id='one'
  67.149 +                             , version='1'
  67.150 +                             , handler=ONE_FUNC
  67.151 +                             , dependencies=( 'two', 'three' )
  67.152 +                             , title='One Step'
  67.153 +                             , description='One small step'
  67.154 +                             )
  67.155 +
  67.156 +        steps = registry.listSteps()
  67.157 +        self.assertEqual( len( steps ), 1 )
  67.158 +        self.failUnless( 'one' in steps )
  67.159 +
  67.160 +        sorted = registry.sortSteps()
  67.161 +        self.assertEqual( len( sorted ), 1 )
  67.162 +        self.assertEqual( sorted[ 0 ], 'one' )
  67.163 +
  67.164 +        self.assertEqual( registry.getStep( 'one' ), ONE_FUNC )
  67.165 +
  67.166 +        info = registry.getStepMetadata( 'one' )
  67.167 +        self.assertEqual( info[ 'id' ], 'one' )
  67.168 +        self.assertEqual( info[ 'version' ], '1' )
  67.169 +        self.assertEqual( info[ 'handler' ], ONE_FUNC_NAME )
  67.170 +        self.assertEqual( info[ 'dependencies' ], ( 'two', 'three' ) )
  67.171 +        self.assertEqual( info[ 'title' ], 'One Step' )
  67.172 +        self.assertEqual( info[ 'description' ], 'One small step' )
  67.173 +
  67.174 +        info_list = registry.listStepMetadata()
  67.175 +        self.assertEqual( len( info_list ), 1 )
  67.176 +        self.assertEqual( info, info_list[ 0 ] )
  67.177 +
  67.178 +    def test_registerStep_conflict( self ):
  67.179 +
  67.180 +        registry = self._makeOne()
  67.181 +
  67.182 +        registry.registerStep( id='one', version='1', handler=ONE_FUNC )
  67.183 +
  67.184 +        self.assertRaises( KeyError
  67.185 +                         , registry.registerStep
  67.186 +                         , id='one'
  67.187 +                         , version='0'
  67.188 +                         , handler=ONE_FUNC
  67.189 +                         )
  67.190 +
  67.191 +        registry.registerStep( id='one', version='1', handler=ONE_FUNC )
  67.192 +
  67.193 +        info_list = registry.listStepMetadata()
  67.194 +        self.assertEqual( len( info_list ), 1 )
  67.195 +
  67.196 +    def test_registerStep_replacement( self ):
  67.197 +
  67.198 +        registry = self._makeOne()
  67.199 +
  67.200 +        registry.registerStep( id='one'
  67.201 +                             , version='1'
  67.202 +                             , handler=ONE_FUNC
  67.203 +                             , dependencies=( 'two', 'three' )
  67.204 +                             , title='One Step'
  67.205 +                             , description='One small step'
  67.206 +                             )
  67.207 +
  67.208 +        registry.registerStep( id='one'
  67.209 +                             , version='1.1'
  67.210 +                             , handler=ONE_FUNC
  67.211 +                             , dependencies=()
  67.212 +                             , title='Leads to Another'
  67.213 +                             , description='Another small step'
  67.214 +                             )
  67.215 +
  67.216 +        info = registry.getStepMetadata( 'one' )
  67.217 +        self.assertEqual( info[ 'id' ], 'one' )
  67.218 +        self.assertEqual( info[ 'version' ], '1.1' )
  67.219 +        self.assertEqual( info[ 'dependencies' ], () )
  67.220 +        self.assertEqual( info[ 'title' ], 'Leads to Another' )
  67.221 +        self.assertEqual( info[ 'description' ], 'Another small step' )
  67.222 +
  67.223 +    def test_registerStep_multiple( self ):
  67.224 +
  67.225 +        registry = self._makeOne()
  67.226 +
  67.227 +        registry.registerStep( id='one'
  67.228 +                             , version='1'
  67.229 +                             , handler=ONE_FUNC
  67.230 +                             , dependencies=()
  67.231 +                             )
  67.232 +
  67.233 +        registry.registerStep( id='two'
  67.234 +                             , version='2'
  67.235 +                             , handler=TWO_FUNC
  67.236 +                             , dependencies=()
  67.237 +                             )
  67.238 +
  67.239 +        registry.registerStep( id='three'
  67.240 +                             , version='3'
  67.241 +                             , handler=THREE_FUNC
  67.242 +                             , dependencies=()
  67.243 +                             )
  67.244 +
  67.245 +        steps = registry.listSteps()
  67.246 +        self.assertEqual( len( steps ), 3 )
  67.247 +        self.failUnless( 'one' in steps )
  67.248 +        self.failUnless( 'two' in steps )
  67.249 +        self.failUnless( 'three' in steps )
  67.250 +
  67.251 +    def test_sortStep_simple( self ):
  67.252 +
  67.253 +        registry = self._makeOne()
  67.254 +
  67.255 +        registry.registerStep( id='one'
  67.256 +                             , version='1'
  67.257 +                             , handler=ONE_FUNC
  67.258 +                             , dependencies=( 'two', )
  67.259 +                             )
  67.260 +
  67.261 +        registry.registerStep( id='two'
  67.262 +                             , version='2'
  67.263 +                             , handler=TWO_FUNC
  67.264 +                             , dependencies=()
  67.265 +                             )
  67.266 +
  67.267 +        steps = registry.sortSteps()