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
