source: main/waeup.kofa/trunk/src/waeup/kofa/students/reports/student_statistics.py @ 14587

Last change on this file since 14587 was 14514, checked in by Henrik Bettermann, 8 years ago

Adjust reports: Change footer and filename formats.

  • Property svn:keywords set to Id
File size: 12.3 KB
Line 
1## $Id: student_statistics.py 14514 2017-02-08 07:39:08Z henrik $
2##
3## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18import grok
19from zope.i18n import translate
20from zope.catalog.interfaces import ICatalog
21from zope.component import queryUtility, getUtility
22from zope.interface import implementer, Interface, Attribute
23from waeup.kofa.interfaces import (
24    IKofaUtils,
25    academic_sessions_vocab, registration_states_vocab)
26from waeup.kofa.students.vocabularies import StudyLevelSource
27from waeup.kofa.interfaces import MessageFactory as _
28from waeup.kofa.reports import IReport
29
30class IStudentStatisticsReport(IReport):
31
32    session = Attribute('Session to report')
33    level = Attribute('Level to report')
34    mode = Attribute('Study modes group to report')
35    creation_dt_string = Attribute('Human readable report creation datetime')
36
37def get_student_stats(session, mode, level, breakdown):
38    """Get students in a certain session, study mode and current level.
39
40    Returns a table ordered by code (faculty or department, one per row) and
41    registration state (cols). The result is a 3-tuple representing
42    ((<PATHS>), (<STATES>), (<NUM_OF_STUDENTS>)). The
43    (<NUM_OF_STUDENTS>) is an n-tuple with each entry containing the
44    number of students found in that faculty/department and with the respective
45    state.
46
47    Sample result:
48
49      >>> ((u'FAC1', u'FAC2'),
50      ...  ('created', 'accepted', 'registered'),
51      ...  ((12, 10, 1), (0, 5, 25)))
52
53    This result means: there are 5 students in FAC2 in state
54    'accepted' while 12 students in 'FAC1' are in state 'created'.
55    """
56    site = grok.getSite()
57    states = tuple([x.value for x in registration_states_vocab])
58    states = states + (u'Total',)
59    if breakdown == 'faccode':
60        paths = tuple(sorted([x for x in site['faculties'].keys()],
61                                 key=lambda x: x.lower()))
62    elif breakdown == 'depcode':
63        faculties = site['faculties']
64        deppaths = []
65        for fac in faculties.values():
66            for dep in fac.values():
67                deppaths.append('%s/%s' % (fac.code, dep.code))
68        paths = tuple(sorted([x for x in deppaths],
69                                 key=lambda x: x.lower()))
70    # XXX: Here we do _one_ query and then examine the single
71    #   students. One could also do multiple queries and just look for
72    #   the result length (not introspecting the delivered students
73    #   further).
74    cat = queryUtility(ICatalog, name="students_catalog")
75    result = cat.searchResults(current_session=(session, session))
76    table = [[0 for x in xrange(len(states))] for y in xrange(len(paths)+1)]
77    mode_groups = getUtility(IKofaUtils).MODE_GROUPS
78    for stud in result:
79        if mode != 'All' and stud.current_mode \
80            and stud.current_mode not in mode_groups[mode]:
81            continue
82        if level != 0 and stud.current_level != level:
83            continue
84        if getattr(stud, breakdown) is None:
85            continue
86        if breakdown == 'faccode':
87            row_name = getattr(stud, 'faccode')
88        elif breakdown == 'depcode':
89            row_name = '%s/%s' % (
90                getattr(stud, 'faccode'), getattr(stud, 'depcode'))
91        row = paths.index(row_name)
92        col = states.index(stud.state)
93        table[row][col] += 1
94        table[-1][col] += 1
95        table[row][-1] += 1
96        table[-1][-1] += 1
97    # turn lists into tuples
98    table = tuple([tuple(row) for row in table])
99
100    paths = paths + (u'Total',)
101    return (paths, states, table)
102
103from reportlab.lib import colors
104from reportlab.lib.styles import getSampleStyleSheet
105from reportlab.lib.units import cm
106from reportlab.platypus import Paragraph, Table, Spacer
107from waeup.kofa.reports import IReport, IReportGenerator
108from waeup.kofa.reports import Report
109from waeup.kofa.browser.interfaces import IPDFCreator
110
111STYLE = getSampleStyleSheet()
112
113def tbl_data_to_table(row_names, col_names, data):
114    result = []
115    new_col_names = []
116    for name in col_names:
117        new_col_names.append(name.replace(' ', '\n'))
118    head = [''] + list(new_col_names)
119    result = [head]
120    for idx, row_name in enumerate(row_names):
121        row = [row_name] + list(data[idx])
122        result.append(row)
123    return result
124
125TABLE_STYLE = [
126    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
127    ('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8),
128    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8),
129    ('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8),
130    ('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8),
131    ('ALIGN', (1,1), (-1,-1), 'RIGHT'),
132    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
133    ('LINEBELOW', (0,-1), (-1,-1), 0.25, colors.black),
134    ('LINEAFTER', (-1,0), (-1,-1), 0.25, colors.black),
135    ('LINEBEFORE', (-1,0), (-1,-1), 1.0, colors.black),
136    #('LINEABOVE', (0,-1), (-1,-1), 1.0, colors.black),
137    #('LINEABOVE', (0,0), (-1,0), 0.25, colors.black),
138    ]
139
140@implementer(IStudentStatisticsReport)
141class StudentStatisticsReport(Report):
142    data = None
143    session = None
144    mode = None
145    pdfcreator = 'landscape'
146
147    def __init__(self, session, mode, level, breakdown, author='System'):
148        super(StudentStatisticsReport, self).__init__(
149            args=[session, mode, breakdown], kwargs={'author':author})
150        self.sessioncode = session
151        self.levelcode = level
152        self.studylevelsource = StudyLevelSource().factory
153        self.portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
154        self.session = academic_sessions_vocab.getTerm(session).title
155        self.mode = mode
156        if level == 0:
157            self.level = 'All levels'
158        else:
159            self.level = translate(
160                self.studylevelsource.getTitle(None, int(level)),
161                'waeup.kofa', target_language=self.portal_language)
162        self.breakdown = breakdown
163        self.author = author
164        self.creation_dt_string = self.creation_dt.astimezone(
165            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
166        self.data = get_student_stats(session, mode, level, breakdown)
167
168    def create_pdf(self, job_id):
169        creator = getUtility(IPDFCreator, name=self.pdfcreator)
170        table_data = tbl_data_to_table(*self.data)
171        col_widths = [None,] + [1.6*cm] * len(self.data[1]) + [None,]
172        pdf_data = [Paragraph('<b>%s - Report %s</b>'
173                              % (self.creation_dt_string, job_id),
174                              STYLE["Normal"]),
175                    Spacer(1, 12),]
176        pdf_data += [Paragraph(
177                    translate(
178                        'Study Mode: ${a}<br />'
179                        'Academic Session: ${b}<br />Level: ${c}<br />',
180                        mapping = {'a':self.mode,
181                                   'b':self.session,
182                                   'c':self.level,
183                                   }),
184                    STYLE["Normal"]),
185                    Spacer(1, 12),]
186        pdf_data += [
187            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
188        doc_title = translate(_('Student Statistics'))
189        right_footer = translate(
190            _('${a} Students - ${b} -',
191            mapping = {'a':self.mode, 'b':self.session}))
192        pdf = creator.create_pdf(
193            pdf_data, None, doc_title, self.author, right_footer
194            )
195        return pdf
196
197@implementer(IReportGenerator)
198class StudentStatisticsReportGenerator(grok.GlobalUtility):
199
200    title = _('Student Statistics')
201    grok.name('student_stats')
202
203    def generate(
204        self, site, session=None, mode=None,
205        level=None, breakdown=None, author=None):
206        result = StudentStatisticsReport(
207            session=session, mode=mode, level=level,
208            breakdown=breakdown, author=author)
209        return result
210
211###############################################################
212## Browser related stuff
213##
214## XXX: move to local browser module
215###############################################################
216from waeup.kofa.browser.layout import KofaPage
217from waeup.kofa.interfaces import academic_sessions_vocab
218from waeup.kofa.reports import get_generators
219from waeup.kofa.browser.breadcrumbs import Breadcrumb
220grok.templatedir('browser_templates')
221class StudentStatisticsReportGeneratorPage(KofaPage):
222
223    grok.context(StudentStatisticsReportGenerator)
224    grok.name('index.html')
225    grok.require('waeup.handleReports')
226
227    label = _('Create student statistics report')
228
229    @property
230    def generator_name(self):
231        for name, gen in get_generators():
232            if gen == self.context:
233                return name
234        return None
235
236    def update(
237        self, CREATE=None, session=None, mode=None, level=None, breakdown=None):
238        self.parent_url = self.url(self.context.__parent__)
239        self._set_session_values()
240        self._set_mode_values()
241        self._set_level_values()
242        self._set_breakdown_values()
243        if CREATE and session:
244            # create a new report job for students by session and level
245            container = self.context.__parent__
246            user_id = self.request.principal.id
247            kw = dict(
248                session=int(session),
249                mode=mode,
250                level=int(level),
251                breakdown=breakdown)
252            self.flash(_('New report is being created in background'))
253            job_id = container.start_report_job(
254                self.generator_name, user_id, kw=kw)
255            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
256            grok.getSite().logger.info(
257                '%s - report %s created: %s '
258                '(session=%s, mode=%s, level=%s, breakdown=%s)' % (
259                ob_class, job_id, self.context.title,
260                session, mode, level, breakdown))
261            self.redirect(self.parent_url)
262            return
263        return
264
265    def _set_session_values(self):
266        vocab_terms = academic_sessions_vocab.by_value.values()
267        self.sessions = [(x.title, x.token) for x in vocab_terms]
268        return
269
270    def _set_mode_values(self):
271        mode_groups = getUtility(IKofaUtils).MODE_GROUPS
272        self.modes = sorted([(key, key) for key in mode_groups.keys()])
273        return
274
275    def _set_level_values(self):
276        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
277        studylevelsource = StudyLevelSource().factory
278        self.levels = [(u'All', 0)]
279        for code in studylevelsource.getValues(None):
280            title = translate(studylevelsource.getTitle(None, code),
281                'waeup.kofa', target_language=portal_language)
282            self.levels.append((title, code))
283        return
284
285    def _set_breakdown_values(self):
286        self.breakdowns = [('Faculties', 'faccode'), ('Departments', 'depcode')]
287        return
288
289class StudentStatisticsReportPDFView(grok.View):
290
291    grok.context(IStudentStatisticsReport)
292    grok.name('pdf')
293    grok.require('waeup.handleReports')
294    prefix = ''
295
296    def _filename(self):
297        return 'StudentStatisticsReport_rno%s_%s.pdf' % (
298            self.context.__name__,
299            self.context.creation_dt_string)
300
301    def render(self):
302        filename = self._filename().replace(
303            '/', '_').replace(' ','_').replace(':', '-')
304        self.response.setHeader(
305            'Content-Type', 'application/pdf')
306        self.response.setHeader(
307            'Content-Disposition:', 'attachment; filename="%s' % filename)
308        pdf_stream = self.context.create_pdf(self.context.__name__)
309        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
310        grok.getSite().logger.info('%s - report %s downloaded: %s' % (
311            ob_class, self.context.__name__, filename))
312        return pdf_stream
313
314class StudentStatsBreadcrumb(Breadcrumb):
315    """A breadcrumb for reports.
316    """
317    grok.context(StudentStatisticsReportGenerator)
318    title = _(u'Student Statistics')
319    target = None
Note: See TracBrowser for help on using the repository browser.