Index: main/waeup.ikoba/trunk/CHANGES.txt
===================================================================
--- main/waeup.ikoba/trunk/CHANGES.txt	(revision 14172)
+++ main/waeup.ikoba/trunk/CHANGES.txt	(revision 14173)
@@ -4,4 +4,6 @@
 0.2.dev0 (unreleased)
 =====================
+
+* Add reports and exports purge buttons.
 
 * Add portal maintenance mode.
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py	(revision 14172)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py	(revision 14173)
@@ -196,7 +196,8 @@
     return
 
-def doll_up(view, user=None):
+def doll_up(view, user=None, purge=False):
     """Doll up export jobs for displaying in table.
     """
+    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
     job_entries = view.context.get_running_export_jobs(user)
     job_manager = getUtility(IJobManager)
@@ -222,4 +223,10 @@
                                job.failed and time_delta and \
                                time_delta.days < 1
+        if not show_download_button and purge:
+            entry = view.context.entry_from_job_id(job_id)
+            view.context.delete_export_entry(entry)
+            view.context.logger.info(
+                '%s - purged: job_id=%s' % (ob_class, job_id))
+            continue
         new_entry = dict(
             job=job_id,
@@ -1628,5 +1635,5 @@
         return sorted(title_name_tuples)
 
-    def update(self, CREATE=None, DISCARD=None, exporter=None,
+    def update(self, CREATE=None, DISCARD=None, PURGE=None, exporter=None,
                job_id=None, CANCEL=None):
         if CANCEL is not None:
@@ -1657,5 +1664,7 @@
                 '%s - discarded: job_id=%s' % (ob_class, job_id))
             self.flash(_('Discarded export') + ' %s' % job_id)
-        self.entries = doll_up(self, user=None)
+        self.entries = doll_up(self, user=None, purge=PURGE)
+        if PURGE:
+            self.flash(_('Exports successfully purged.'))
         return
 
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/browser/reports.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/browser/reports.py	(revision 14172)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/browser/reports.py	(revision 14173)
@@ -19,9 +19,11 @@
 """
 import grok
+from datetime import datetime, timedelta
 from zope.component import getUtility, queryUtility
 from zope.location.location import located
+from zope.security import checkPermission
 from waeup.ikoba.interfaces import IJobManager, IIkobaUtils
 from waeup.ikoba.interfaces import MessageFactory as _
-from waeup.ikoba.browser.layout import IkobaPage
+from waeup.ikoba.browser.layout import IkobaPage, jsaction
 from waeup.ikoba.reports import IReportsContainer, IReportGenerator
 from waeup.ikoba.reports import get_generators
@@ -42,17 +44,36 @@
         return self.url(self.context, '%s/pdf' % job_id)
 
-    def update(self, job_id=None, DISCARD=None, DOWNLOAD=None):
+    def update(self, job_id=None, DISCARD=None, DOWNLOAD=None, PURGE=None):
         self.entries = []
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.', '')
         if job_id and DISCARD:
             entry = self.context.report_entry_from_job_id(job_id)
             self.context.delete_report_entry(entry)
-            self.flash('Report discarded: %s' % job_id)
-            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            self.flash('Report %s discarded' % job_id)
             grok.getSite().logger.info(
                 '%s - report %s discarded' % (ob_class, job_id))
-        self.entries = self._generate_entries(user_id=None)
+        if not checkPermission('waeup.manageReports', self.context):
+            user_id = get_current_principal().id
+        else:
+            user_id = None
+        self.entries = self._generate_entries(user_id=user_id)
         if job_id and DOWNLOAD:
             self.redirect(self._report_url(job_id))
             return
+        if PURGE:
+            counter = 0
+            for entry in self.context.get_running_report_jobs(user_id=user_id):
+                job_id, gen_name, user = entry
+                job = getUtility(IJobManager).get(job_id)
+                starttime = getattr(job, 'begin_after', None)
+                delta = timedelta(weeks=4)
+                tz = getUtility(IIkobaUtils).tzinfo
+                if datetime.now(tz) - delta > starttime:
+                    self.context.delete_report_entry(entry)
+                    counter += 1
+                    grok.getSite().logger.info(
+                        '%s - report %s purged' % (ob_class, job_id))
+            self.flash('%s report(s) purged.' % counter)
+            self.redirect(self.url(self.context))
         return
 
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/datacenterexportpage.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/datacenterexportpage.pt	(revision 14172)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/datacenterexportpage.pt	(revision 14173)
@@ -79,5 +79,6 @@
           <a href="" class="btn btn-primary btn-xs"
                  tal:attributes="href job/download_url"
-                 tal:condition="job/show_download_button">
+                 tal:condition="job/show_download_button"
+                 i18n:translate="">
             Download</a>
           <input type="hidden" name="job_id"
@@ -85,5 +86,6 @@
           <input type="submit" class="btn btn-default btn-xs"
                  name="DISCARD" value="Discard"
-                 tal:condition="job/show_discard_button" />
+                 tal:condition="job/show_discard_button"
+                 i18n:translate=""/>
         </form>
       </td>
@@ -91,2 +93,13 @@
   </tbody>
 </table>
+
+<form method="POST" i18n:domain="waeup.ikoba">
+  <div class="form-group">
+    <input type="submit" class="btn btn-primary"
+      name="PURGE" value="Purge outdated exports"
+      data-toggle="tooltip"
+      title="All exports older than 24 hours will be discarded!"
+      i18n:translate=""
+      />
+  </div>
+</form>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/reportscontainerpage.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/reportscontainerpage.pt	(revision 14172)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/reportscontainerpage.pt	(revision 14173)
@@ -48,7 +48,13 @@
 </table>
 <form method="POST" i18n:domain="waeup.ikoba">
-  <a tal:attributes="href python:view.url(context, 'create')"
-     class="btn btn-primary" i18n:translate="">
-    Create new report
-  </a>
+  <div class="form-group">
+    <a tal:attributes="href python:view.url(context, 'create')"
+       class="btn btn-primary" i18n:translate="">
+      Create new report
+    </a>
+    <input type="submit" class="btn btn-default"
+      name="PURGE" value="Purge outdated reports"
+      data-toggle="tooltip" title="All reports older than 4 weeks will be discarded!"
+      onclick="return window.confirm('Are you really sure?')"/>
+  </div>
 </form>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/browser/tests/test_browser.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/browser/tests/test_browser.py	(revision 14172)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/browser/tests/test_browser.py	(revision 14173)
@@ -24,4 +24,5 @@
 import os
 import grok
+from datetime import datetime, timedelta
 from zc.async.testing import wait_for_result
 from zope.component import createObject, getUtility
@@ -32,5 +33,5 @@
 from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
 from waeup.ikoba.app import Company
-from waeup.ikoba.interfaces import IJobManager, IUserAccount
+from waeup.ikoba.interfaces import IJobManager, IUserAccount, IIkobaUtils
 from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
 
@@ -83,4 +84,13 @@
         wait_for_result(job)
         return job_id
+
+    def trigger_export(self):
+        # helper to start an export. Make sure to remove the result file
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.browser.getLink("Export data").click()
+        self.browser.getControl(name="exporter").value = ['products']
+        self.browser.getControl("Create CSV file").click()
+        return
 
     def cleanup_run_job(self):
@@ -194,4 +204,79 @@
         return
 
+    def test_export_download(self):
+        # we can download a generated export result
+        self.trigger_export()
+        # while the export file is created, we get a reload button
+        # (or a loading bar if javascript is enabled)...
+        self.assertTrue('Reload' in self.browser.contents)
+        # ...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()
+        self.browser.open(self.datacenter_path + '/export')
+        self.assertFalse('Reload' in self.browser.contents)
+        # ...we can download the result
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'text/csv; charset=UTF-8')
+        self.assertEqual(self.browser.headers['content-disposition'],
+            'attachment; filename="WAeUP.Ikoba_products_%s.csv' % job_id)
+        self.assertEqual(self.browser.contents,
+            'contract_category,contract_title,description,options,'
+            'product_id,terms_and_conditions,title,valid_from,valid_to,'
+            'users_with_local_roles\r\n')
+        # Thew job can be discarded
+        self.browser.open(self.datacenter_path + '/export')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - browser.pages.ExportCSVPage - exported: '
+                        'products, job_id=' in logcontent)
+        self.assertTrue('zope.mgr - browser.pages.ExportCSVView - '
+                        'downloaded: WAeUP.Ikoba_products_%s.csv, job_id=%s'
+                        % (job_id, job_id) in logcontent)
+        return
+
+    def test_export_discard(self):
+        # we can discard a generated export result
+        self.trigger_export()
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(self.datacenter_path + '/@@export')
+        self.browser.getControl("Discard").click()
+        self.assertTrue('Discarded export' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVPage - discarded: job_id=%s'
+            % job_id in logcontent)
+        return
+
+    def test_export_purge(self):
+        self.trigger_export()
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(self.datacenter_path + '/export')
+        self.browser.getControl("Purge").click()
+        # Flash message always says successful ...
+        self.assertTrue(
+            'Exports successfully purged.' in self.browser.contents)
+        # ... but the export is still there
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+
+        job_id, gen_name, user = self.app['datacenter'].get_running_export_jobs()[0]
+        job = getUtility(IJobManager).get(job_id)
+        tz = getUtility(IIkobaUtils).tzinfo
+        delta = timedelta(days=5)
+        setattr(job, '_begin_after', datetime.now(tz) - delta)
+        self.browser.getControl("Purge").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - browser.pages.ExportCSVPage - '
+                        'purged: job_id=%s' % job_id in logcontent)
+        return
+
 
 class SupplementaryBrowserTests(CompanySetup):
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py	(revision 14172)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py	(revision 14173)
@@ -1,8 +1,9 @@
 # Tests for contract related reports
 import os
+from datetime import datetime, timedelta
 from zc.async.testing import wait_for_result
 from zope.interface.verify import verifyClass, verifyObject
 from zope.component import getUtility
-from waeup.ikoba.interfaces import IJobManager
+from waeup.ikoba.interfaces import IJobManager, IIkobaUtils
 from waeup.ikoba.customers.reports.contract_statistics import (
     get_contract_stats, ContractStatisticsReport, IContractStatisticsReport)
@@ -118,2 +119,24 @@
             )
         return
+
+    def test_report_purge(self):
+        self.trigger_report_creation()
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Purge").click()
+        self.assertTrue('0 report(s) purged' in self.browser.contents)
+        job_id, gen_name, user = self.app['reports'].get_running_report_jobs(user_id=None)[0]
+        job = getUtility(IJobManager).get(job_id)
+        tz = getUtility(IIkobaUtils).tzinfo
+        delta = timedelta(weeks=5)
+        setattr(job, '_begin_after', datetime.now(tz) - delta)
+        self.browser.getControl("Purge").click()
+        self.assertTrue('1 report(s) purged' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s purged' % job_id in logcontent
+            )
+        return
