Index: main/waeup.kofa/trunk/src/waeup/kofa/browser/reports.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/reports.py	(revision 9633)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/reports.py	(revision 9633)
@@ -0,0 +1,104 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Browser components for report generation.
+"""
+import grok
+from zope.component import getUtility
+from zope.location.location import located
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.browser.resources import tabs
+from waeup.kofa.reports import IReport, IReportsContainer
+from waeup.kofa.reports import get_generators
+
+grok.templatedir('templates')
+
+class ReportsContainerPage(KofaPage):
+    """A view on a reports container.
+    """
+    grok.name('index')
+    grok.context(IReportsContainer)
+    grok.require('waeup.manageReports')
+
+    def _report_url(self, job_id):
+        """Get the PDF download URL of a report.
+        """
+        return self.url(self.context, '%s/pdf' % job_id)
+
+    def update(self, job_id=None, DISCARD=None, DOWNLOAD=None):
+        self.entries = []
+        user_id = self.request.principal.id
+        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)
+        self.entries = self.context.get_report_jobs_description(user_id)
+        if job_id and DOWNLOAD:
+            self.redirect(self._report_url(job_id))
+            return
+        return
+
+
+class ReportsContainerTraverser(grok.Traverser):
+    """A traverser for reports containers.
+    """
+    grok.context(IReportsContainer)
+    def traverse(self, name):
+        """Return a report generator or report if one is registered under
+        `name`.
+
+        Generators are registered by their utility names while reports
+        are looked up by their job id. So, `name` must be a report
+        generator name or a valid job_id of a report job.
+        """
+        generators = dict(list(get_generators()))
+        result = generators.get(name, None)
+        if result:
+            # give generator a location in URLs (make url() work)
+            return located(result, self.context, name)
+        result = self.context.report_entry_from_job_id(name)
+        if result:
+            manager = getUtility(IJobManager)
+            job = manager.get(name)
+            report = job.result
+            return located(report, self.context, name)
+        return None
+
+class ReportsContainerCreate(KofaPage):
+    """Create a new report.
+    """
+    grok.name('create')
+    grok.context(IReportsContainer)
+    grok.require('waeup.manageReports')
+
+    def update(self, START_GENERATOR=None, generator=None):
+        self.creators = self.get_creators()
+        self.generator_names = [x[1] for x in self.creators]
+        if START_GENERATOR and generator and generator in self.generator_names:
+            self.redirect(self.url(self.context, generator))
+        pass
+
+    def get_creators(self):
+        """Get all registered report generator names.
+
+        Returns a list of tuples (<TITLE>, <NAME>) with ``<TITLE>``
+        being a human readable description of the respective generator
+        and ``<NAME>`` being the registration name with the ZCA.
+        """
+        result = [(gen.title, name) for name, gen in get_generators()]
+        return result
Index: main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/reportscontainercreate.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/reportscontainercreate.pt	(revision 9633)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/reportscontainercreate.pt	(revision 9633)
@@ -0,0 +1,50 @@
+<div i18n:domain="waeup.kofa">
+  <h2 i18n:translate="">Create Report</h2>
+  <div class="row">
+    <div class="span8 columns">
+      <div class="span8 columns">
+	<p i18n:translate="">
+	  Here you can create reports representing portal data.
+	</p>
+	<p i18n:translate="">
+	  Please pick the type of report you want to create from the
+	  selection below.
+	</p>
+	<p i18n:translate="">
+	  Afterwards you can configure the report (do the settings).
+	  The report data will be calculated and then made available
+	  for you to view/download.
+	</p>
+      </div>
+      <form  method="POST">
+      <fieldset>
+
+	<div class="clearfix">
+	  <label for="generator">Report Type:</label>
+	  <div class="input">
+	    <select name="generator">
+	      <span tal:repeat="items view/creators" tal:omit-tag="">
+		<option
+		    tal:define="name python: items[1]; title python: items[0]"
+		    tal:attributes="value name">
+		  <span tal:replace="title">TITLE</span>
+		</option>
+	      </span>
+	    </select>
+	    <span class="help-inline" i18n:translate="">
+	      Type of report to create
+	    </span>
+	  </div>
+
+	</div>
+	<div class="input">
+	  <input i18n:translate="" type="submit" class="btn primary"
+		 name="START_GENERATOR" value="Configure" />
+	</div>
+
+      </fieldset>
+    </form>
+
+    </div>
+  </div>
+</div>
Index: main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/reportscontainerpage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/reportscontainerpage.pt	(revision 9633)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/reportscontainerpage.pt	(revision 9633)
@@ -0,0 +1,41 @@
+<h2>Reports</h2>
+<div class="span12">
+  <form method="POST">
+    <a href="" class="btn primary">Reload</a>
+    <a href="create" class="btn secondary">Create new</a>
+  </form>
+</div>
+<hr />
+<div tal:repeat="entry view/entries" class="row">
+  <div class="span12">
+    <form method="POST">
+      <div class="span10">
+	<b>Report <span tal:replace="python: entry[0]">12</span>:</b>
+	<span tal:replace="python: entry[1]">Report Description</span>
+      </div>
+      <div class="span10">
+	status:
+	<b><span tal:replace="python: entry[2]">status</span></b>
+      </div>
+      <div class="span10">
+	<input type="submit" class="btn secondary"
+	       name="DISCARD" value="Discard" />
+	<input type="submit" class="btn secondary"
+	       name="DOWNLOAD" value="Download" />
+      </div>
+      <div class="span6">
+	<input type="hidden" name="job_id"
+	       tal:attributes="value python: entry[0]" />
+      </div>
+      <!-- span tal:replace="entry">ENRY</span -->
+    </form>
+    <hr />
+  </div>
+</div>
+
+<div class="span12">
+  <form method="POST">
+    <a href="" class="btn primary">Reload</a>
+    <a href="create" class="btn secondary">Create new</a>
+  </form>
+</div>
Index: main/waeup.kofa/trunk/src/waeup/kofa/reports.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/reports.py	(revision 9632)
+++ main/waeup.kofa/trunk/src/waeup/kofa/reports.py	(revision 9633)
@@ -44,4 +44,14 @@
         """not when any output was created.""")
 
+    title = schema.TextLine(
+        title = u"A human readable short description for a report.",
+        default = u'Untitled',
+        )
+
+    description = schema.Text(
+        title = u"A human readable text describing a report.",
+        default = u'No description'
+        )
+
     def create_pdf():
         """Generate a PDF copy.
@@ -121,4 +131,14 @@
         """
 
+    def get_report_jobs_description(user_id=None):
+        """Get running/completed report jobs fur ?user_id` as list of tuples.
+
+        Each tuple holds ``<job_id>, <description>,
+        <status_translated>`` in that order, where ``<description>``
+        is a human readable description of the report run and
+        ``<status_translated>`` is the report jobs' status
+        (translated).
+        """
+
     def delete_report_entry(entry):
         """Delete the report job denoted by `entry`.
@@ -162,4 +182,12 @@
 class Report(object):
     creation_dt = None
+
+    @property
+    def title(self):
+        return _(u'A report')
+
+    @property
+    def description(self):
+        return _(u'A dummy report')
 
     def __init__(self, args=[], kwargs={}):
@@ -170,4 +198,8 @@
     def create_pdf(self):
         raise NotImplementedError()
+
+    def __repr__(self):
+        return 'Report(args=%r, kwargs=%r)' % (self.args, self.kwargs)
+
 
 @implementer(IReportGenerator)
@@ -266,9 +298,9 @@
         str_repr = '(' + str_repr + ')'
         try:
-            generator = queryUtility(
+            generator = getUtility(
                 IReportGenerator, name=self._generator_name)
-            name = generator.name
+            name = generator.title
         except:
-            name = _('Invalid Report Generator')
+            name = _('Unregistered Report Generator')
         return name + ' ' + str_repr
 
@@ -351,4 +383,24 @@
         return result
 
+    def get_report_jobs_description(self, user_id=None):
+        """Get running/completed report jobs fur ?user_id` as list of tuples.
+
+        Each tuple holds ``<job_id>, <description>,
+        <status_translated>`` in that order, where ``<description>``
+        is a human readable description of the report run and
+        ``<status_translated>`` is the report jobs' status
+        (translated).
+        """
+        entries = self.get_running_report_jobs(user_id)
+        result = []
+        for (job_id, gen_name, user_name) in entries:
+            manager = getUtility(IJobManager)
+            job = manager.get(job_id)
+            status = JOB_STATUS_MAP.get(job.status, job.status)[1]
+            if not hasattr(job, 'description'):
+                continue
+            result.append((job_id, job.description, status),)
+        return result
+
     def delete_report_entry(self, entry):
         """Delete the report job denoted by `entry`.
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/studentreportgeneratorpage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/studentreportgeneratorpage.pt	(revision 9633)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/studentreportgeneratorpage.pt	(revision 9633)
@@ -0,0 +1,50 @@
+<div i18n:domain="waeup.kofa">
+  <h2 i18n:translate="">Create Report: Students by Session</h2>
+
+  <div class="row">
+    <div class="span8 columns">
+      <div class="span8 columns">
+	<p i18n:translate="">
+	  Please pick the session for which the report should be
+	  generated.
+	</p>
+	<p i18n:translate="">
+	  The report data will be calculated and then made available
+	  for you to view/download.
+	</p>
+      </div>
+      <form  method="POST">
+      <fieldset>
+
+	<div class="clearfix">
+	  <label for="session">Academic Session:</label>
+	  <div class="input">
+	    <select name="session">
+	      <span tal:repeat="items view/sessions" tal:omit-tag="">
+		<option
+		    tal:define="name python: items[1]; title python: items[0]"
+		    tal:attributes="value name">
+		  <span tal:replace="title">TITLE</span>
+		</option>
+	      </span>
+	    </select>
+	    <span class="help-inline" i18n:translate="">
+	      Academic session to report
+	    </span>
+	  </div>
+
+	</div>
+	<div class="input">
+	  <input i18n:translate="" type="submit" class="btn primary"
+		 name="CREATE" value="Create" />
+	  <a class="btn secondary" href=""
+	     tal:attributes="href view/parent_url">Abort</a>
+	</div>
+
+      </fieldset>
+    </form>
+
+    </div>
+  </div>
+
+</div>
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/reports.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/reports.py	(revision 9633)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/reports.py	(revision 9633)
@@ -0,0 +1,217 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.interface import implementer, Interface
+from waeup.kofa.interfaces import (
+     academic_sessions_vocab, registration_states_vocab)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.reports import IReport
+
+class IStudentReport(IReport):
+    pass
+
+def get_students_by_session(session):
+    """Get students of a certain session.
+
+    Returns a table ordered by faculty code (one per row) and
+    registration state (cols). The result is a 3-tuple representing
+    ((<FACULTY_CODES>), (<STATES>), (<NUM_OF_STUDENTS>)). The
+    (<NUM_OF_STUDENTS>) is an n-tuple with each entry containing the
+    number of students found in that faculty and with the respective
+    state.
+
+    Sample result:
+
+      >>> ((u'FAC1', u'FAC2'),
+      ...  ('created', 'accepted', 'registered'),
+      ...  ((12, 10, 1), (0, 5, 25)))
+
+    This result means: there are 5 students in FAC2 in state
+    'accepted' while 12 students in 'FAC1' are in state 'created'.
+    """
+    site = grok.getSite()
+    states = tuple([x.value for x in registration_states_vocab])
+    states = states + (u'Total',)
+    fac_codes = tuple(sorted([x for x in site['faculties'].keys()],
+                             key=lambda x: x.lower()))
+    fac_codes = fac_codes + ('Total',)
+    # XXX: Here we do _one_ query and then examine the single
+    #   students. One could also do multiple queries and just look for
+    #   the result length (not introspecting the delivered students
+    #   further).
+    cat = queryUtility(ICatalog, name="students_catalog")
+    result = cat.searchResults(current_session=(session, session))
+    table = [[0 for x in xrange(len(states))] for y in xrange(len(fac_codes))]
+    for stud in result:
+        row = fac_codes.index(stud.faccode)
+        col = states.index(stud.state)
+        table[row][col] += 1
+        table[-1][col] += 1
+        table[row][-1] += 1
+        table[-1][-1] += 1
+    # turn lists into tuples
+    table = tuple([tuple(row) for row in table])
+    return (fac_codes, states, table)
+
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, Table, Spacer
+from waeup.kofa.reports import IReport, IReportGenerator
+from waeup.kofa.reports import Report
+from waeup.kofa.browser.interfaces import IPDFCreator
+
+STYLE = getSampleStyleSheet()
+
+def tbl_data_to_table(row_names, col_names, data):
+    result = []
+    new_col_names = []
+    for name in col_names:
+        new_col_names.append(name.replace(' ', '\n'))
+    head = [''] + list(new_col_names)
+    result = [head]
+    for idx, row_name in enumerate(row_names):
+        row = [row_name] + list(data[idx])
+        result.append(row)
+    return result
+
+TABLE_STYLE = [
+    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    ('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8),
+    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8),
+    ('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8),
+    ('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8),
+    ('ALIGN', (1,1), (-1,-1), 'RIGHT'),
+    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+    ('LINEBELOW', (0,-1), (-1,-1), 0.25, colors.black),
+    ('LINEAFTER', (-1,0), (-1,-1), 0.25, colors.black),
+    ('LINEBEFORE', (-1,0), (-1,-1), 1.0, colors.black),
+    ('LINEABOVE', (0,-1), (-1,-1), 1.0, colors.black),
+    ('LINEABOVE', (0,0), (-1,0), 0.25, colors.black),
+    ]
+
+@implementer(IStudentReport)
+class StudentReport(Report):
+    data = None
+    session = None
+
+    def __init__(self, session, author='System'):
+        super(StudentReport, self).__init__(
+            args=[session], kwargs={'author':author})
+        self.session = session
+        self.author = author
+        self.data = get_students_by_session(session)
+
+    def __repr__(self):
+        return 'StudentReport(session=%s, author=%r)' % (
+            self.session, self.author)
+
+    def get_data(self):
+        self.data = get_students_by_session(self.session)
+
+    def create_pdf(self):
+        creator = getUtility(IPDFCreator)
+        table_data = tbl_data_to_table(*self.data)
+        col_widths = [None,] + [1.6*cm] * len(self.data[1]) + [None,]
+        pdf_data = [Paragraph('<b>Session:</b> %s' % self.session,
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [
+            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
+        doc_title = 'Report: Students of Session %s\nby faculty and status' % (
+            self.session,)
+        pdf = creator.create_pdf(
+            pdf_data, None, doc_title, self.author,
+            'Session %s Students,' % self.session)
+        open('/tmp/result.pdf', 'wb').write(pdf)
+        return pdf
+
+@implementer(IReportGenerator)
+class StudentReportGenerator(grok.GlobalUtility):
+
+    title = _('Students by Session')
+    grok.name('students_by_session')
+
+    @property
+    def description(self):
+        return self.title
+
+    def generate(self, site, session=None, author=None):
+        result = StudentReport(session=session, author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.interfaces import academic_sessions_vocab
+from waeup.kofa.reports import get_generators
+grok.templatedir('browser_templates')
+class StudentReportGeneratorPage(KofaPage):
+
+    grok.context(StudentReportGenerator)
+    grok.name('index.html')
+    grok.require('waeup.manageReports')
+
+    @property
+    def generator_name(self):
+        for name, gen in get_generators():
+            if gen == self.context:
+                return name
+        return None
+
+    def update(self, CREATE=None, session=None):
+        self.parent_url = self.url(self.context.__parent__)
+        self._set_session_values()
+        if CREATE and session:
+            # create a new report job for students by session
+            container = self.context.__parent__
+            user_id = self.request.principal.id
+            kw = dict(session=self._value_dict[session])
+            self.flash(_('New report is being created in background'))
+            container.start_report_job(
+                self.generator_name, user_id, kw=kw)
+            self.redirect(self.parent_url)
+            return
+        return
+
+    def _set_session_values(self):
+        vocab_terms = academic_sessions_vocab.by_value.values()
+        self.sessions = [(x.title, x.token) for x in vocab_terms]
+        self._value_dict = dict([(x.token, x.value) for x in vocab_terms])
+        return
+
+
+class StudentReportPDFView(grok.View):
+
+    grok.context(IStudentReport)
+    grok.name('pdf')
+    grok.require('waeup.Public')
+
+    def render(self):
+        filename = 'report.pdf'
+        self.response.setHeader(
+            'Content-Type', 'application/pdf')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' % filename)
+        pdf_stream = self.context.create_pdf()
+        return pdf_stream
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_reports.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_reports.py	(revision 9633)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_reports.py	(revision 9633)
@@ -0,0 +1,58 @@
+# Tests for student related reports
+from waeup.kofa.students.reports import get_students_by_session, StudentReport
+from waeup.kofa.students.tests.test_catalog import CatalogTestSetup
+from waeup.kofa.testing import FunctionalLayer
+
+class StudentReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    states = ('created', 'admitted', 'clearance started',
+              'clearance requested', 'cleared', 'school fee paid',
+              'returning', 'courses registered', 'courses validated', 'Total')
+
+    def test_get_students_by_session_simple(self):
+        # we can get a table with one student
+        result1 = get_students_by_session(2010)
+        result2 = get_students_by_session(2009)
+        self.assertEqual(
+            result1,
+            ((u'fac1', u'Total',),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+              (1, 0, 0, 0, 0, 0, 0, 0, 0, 1),)))
+        self.assertEqual(
+            result2,
+            ((u'fac1', u'Total'),
+             self.states,
+             ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),)))
+        return
+
+    def test_get_students_by_session_multiple(self):
+        # we can get a table with several students
+        self.create_cert('fac2', 'dept2', 'CERT2')
+        result1 = get_students_by_session(2010)
+        result2 = get_students_by_session(2009)
+        self.assertEqual(
+            result1,
+            ((u'fac1', u'fac2', u'Total'),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (1, 0, 0, 0, 0, 0, 0, 0, 0, 1),)))
+        self.assertEqual(
+            result2,
+            ((u'fac1', u'fac2', u'Total'),
+             self.states,
+             ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),)))
+        return
+
+    def test_create_pdf(self):
+        self.create_cert('FAC2', 'dept2', 'CERT2')
+        report = StudentReport(2010)
+        result = report.create_pdf()
+        self.assertTrue(result.startswith('%PDF-'))
+        return
Index: main/waeup.kofa/trunk/src/waeup/kofa/tests/test_reports.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/tests/test_reports.py	(revision 9632)
+++ main/waeup.kofa/trunk/src/waeup/kofa/tests/test_reports.py	(revision 9633)
@@ -25,5 +25,5 @@
 
     def __init__(self, name=None, perm_create=None, perm_view=None):
-        self.name = 'Report 1'
+        self.title = 'Report 1'
         self.perm_create = perm_create
         self.perm_view = perm_view
@@ -31,7 +31,7 @@
 
     def __eq__(self, obj):
-        if getattr(obj, 'name', None) is None:
+        if getattr(obj, 'title', None) is None:
             return False
-        return self.name == obj.name
+        return self.title == obj.title
 
     def generate(self, site, args=[], kw={}):
@@ -142,6 +142,6 @@
         self.assertEqual(
             result,
-            [('report1', FakeReportGenerator('report1')),
-             ('report2', FakeReportGenerator('report2'))])
+            [(u'report1', FakeReportGenerator('report1')),
+             (u'report2', FakeReportGenerator('report2'))])
         return
 
@@ -238,29 +238,25 @@
         job = AsyncReportJob(self.root_folder, 'NOT EXISTENT')
         self.assertEqual(
-            job.description, u'Invalid Report Generator ()')
+            job.description, u'Unregistered Report Generator ()')
         # with args set (no kws)
         job = AsyncReportJob(
             self.root_folder, 'NOT EXISTENT', args=['a', 'b'])
         self.assertEqual(
-            job.description, u"Invalid Report Generator ('a', 'b')")
+            job.description, u"Unregistered Report Generator ('a', 'b')")
         # with kw set (no args)
         job = AsyncReportJob(
             self.root_folder, 'NOT_EXISTENT', kw=dict(a=1, b=2))
         self.assertEqual(
-            job.description, u'Invalid Report Generator (a=1, b=2)')
+            job.description, u'Unregistered Report Generator (a=1, b=2)')
         # with args and kws set
         job = AsyncReportJob(
             self.root_folder, 'NOT_EXISTENT', args=['a'], kw=dict(b=2))
         self.assertEqual(
-            job.description, u"Invalid Report Generator ('a', b=2)")
-        return
-
+            job.description, u"Unregistered Report Generator ('a', b=2)")
+        return
 
 class FakeJobWithResult(FakeJob):
 
     def __init__(self, args=[], kw={}):
-        #self.dir_path = tempfile.mkdtemp()
-        #self.result = os.path.join(self.dir_path, 'fake.csv')
-        #open(self.result, 'wb').write('a fake result')
         self.result = Report()
         self.result.args = args
@@ -358,6 +354,19 @@
         self.assertEqual(
             result,
-            [('new', u'new', u'Unnamed Report'),
-             ('completed', u'completed', u'Unnamed Report')]
+            [('new', u'new', u'Report 1'),
+             ('completed', u'completed', u'Report 1')]
+            )
+        return
+
+    def test_get_report_job_description(self):
+        # we can get the descriptions of jobs...
+        container = ReportJobContainer()
+        container.start_report_job('report1', 'alice')
+        container.start_report_job('report1', 'bob')
+        result = container.get_report_jobs_description(user_id='bob')
+        # we'll get the job id, a description and the status
+        self.assertEqual(
+            result,
+            [('2', 'Report 1 ()', u'new'),]
             )
         return
