Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/accesscodes/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/accesscodes/browser.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/accesscodes/browser.py	(revision 9166)
@@ -23,5 +23,5 @@
 from hurry.workflow.interfaces import InvalidTransitionError
 from waeup.kofa.browser.resources import datatable
-from waeup.kofa.browser import KofaPage, KofaAddFormPage, NullValidator
+from waeup.kofa.browser.layout import KofaPage, KofaAddFormPage, NullValidator
 from waeup.kofa.browser.breadcrumbs import Breadcrumb
 from waeup.kofa.browser.viewlets import (
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/applicants/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/applicants/browser.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/applicants/browser.py	(revision 9166)
@@ -38,6 +38,8 @@
     INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED)
 from waeup.kofa.browser import (
-    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
+#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
     DEFAULT_PASSPORT_IMAGE_PATH)
+from waeup.kofa.browser.layout import (
+    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
 from waeup.kofa.browser.interfaces import ICaptchaManager
 from waeup.kofa.browser.breadcrumbs import Breadcrumb
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/__init__.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/__init__.py	(revision 9166)
@@ -1,8 +1,8 @@
 import os
 
-from waeup.kofa.browser.layout import (
-    KofaPage, KofaForm, KofaLayout, KofaDisplayFormPage, KofaEditFormPage,
-    KofaAddFormPage, NullValidator)
-from waeup.kofa.browser.pages import ContactAdminForm
+#from waeup.kofa.browser.layout import (
+#    KofaPage, KofaForm, KofaLayout, KofaDisplayFormPage, KofaEditFormPage,
+#    KofaAddFormPage, NullValidator)
+#from waeup.kofa.browser.pages import ContactAdminForm
 
 IMAGE_PATH = os.path.join(
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/async.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/async.py	(revision 9166)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/async.py	(revision 9166)
@@ -0,0 +1,151 @@
+import grok
+from zc.async.interfaces import COMPLETED, NEW
+from zope.interface import Interface
+from waeup.kofa.async import IJob, IJobManager
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.browser.resources import jquery, loadbar
+from waeup.kofa.interfaces import IKofaObject, IDataCenter
+from waeup.kofa.interfaces import MessageFactory as _
+
+grok.templatedir('templates')
+grok.context(IKofaObject)
+
+loadingbar_template = '''
+<input id="status_url" type="hidden" name="status_url" value="%s" />
+<div id="loadbar">%%s</div>
+'''
+
+class LoadingBar(grok.ViewletManager):
+    grok.name('loadingbar')
+
+class LoadingBarViewlet(grok.Viewlet):
+    grok.context(Interface)
+    grok.viewletmanager(LoadingBar)
+
+    _running_exports = None
+
+    def getRunningExports(self):
+        """Returns running exports as list of tuples.
+
+        Only exports triggered by the current user (identified by
+        principal.id) are returned.
+
+        Each tuple has the form (<STATUS>, <STATUS_TITLE>, <EXPORTER_NAME>).
+
+        ``STATUS``:
+           the status as machine readable string (something like
+           ``'completed'``)
+
+        ``STATUS_TITLE``:
+           status of export as translated string.
+
+        ``EXPORTER_NAME``:
+           string representing the exporter title used when triggering
+           the export job.
+        """
+        result = self.context.get_export_jobs_status(self.user_id)
+        return result
+
+    def update(self):
+        self.user_id = self.request.principal.id
+        self.exports = self.getRunningExports()
+        self.uncompleted = [x for x in self.exports if x[0] != 'completed']
+        loadbar.need()
+
+    def render(self):
+        status_url = self.view.url('status')
+        template = loadingbar_template % (status_url,)
+        content = ''
+        return template % content
+
+class DataCenterJSON(grok.JSON):
+    grok.context(IDataCenter)
+
+    def status(self):
+        """Get status of currently handled export jobs for users.
+
+        The result is a set of valus useful for JavaScript handlers to
+        update some page handling data center exports.
+
+        Returns a mapping::
+
+          {html=<string>, interval=<int>, reload=<bool>}
+
+        where `html` is some HTML code that might be shown in a
+        loading bar section.
+
+        `interval` is the time in microseconds after which the loading
+        bar should be reloaded (by a new JSON call). If the value is
+        zero, then the loading bar should not be renewed and further
+        JSON calls be canceled.
+
+        `reload` is a hint to tell, whether the complete page should
+        be reloaded or not. Reloading is neccessary, if the export
+        file creation finished and download buttons or similar should
+        appear.
+        """
+        user_id = self.request.principal.id
+        jobs = self.context.get_export_jobs_status(user_id)
+        uncompleted = [x for x in jobs if x[0] != 'completed']
+        result = dict(
+            html='', interval=0, reload=False)
+        if len(uncompleted):
+            html = '<em>%s</em>' % (_("Please wait..."),)
+            result.update(interval=500, html=html)
+        else:
+            if len(jobs):
+                result.update(reload=True)
+        return result
+
+
+class JobManagerView(KofaPage):
+    """The main view for the job container.
+    """
+    grok.name('index.html')
+    grok.context(IJobManager)
+    grok.template('virtjobscontainerindex')
+    grok.require('waeup.manageJobs')
+
+    def jobs(self):
+        for job_id, job in sorted(self.context.jobs(),
+                                  cmp=lambda x, y: cmp(int(x[0]), int(y[0]))):
+            link = self.url(self.context, job_id)
+            removable = job.status in (COMPLETED, NEW)
+            yield dict(
+                job_id = job_id,
+                job = job,
+                link = link,
+                status = job.status,
+                begin_after = job.begin_after,
+                begin_by = job.begin_by,
+                annotations = '%r' % [
+                    (x, y) for x, y in job.annotations.items()],
+                removable = removable,
+                failure = '%r' % getattr(job, 'traceback', None),
+                )
+
+    def update(self, START_NEW=None, REMOVE=None, job_id=None):
+        jquery.need()
+        if REMOVE and job_id:
+            self.context.remove(job_id)
+            self.flash('Removed job "%s"' % job_id)
+        if START_NEW:
+            self.context.start_test_job()                    #pragma NO COVER
+            self.flash('Started new test job "%s"' % job_id) #pragma NO COVER
+        return
+
+class JobView(grok.View):
+    grok.name('index.html')
+    grok.context(IJob)
+    grok.template('jobindex')
+    grok.require('waeup.manageJobs')
+
+    def percent(self):
+        value = self.context.annotations.get('percent', None)
+        if not value:
+            return
+        value = int(value+0.5)
+        return min(value, 100)
+
+    def update(self):
+        pass
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/breadcrumbs.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/breadcrumbs.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/breadcrumbs.py	(revision 9166)
@@ -26,7 +26,10 @@
     IConfigurationContainer, ISessionConfiguration)
 from waeup.kofa.interfaces import MessageFactory as _
-from waeup.kofa.browser import interfaces
-from waeup.kofa.browser.interfaces import (IBreadcrumb,
-    IBreadcrumbIgnorable, IBreadcrumbContainer)
+from waeup.kofa.browser.interfaces import (
+    IBreadcrumb, IBreadcrumbIgnorable, IBreadcrumbContainer, IKofaObject,
+    IUniversity, IFacultiesContainer, IUsersContainer, IDataCenter, IFaculty,
+    IDepartment, ICourse, ICertificate, ICoursesContainer, ICertificateCourse,
+    ICertificatesContainer,
+    )
 
 class Breadcrumb(grok.Adapter):
@@ -34,5 +37,5 @@
     """
     grok.provides(IBreadcrumb)
-    grok.context(interfaces.IKofaObject)
+    grok.context(IKofaObject)
     grok.name('index')
 
@@ -87,5 +90,5 @@
     """A breadcrumb for university index pages.
     """
-    grok.context(interfaces.IUniversity)
+    grok.context(IUniversity)
     title = _(u'Home')
     parent = None
@@ -100,5 +103,5 @@
     itself is bound to.
     """
-    grok.context(interfaces.IUniversity)
+    grok.context(IUniversity)
     grok.name('manage')
     title = _(u'Portal Settings')
@@ -113,5 +116,5 @@
     """A breadcrumb for faculty containers.
     """
-    grok.context(interfaces.IFacultiesContainer)
+    grok.context(IFacultiesContainer)
     title = _(u'Academics')
 
@@ -119,5 +122,5 @@
     """A breadcrumb for administration areas of University instances.
     """
-    grok.context(interfaces.IUniversity)
+    grok.context(IUniversity)
     grok.name('administration')
     title = _(u'Administration')
@@ -145,5 +148,5 @@
     """A breadcrumb for user containers.
     """
-    grok.context(interfaces.IUsersContainer)
+    grok.context(IUsersContainer)
     title = _(u'Portal Users')
     parent_viewname = 'administration'
@@ -152,5 +155,5 @@
     """A breadcrumb for data centers.
     """
-    grok.context(interfaces.IDataCenter)
+    grok.context(IDataCenter)
     title = _(u'Data Center')
     parent_viewname = 'administration'
@@ -159,5 +162,5 @@
     """A breadcrumb for faculties.
     """
-    grok.context(interfaces.IFaculty)
+    grok.context(IFaculty)
 
     @property
@@ -168,20 +171,20 @@
     """A breadcrumb for departments.
     """
-    grok.context(interfaces.IDepartment)
+    grok.context(IDepartment)
 
 class CourseBreadcrumb(FacultyBreadcrumb):
     """A breadcrumb for courses.
     """
-    grok.context(interfaces.ICourse)
+    grok.context(ICourse)
 
 class CertificateBreadcrumb(FacultyBreadcrumb):
     """A breadcrumb for certificates.
     """
-    grok.context(interfaces.ICertificate)
+    grok.context(ICertificate)
 
 class CoursesContainerBreadcrumb(Breadcrumb):
     """ We don't want course container breadcrumbs.
     """
-    grok.context(interfaces.ICoursesContainer)
+    grok.context(ICoursesContainer)
     grok.implements(IBreadcrumbIgnorable)
 
@@ -189,5 +192,5 @@
     """ We don't want course container breadcrumbs.
     """
-    grok.context(interfaces.ICertificatesContainer)
+    grok.context(ICertificatesContainer)
     grok.implements(IBreadcrumbIgnorable)
 
@@ -195,5 +198,5 @@
     """ We don't want course container breadcrumbs.
     """
-    grok.context(interfaces.ICertificateCourse)
+    grok.context(ICertificateCourse)
     @property
     def title(self):
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/captcha.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/captcha.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/captcha.py	(revision 9166)
@@ -28,5 +28,6 @@
 from zope.interface import Interface
 from zope.publisher.interfaces.http import IHTTPRequest
-from waeup.kofa.browser import KofaPage, resources
+from waeup.kofa.browser import resources
+from waeup.kofa.browser.layout import KofaPage
 from waeup.kofa.browser.interfaces import (
     ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/pages.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/pages.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/pages.py	(revision 9166)
@@ -39,5 +39,5 @@
     IPrincipalRoleManager, IPrincipalRoleMap)
 from zope.session.interfaces import ISession
-from waeup.kofa.browser import (
+from waeup.kofa.browser.layout import (
     KofaPage, KofaForm, KofaEditFormPage, KofaAddFormPage,
     KofaDisplayFormPage, NullValidator)
@@ -48,5 +48,6 @@
     ICaptchaManager, IChangePassword)
 from waeup.kofa.browser.layout import jsaction, action, UtilityView
-from waeup.kofa.browser.resources import warning, datepicker, tabs, datatable
+from waeup.kofa.browser.resources import (
+    warning, datepicker, tabs, datatable, page_reloader, page_not_reloader)
 from waeup.kofa.interfaces import MessageFactory as _
 from waeup.kofa.interfaces import(
@@ -54,8 +55,9 @@
     IKofaXMLImporter, IKofaXMLExporter, IBatchProcessor,
     ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
-    ISessionConfiguration, ISessionConfigurationAdd,
+    ISessionConfiguration, ISessionConfigurationAdd, IJobManager,
     IPasswordValidator, IContactForm, IKofaUtils, ICSVExporter,)
 from waeup.kofa.permissions import (
     get_users_with_local_roles, get_all_roles, get_all_users)
+
 from waeup.kofa.students.catalog import search as searchstudents
 from waeup.kofa.university.catalog import search
@@ -1330,5 +1332,6 @@
     label = _('Download portal data as CSV file')
     pnav = 0
-    export_button = _(u'Download')
+    export_button = _(u'Create CSV file')
+    _running_exports = None
 
     def getExporters(self):
@@ -1338,9 +1341,68 @@
         return sorted(title_name_tuples)
 
-    def update(self, export=None, exporter=None):
-        if None in (export, exporter):
-            return
-        self.redirect(
-            self.url(self.context, 'export.csv') + '?exporter=%s' % exporter)
+    def job_finished(self, status):
+        return status == 'completed'
+
+    def getRunningExports(self):
+        """Returns running exports as list of tuples.
+
+        Only exports triggered by the current user (identified by
+        principal.id) are returned.
+
+        Each tuple has the form (<STATUS>, <STATUS_TITLE>, <EXPORTER_NAME>).
+
+        ``STATUS``:
+           the status as machine readable string (something like
+           ``'completed'``)
+
+        ``STATUS_TITLE``:
+           status of export as translated string.
+
+        ``EXPORTER_NAME``:
+           string representing the exporter title used when triggering
+           the export job.
+
+        Calling this method also installs a JavaScript page reloader
+        that reloads the page after some time (1.5 secs), if there is
+        a running export that is not completed yet.
+
+        The method caches results.
+        """
+        if self._running_exports is None:
+            self._running_exports = self._getRunningExports()
+        return self._running_exports
+
+    def _getRunningExports(self):
+        result = self.context.get_export_jobs_status(self.user_id)
+        uncompleted = [x for x in result if x[0] != 'completed']
+        #if len(uncompleted):
+        #    page_reloader.need()
+        #else:
+        #    page_not_reloader.need()
+        return result
+
+    def update(self, export=None, start_export=None, exporter=None,
+               discard=None, download=None):
+        self.user_id = self.request.principal.id
+        if discard:
+            myjobs = self.context.get_running_export_jobs(self.user_id)
+            for entry in myjobs:
+                self.context.delete_export_entry(entry)
+                self.flash(_('Discarded export result'))
+            return
+        if download:
+            myjobs = self.context.get_running_export_jobs(self.user_id)
+            if not len(myjobs):
+                self.flash(_('This export was already discarded.'))
+                return
+            job_id = myjobs[0][0]
+            self.redirect(
+                self.url(self.context, 'export.csv') + '?job_id=%s' % job_id)
+            return
+        if None in (start_export, exporter):
+            return
+        job_id = self.context.start_export_job(
+            exporter, self.request.principal.id)
+        self.redirect(self.url(self.context, 'export'))
         return
 
@@ -1350,16 +1412,24 @@
     grok.require('waeup.manageDataCenter')
 
-    def render(self, exporter=None):
-        if exporter is None:
-            return
-        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
-        self.context.logger.info(
-            '%s - exported: %s' % (ob_class, exporter))
-        exporter = getUtility(ICSVExporter, name=exporter)
-        csv_data = exporter.export_all(grok.getSite())
-        #csv_data.seek(0)
+    def render(self, exporter=None, job_id=None):
+
+        manager = getUtility(IJobManager)
+        job = manager.get(job_id)
+        if job is None:
+            return
+        if hasattr(job.result, 'traceback'):
+            # XXX: Some error happened. Do something more approriate here...
+            return
+        path = job.result
+        if not os.path.exists(path):
+            # XXX: Do something more appropriate here...
+            return
+        result = open(path, 'rb').read()
         self.response.setHeader(
             'Content-Type', 'text/csv; charset=UTF-8')
-        return csv_data
+        # remove job and running_exports entry from context
+        self.context.delete_export_entry(
+            self.context.entry_from_job_id(job_id))
+        return result
 
 class ExportXMLPage(grok.View):
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/resources.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/resources.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/resources.py	(revision 9166)
@@ -302,2 +302,13 @@
     waeup_kofa, 'recaptcha_white.js'
     )
+
+#: A page reloader (reloads after 3 secs)
+page_reloader = ResourceInclusion(
+    waeup_kofa, 'page_reloader.js', depends=[jquery])
+
+#: Disables page reloader (which is triggered by a JavaScript timer).
+page_not_reloader = ResourceInclusion(
+    waeup_kofa, 'page_not_reloader.js', depends=[jquery])
+
+loadbar = ResourceInclusion(
+    waeup_kofa, 'loadbar.js', depends=[jquery])
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/loadbar.js
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/loadbar.js	(revision 9166)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/loadbar.js	(revision 9166)
@@ -0,0 +1,23 @@
+
+var loadbar_interval = null;
+$(document).ready(
+    function update() {
+	$.getJSON(
+	    $("#status_url").attr("value"),
+	    {},
+	    function(data) {
+		$('input[name="reload"]').remove();
+		clearInterval(loadbar_interval);
+		$("#loadbar").html(data.html);
+		if (data.reload == true) {
+		    if ($('input[name="download"]').attr("value") == null) {
+			location.reload();
+		    }
+		}
+		if (data.interval != 0) {
+		    loadbar_interval = setInterval(update, data.interval);
+		}
+	    }
+	)
+    }
+)
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/page_not_reloader.js
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/page_not_reloader.js	(revision 9166)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/page_not_reloader.js	(revision 9166)
@@ -0,0 +1,8 @@
+$(document).ready(function() {
+    if(window.top==window) {
+	// you're not in a frame so you reload the site
+	window.clearTimeout('location.reload()'); //reloads after 1.5 seconds
+    } else {
+	//you're inside a frame, so you stop reloading
+    }
+});
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/page_reloader.js
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/page_reloader.js	(revision 9166)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/static/page_reloader.js	(revision 9166)
@@ -0,0 +1,8 @@
+$(document).ready(function() {
+    if(window.top==window) {
+	// you're not in a frame so you reload the site
+	window.setTimeout('location.reload()', 1500); //reloads after 1.5 seconds
+    } else {
+	//you're inside a frame, so you stop reloading
+    }
+});
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/datacenterexportpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/datacenterexportpage.pt	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/datacenterexportpage.pt	(revision 9166)
@@ -10,6 +10,7 @@
       </p>
       <p i18n:translate="">
-	The file will be generated and then be made available for you to
-	download.
+	The file will be generated and then be made available for you
+	to download. You must download or discard any existing export
+	file before creating a new one.
       </p>
     </div>
@@ -48,4 +49,8 @@
 	      </div>
 	    </div>
+
+	    <div>
+	      <tal:loading_bar content="structure provider:loadingbar" />
+	    </div>
 	  </div>
 
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/jobindex.pt
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/jobindex.pt	(revision 9166)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/jobindex.pt	(revision 9166)
@@ -0,0 +1,21 @@
+<html>
+  <head>
+  </head>
+  <body>
+    <div>
+      Job: <span tal:content="python:view.context.__repr__()">JOB</span>
+    </div>
+    <div>
+      Status: <span tal:content="view/context/status">status</span>
+    </div>
+    <div>
+      Begin after: <span tal:content="view/context/begin_after">after</span>
+    </div>
+    <div>
+      Begin by: <span tal:content="view/context/begin_by">by</span>
+    </div>
+    <div>
+      Percent done: <span tal:content="view/percent">percent</span>
+    </div>
+  </body>
+</html>
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt	(revision 9166)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt	(revision 9166)
@@ -0,0 +1,45 @@
+<h1>Running and Completed Jobs</h1>
+<div>
+  <form method="POST">
+    <input type="submit" class="btn primary" name="RELOAD" value="reload" />
+    <input type="submit" class="btn secondary"
+	   name="START_NEW" value="start new test job" />
+  </form>
+</div>
+<hr />
+
+<div tal:repeat="job view/jobs">
+  <div>
+    JOB
+    <a href="" tal:attributes="href job/link">
+      <span tal:replace="job/job_id">12</span>
+    </a>
+    status: <span tal:replace="job/status">new</span>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">begin by:</span>
+      <span tal:content="job/begin_by"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">begin after:</span>
+      <span tal:content="job/begin_after"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">annotations:</span>
+      <span tal:content="job/annotations"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">failure:</span>
+      <span tal:content="job/failure"></span>
+    </div>
+  </div>
+  <form tal:condition="job/removable" method="POST">
+    <input type="submit" class="btn secondary" name="REMOVE" value="Remove" />
+    <input type="hidden" name="job_id"
+	   tal:attributes="value job/job_id" />
+  </form>
+  <hr />
+</div>
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/tests/test_async.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/tests/test_async.py	(revision 9166)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/tests/test_async.py	(revision 9166)
@@ -0,0 +1,174 @@
+import os
+import shutil
+import tempfile
+import transaction
+from zc.async.testing import wait_for_result
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.security.interfaces import Unauthorized
+from zope.testbrowser.testing import Browser
+from waeup.kofa.app import University
+from waeup.kofa.async import AsyncJob, get_job_id
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+
+def fake_get_export_jobs_status(user_id=None):
+    return [('completed', 'Completed', 'my_exporter'),]
+
+def dummy_func():
+    return 42
+
+class AsyncBrowserTests(FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AsyncBrowserTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.storage = os.path.join(self.workdir, 'storage')
+        os.mkdir(self.storage)
+        app = University()
+        app['datacenter'].setStoragePath(self.storage)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+        self.browser = Browser()
+        self.browser.handleErrors = False
+        self.datacenter_path = 'http://localhost/app/datacenter'
+
+    def tearDown(self):
+        super(AsyncBrowserTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+
+class JobViewTests(AsyncBrowserTests):
+    # Tests for the view of single jobs
+    overview_path = 'http://localhost/app/jobs'
+
+    def setUp(self):
+        super(JobViewTests, self).setUp()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+
+    def start_job(self):
+        # helper that starts a job in background and returns the job
+        manager = getUtility(IJobManager)
+        job = AsyncJob(dummy_func)
+        manager.put(job, site=self.getRootFolder())
+        return job
+
+    def test_get_job_view(self):
+        # we can get a job on a view
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.open(self.overview_path)
+        job_id = get_job_id(job)
+        link = self.browser.getLink(job_id)
+        expected_url = 'http://localhost/app/jobs/%s' % job_id
+        self.assertEqual(link.url, expected_url)
+        link.click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_anonymous_forbidden(self):
+        # we need some permissions to get a job view
+        transaction.commit() # make sure the 'app' is stored in ZODB
+        job = self.start_job()
+        wait_for_result(job)
+        job_id = get_job_id(job)
+        link_url = 'http://localhost/app/jobs/%s' % job_id
+
+        # get a new browser, where we're not authenticated
+        browser = Browser()
+        browser.handleErrors = False
+        self.assertRaises(Unauthorized, browser.open, link_url)
+        return
+
+class JobOverviewTests(AsyncBrowserTests):
+    # Tests for the job overview page of universities
+    overview_path = 'http://localhost/app/jobs'
+
+    def setUp(self):
+        super(JobOverviewTests, self).setUp()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+
+    def start_job(self):
+        # helper that start a job in background and returns the job
+        manager = getUtility(IJobManager)
+        job = AsyncJob(dummy_func)
+        manager.put(job, site=self.getRootFolder())
+        return job
+
+    def test_job_get(self):
+        # we can get the overview page
+        self.browser.open(self.overview_path)
+        self.assertTrue("Running and Completed Jobs" in self.browser.contents)
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_anonymous_forbidden(self):
+        # anonymous user will be kept out
+        browser = Browser() # get a browser w/o credentials set
+        browser.handleErrors = False
+        self.assertRaises(Unauthorized, browser.open, self.overview_path)
+        return
+
+    def test_job_reload(self):
+        # we can reload the overview page
+        self.browser.open(self.overview_path)
+        self.browser.getControl('reload').click()
+        self.assertTrue("Running and Completed Jobs" in self.browser.contents)
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_jobs_appear(self):
+        # any job running will appear
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.getControl('reload').click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        self.assertTrue('status: Completed' in self.browser.contents)
+        return
+
+    def test_job_removal(self):
+        # we can remove jobs via the overview page
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.getControl('reload').click()
+        self.assertEqual(len(list(getUtility(IJobManager).jobs())), 1)
+        self.browser.getControl('Remove').click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        self.assertTrue('Removed job' in self.browser.contents)
+        self.assertEqual(len(list(getUtility(IJobManager).jobs())), 0)
+        return
+
+class DataCenterJSONTests(AsyncBrowserTests):
+
+    def test_status_plain(self):
+        # if no job was triggered, no need to update something.
+        path = '%s/status' % self.datacenter_path
+        self.browser.open(path)
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/json')
+        self.assertEqual(self.browser.contents,
+                         '{"reload": false, "interval": 0, "html": ""}')
+        return
+
+    def test_status_completed(self):
+        # if all jobs are completed, we should reload the page
+        self.app[
+            'datacenter'].get_export_jobs_status = fake_get_export_jobs_status
+        path = '%s/status' % self.datacenter_path
+        self.browser.open(path)
+        self.assertEqual(self.browser.contents,
+                         '{"reload": true, "interval": 0, "html": ""}')
+        return
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/tests/test_browser.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/tests/test_browser.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/tests/test_browser.py	(revision 9166)
@@ -26,6 +26,7 @@
 import os
 import grok
+from zc.async.testing import wait_for_result
 from zope.event import notify
-from zope.component import createObject, queryUtility
+from zope.component import createObject, queryUtility, getUtility
 from zope.component.hooks import setSite, clearSite
 from zope.catalog.interfaces import ICatalog
@@ -36,6 +37,11 @@
 from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
 from waeup.kofa.app import University
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
 from waeup.kofa.university.faculty import Faculty
 from waeup.kofa.university.department import Department
+
+
+
 
 SAMPLE_FILE = os.path.join(os.path.dirname(__file__), 'test_file.csv')
@@ -106,5 +112,4 @@
         shutil.rmtree(self.dc_root)
 
-
 class DataCenterUITests(UniversitySetup):
     # Tests for DataCenter class views and pages
@@ -149,5 +154,27 @@
         return
 
-    def test_export(self):
+
+class DataCenterUIExportTests(UniversitySetup, FunctionalAsyncTestCase):
+    # Tests for DataCenter class views and pages
+
+    layer = FunctionalLayer
+
+    def wait_for_export_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_datacenter(self, job_id):
+        # tell whether job_id is stored in datacenter's running jobs list
+        for entry in list(self.app['datacenter'].running_exports):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def test_export_start(self):
+        # we can trigger export file creation
         self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
         self.browser.open(self.datacenter_path)
@@ -156,14 +183,48 @@
         self.browser.getLink("Export CSV file").click()
         self.browser.getControl(name="exporter").value = ['faculties']
+        self.browser.getControl("Create CSV file").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        return
+
+    def test_export_download(self):
+        # we can download a generated export result
+        self.test_export_start()
+        # while the export file is created, we get a reload button
+        # (or a loading bar if javascript is enabled)...
+        self.browser.getControl("Reload").click()
+        # ...which is displayed as long as the job is not finished.
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        try:
+            self.browser.getControl("Reload").click()
+        except LookupError:
+            # if the job completed very fast, we will get the download
+            # link immediately
+            pass
+        # ...we can download the result
         self.browser.getControl("Download").click()
-        self.assertEqual(self.browser.headers['Status'], '200 Ok')
         self.assertEqual(self.browser.headers['Content-Type'],
                          'text/csv; charset=UTF-8')
         self.assertEqual(self.browser.contents,
             'code,title,title_prefix\r\nfac1,Unnamed Faculty,faculty\r\n')
-        logfile = os.path.join(
-            self.app['datacenter'].storage, 'logs', 'datacenter.log')
-        logcontent = open(logfile).read()
-        self.assertTrue('zope.mgr - browser.pages.ExportCSVView - '
-                        'exported: faculties' in logcontent)
-        return
+
+        # after download, the job and the result file are removed
+        manager = getUtility(IJobManager)
+        result = manager.get(job_id)
+        self.assertEqual(result, None)
+        self.assertEqual(self.stored_in_datacenter(job_id), False)
+        #logfile = os.path.join(
+        #    self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        #logcontent = open(logfile).read()
+        #self.assertTrue('zope.mgr - browser.pages.ExportCSVView - '
+        #                'exported: faculties' in logcontent)
+        return
+
+    def test_export_discard(self):
+        # we can discard a generated export result
+        self.test_export_start()
+        self.wait_for_export_job_completed()
+        self.browser.open(self.datacenter_path + '/@@export')
+        self.browser.getControl("Discard").click()
+        self.assertTrue('Discarded export result' in self.browser.contents)
+        return
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/hostels/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/hostels/browser.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/hostels/browser.py	(revision 9166)
@@ -22,5 +22,5 @@
 from zope.i18n import translate
 from zope.component import getUtility
-from waeup.kofa.browser import (
+from waeup.kofa.browser.layout import (
     KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
     NullValidator)
Index: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/students/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/students/browser.py	(revision 9165)
+++ main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/students/browser.py	(revision 9166)
@@ -33,7 +33,8 @@
     invalidate_accesscode, get_access_code)
 from waeup.kofa.accesscodes.workflow import USED
-from waeup.kofa.browser import (
+from waeup.kofa.browser.layout import (
     KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
-    ContactAdminForm, KofaForm, NullValidator)
+    KofaForm, NullValidator)
+from waeup.kofa.browser.pages import ContactAdminForm
 from waeup.kofa.browser.breadcrumbs import Breadcrumb
 from waeup.kofa.browser.resources import datepicker, datatable, tabs, warning
