Index: main/waeup.kofa/trunk/src/waeup/kofa/reports.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/reports.py	(revision 9507)
+++ main/waeup.kofa/trunk/src/waeup/kofa/reports.py	(revision 9510)
@@ -21,5 +21,6 @@
 import zc.async.interfaces
 from persistent.list import PersistentList
-from zope.component import getUtility, getUtilitiesFor
+from zope import schema
+from zope.component import getUtility, getUtilitiesFor, queryUtility
 from zope.component.hooks import setSite
 from zope.interface import implementer
@@ -27,5 +28,6 @@
 from waeup.kofa.async import AsyncJob
 from waeup.kofa.interfaces import (
-    IJobManager, JOB_STATUS_MAP,)
+    IJobManager, JOB_STATUS_MAP, IKofaPluggable, IKofaObject)
+from waeup.kofa.interfaces import MessageFactory as _
 from waeup.kofa.utils.helpers import now
 
@@ -33,4 +35,8 @@
     """A report.
     """
+    args = Attribute("""The args passed to constructor""")
+
+    kwargs = Attribute("""The keywords passed to constructor""")
+
     creation_dt = Attribute(
         """Datetime when a report was created. The datetime should """
@@ -42,15 +48,34 @@
         """
 
-class IReportGenerator(Interface):
+class IReportGenerator(IKofaObject):
     """A report generator.
     """
-    def generate(site):
+    title = Attribute("""Human readable description of report type.""")
+    def generate(site, args=[], kw={}):
         """Generate a report.
-        """
-        return Report()
+
+        `args` and `kw` are the parameters needed to create a specific
+        report (if any).
+        """
 
 class IReportJob(zc.async.interfaces.IJob):
-    def __init__(site, exporter_name):
-        pass
+
+    finished = schema.Bool(
+        title = u'`True` if the job finished.`',
+        default = False,
+        )
+
+    failed = schema.Bool(
+        title = u"`True` iff the job finished and didn't provide a report.",
+        default = None,
+        )
+
+    description = schema.TextLine(
+        title = u"""Textual representation of arguments and keywords.""",
+        default = u"",
+        )
+
+    def __init__(site, generator_name):
+        """Create a report job via generator."""
 
 class IReportJobContainer(Interface):
@@ -113,4 +138,14 @@
         """
 
+class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer,
+                        IKofaObject):
+    """A grok container that holds report jobs.
+    """
+
+class manageReportsPermission(grok.Permission):
+    """A permission to manage reports.
+    """
+    grok.name('waeup.manageReports')
+
 def get_generators():
     """Get available report generators.
@@ -128,13 +163,17 @@
     creation_dt = None
 
-    def __init__(self):
+    def __init__(self, args=[], kwargs={}):
+        self.args = args
+        self.kwargs = kwargs
         self.creation_dt = now()
 
     def create_pdf(self):
-        raise NotImplemented
+        raise NotImplementedError()
 
 @implementer(IReportGenerator)
 class ReportGenerator(object):
-    def generate(self, site):
+
+    title = _("Unnamed Report")
+    def generate(self, site, args=[], kw={}):
         result = Report()
         return result
@@ -179,6 +218,59 @@
     """
     def __init__(self, site, generator_name, args=[], kw={}):
+        self._generator_name = generator_name
         super(AsyncReportJob, self).__init__(
-            report_job, site, generator_name, *args, **kw)
+            report_job, site, generator_name, args=args, kw=kw)
+
+    @property
+    def finished(self):
+        """A job is marked `finished` if it is completed.
+
+        Please note: a finished report job does not neccessarily
+        provide an IReport result. See meth:`failed`.
+        """
+        return self.status == zc.async.interfaces.COMPLETED
+
+    @property
+    def failed(self):
+        """A report job is marked failed iff it is finished and the
+        result does not provide IReport.
+
+        While a job is unfinished, the `failed` status is ``None``.
+
+        Failed jobs normally provide a `traceback` to examine reasons.
+        """
+        if not self.finished:
+            return None
+        if not IReport.providedBy(self.result):
+            return True
+        return False
+
+    @property
+    def description(self):
+        """A description gives a representation of the report to generate.
+
+        The description contains the name of the report generator
+        (trying to fetch the `name` attribute of the requested report
+        generator) and the arguments and keywords passed in.
+
+        Please note that this method is expensive!
+        """
+        args = self.kwargs.get('args', [])
+        kw = self.kwargs.get('kw', dict())
+        args = ', '.join(["%r" % x for x in args])
+        kw = ', '.join(['%s=%r' % (key, val) for key, val in kw.items()])
+        if len(args) and len(kw):
+            str_repr = args + ', ' + kw
+        else:
+            str_repr = args + kw
+        str_repr = '(' + str_repr + ')'
+        try:
+            generator = queryUtility(
+                IReportGenerator, name=self._generator_name)
+            name = generator.name
+        except:
+            name = _('Invalid Report Generator')
+        return name + ' ' + str_repr
+
 
 @implementer(IReportJobContainer)
@@ -212,5 +304,5 @@
         # Make sure that the persisted list is stored in ZODB
         self.running_report_jobs = PersistentList(self.running_report_jobs)
-        self.running_report_jobs.append((job_id, generator_name, user_id))
+        self.running_report_jobs.append((job_id, generator_name, user_id),)
         return job_id
 
@@ -253,6 +345,4 @@
         for entry in entries:
             job = manager.get(entry[0])
-            if job is None:
-                continue
             status, status_translated = JOB_STATUS_MAP[job.status]
             generator = getUtility(IReportGenerator, name=entry[1])
@@ -287,2 +377,37 @@
                 return entry
         return None
+
+@implementer(IReportsContainer)
+class ReportsContainer(grok.Container, ReportJobContainer):
+    """A container for reports.
+    """
+
+@implementer(IKofaPluggable)
+class ReportsContainerPlugin(grok.GlobalUtility):
+    """A plugin that updates sites to contain a reports container.
+    """
+
+    grok.name('reports')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        """Add a reports container for `site`.
+
+        If there is such an object already, we install a fresh one.
+        """
+        if site.get('reports', None) is not None:
+            del site['reports']
+            logger.info('Removed reports container for site "%s"' % name)
+        return self.update(site, name, logger)
+
+    def update(self, site, name, logger):
+        """Install a reports container in `site`.
+
+        If one exists already, do nothing.
+        """
+        if site.get('reports', None) is not None:
+            return
+        site['reports'] = ReportsContainer()
+        logger.info('Added reports container for site "%s"' % name)
+        return
