Index: /main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py	(revision 9725)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py	(revision 9726)
@@ -1217,2 +1217,9 @@
         Returns ``None`` if no such entry can be found.
         """
+
+class IExportContainerFinder(Interface):
+    """A finder for the central export container.
+    """
+    def __call__():
+        """Return the currently used global or site-wide IExportContainer.
+        """
Index: /main/waeup.kofa/trunk/src/waeup/kofa/utils/batching.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/utils/batching.py	(revision 9725)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/utils/batching.py	(revision 9726)
@@ -32,5 +32,5 @@
 from zope.component import createObject, getUtility
 from zope.component.hooks import setSite
-from zope.interface import Interface
+from zope.interface import Interface, implementer
 from zope.schema import getFields
 from zope.event import notify
@@ -39,5 +39,5 @@
     IBatchProcessor, FatalCSVError, IObjectConverter, IJobManager,
     ICSVExporter, IGNORE_MARKER, DuplicationError, JOB_STATUS_MAP,
-    IExportJobContainer, IExportJob)
+    IExportJobContainer, IExportJob, IExportContainerFinder)
 
 class BatchProcessor(grok.GlobalUtility):
@@ -672,2 +672,61 @@
                 return entry
         return None
+
+class VirtualExportJobContainer(ExportJobContainer):
+    """A virtual export job container.
+
+    Virtual ExportJobContainers can be used as a mixin just like real
+    ExportJobContainer.
+
+    They retrieve and store data in the site-wide ExportJobContainer.
+
+    Functionality is currently entirely as for regular
+    ExportJobContainers, except that data is stored elsewhere.
+
+    VirtualExportJobContainers need a registered
+    IExportContainerFinder utility to find a suitable container for
+    storing data.
+    """
+    grok.implements(IExportJobContainer)
+
+    @property
+    def _site_container(self):
+        return getUtility(IExportContainerFinder)()
+
+    # The following is a simple trick. While ExportJobContainers store
+    # only one attribute in ZODB, it is sufficient to replace this
+    # attribute `running_exports` with a suitable manager to make the
+    # whole virtual container work like the original but with the data
+    # stored in the site-wide exports container. This way, virtual
+    # export containers provide the whole functionality of a regular
+    # exports container but store no data at all with themselves.
+    @property
+    def running_exports(self):
+        """Exports stored in the site-wide exports container.
+        """
+        return self._site_container.running_exports
+
+    @running_exports.setter
+    def running_exports(self, value):
+        self._site_container.running_exports = value
+
+    @running_exports.deleter
+    def running_exports(self):
+        del self._site_container.running_exports
+
+
+@implementer(IExportContainerFinder)
+class ExportContainerFinder(grok.GlobalUtility):
+    """Finder for local (site-wide) export container.
+    """
+
+    def __call__(self):
+        """Get the local export container-
+
+        If no site can be determined or the site provides no export
+        container, None is returned.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        return site.get('datacenter', None)
Index: /main/waeup.kofa/trunk/src/waeup/kofa/utils/tests/test_batching.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/utils/tests/test_batching.py	(revision 9725)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/utils/tests/test_batching.py	(revision 9726)
@@ -25,7 +25,7 @@
 from zc.async.interfaces import IJob
 from zope import schema
-from zope.component import provideUtility, getGlobalSiteManager
+from zope.component import provideUtility, getGlobalSiteManager, getUtility
 from zope.component.factory import Factory
-from zope.component.hooks import clearSite
+from zope.component.hooks import clearSite, setSite
 from zope.component.interfaces import IFactory
 from zope.interface import Interface, implements, verify
@@ -33,10 +33,10 @@
 from waeup.kofa.interfaces import (
     ICSVExporter, IBatchProcessor, IExportJobContainer, IJobManager,
-    IExportJob)
+    IExportJob, IExportContainerFinder)
 from waeup.kofa.testing import (
     FunctionalLayer, FunctionalTestCase, FakeJob, FakeJobManager)
 from waeup.kofa.utils.batching import (
     ExporterBase, BatchProcessor, export_job, AsyncExportJob,
-    ExportJobContainer)
+    ExportJobContainer, VirtualExportJobContainer, ExportContainerFinder)
 
 optionflags = (
@@ -396,4 +396,6 @@
     # Test ExportJobContainer
 
+    TestedClass = ExportJobContainer
+
     def setUp(self):
         # register a suitable ICSVExporter as named utility
@@ -412,6 +414,6 @@
     def test_export_job_interfaces(self):
         # the ExportJobContainer implements promised interfaces correctly...
-        container = ExportJobContainer()
-        verify.verifyClass(IExportJobContainer, ExportJobContainer)
+        container = self.TestedClass()
+        verify.verifyClass(IExportJobContainer, self.TestedClass)
         verify.verifyObject(IExportJobContainer, container)
         return
@@ -419,5 +421,5 @@
     def test_start_export_job(self):
         # we can start jobs
-        container = ExportJobContainer()
+        container = self.TestedClass()
         container.start_export_job('cave_exporter', 'bob')
         result = self.job_manager._jobs.values()[0]
@@ -431,5 +433,5 @@
     def test_get_running_export_jobs_all(self):
         # we can get export jobs of all users
-        container = ExportJobContainer()
+        container = self.TestedClass()
         container.start_export_job('cave_exporter', 'bob')
         container.start_export_job('cave_exporter', 'alice')
@@ -444,5 +446,5 @@
     def test_get_running_export_jobs_user(self):
         # we can get the export jobs running for a certain user
-        container = ExportJobContainer()
+        container = self.TestedClass()
         container.start_export_job('cave_exporter', 'bob')
         container.start_export_job('cave_exporter', 'alice')
@@ -457,5 +459,5 @@
     def test_get_running_export_jobs_only_if_exist(self):
         # we get only jobs that are accessible through the job manager...
-        container = ExportJobContainer()
+        container = self.TestedClass()
         container.start_export_job('cave_exporter', 'bob')
         container.start_export_job('cave_exporter', 'bob')
@@ -473,5 +475,5 @@
     def test_get_export_job_status(self):
         # we can get the stati of jobs...
-        container = ExportJobContainer()
+        container = self.TestedClass()
         container.start_export_job('cave_exporter', 'alice')
         container.start_export_job('cave_exporter', 'bob')
@@ -490,5 +492,5 @@
         # we can remove export entries in local lists and the job
         # manager as well...
-        container = ExportJobContainer()
+        container = self.TestedClass()
         container.start_export_job('cave_exporter', 'bob')
         entry = container.running_exports[0]
@@ -503,5 +505,5 @@
     def test_delete_export_entry_remove_file(self):
         # any result files of exports are deleted as well
-        container = ExportJobContainer()
+        container = self.TestedClass()
         entry = ('4', 'cave_exporter', 'bob')
         container.running_exports = [entry]
@@ -515,5 +517,5 @@
     def test_entry_from_job_id(self):
         # we can get an entry for a job_id if the id exists
-        container = ExportJobContainer()
+        container = self.TestedClass()
         entry = ('4', 'cave_exporter', 'bob')
         container.running_exports = [entry]
@@ -528,2 +530,66 @@
         shutil.rmtree(fake_job.dir_path)
         return
+
+class VirtualExportJobContainerTests(ExportJobContainerTests):
+    # VirtualExportJobContainers should provide the
+    # same functionality as regular ones.
+
+    TestedClass = VirtualExportJobContainer
+
+    def setUp(self):
+        super(VirtualExportJobContainerTests, self).setUp()
+        self.root_job_container = ExportJobContainer()
+        def fake_finder():
+            return self.root_job_container
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(fake_finder, IExportContainerFinder)
+        return
+
+class ExportContainerFinderTests(FunctionalTestCase):
+    # Tests for export container finder.
+
+    layer = FunctionalLayer
+
+    def test_get_finder_as_util(self):
+        # we can get a finder by utility lookup
+        finder = getUtility(IExportContainerFinder)
+        self.assertTrue(finder is not None)
+        self.assertEqual(
+            IExportContainerFinder.providedBy(finder),
+            True)
+        return
+
+    def test_iface(self):
+        # the finder complies with the promised interface
+        finder = ExportContainerFinder()
+        verify.verifyClass(IExportContainerFinder,
+                           ExportContainerFinder)
+        verify.verifyObject(IExportContainerFinder, finder)
+        return
+
+    def test_no_site(self):
+        # a finder returns None if no site is available
+        finder = ExportContainerFinder()
+        self.assertEqual(
+            finder(), None)
+        return
+
+    def test_active_site(self):
+        # we get the datafinder if one is installed and site set
+        self.getRootFolder()['app'] = University()
+        finder = getUtility(IExportContainerFinder)
+        setSite(self.getRootFolder()['app'])
+        container = finder()
+        self.assertTrue(container is not None)
+        return
+
+    def test_broken_site(self):
+        # if the current site has no ExportContainer, we get None
+        self.getRootFolder()['app'] = University()
+        app = self.getRootFolder()['app']
+        del app['datacenter'] # datacenter _is_ the export container
+        setSite(app)
+        finder = getUtility(IExportContainerFinder)
+        container = finder()
+        self.assertTrue(container is None)
+        return
