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

Last change on this file since 12943 was 12913, checked in by Henrik Bettermann, 10 years ago

Fix permissions. Reports must not be public.

  • Property svn:keywords set to Id
File size: 12.0 KB
Line 
1## $Id: student_statistics.py 12913 2015-05-08 07:05: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    ((<CODES>), (<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        codes = tuple(sorted([x for x in site['faculties'].keys()],
61                                 key=lambda x: x.lower()))
62        paths = codes
63    elif breakdown == 'depcode':
64        faculties = site['faculties']
65        depcodes = []
66        deppaths = []
67        for fac in faculties.values():
68            for dep in fac.values():
69                depcodes.append(dep.code)
70                deppaths.append('%s/%s' % (fac.code, dep.code))
71        codes = tuple(sorted([x for x in depcodes],
72                                 key=lambda x: x.lower()))
73        paths = tuple(sorted([x for x in deppaths],
74                                 key=lambda x: x.lower()))
75    # XXX: Here we do _one_ query and then examine the single
76    #   students. One could also do multiple queries and just look for
77    #   the result length (not introspecting the delivered students
78    #   further).
79    cat = queryUtility(ICatalog, name="students_catalog")
80    result = cat.searchResults(current_session=(session, session))
81    table = [[0 for x in xrange(len(states))] for y in xrange(len(codes)+1)]
82    mode_groups = getUtility(IKofaUtils).MODE_GROUPS
83    for stud in result:
84        if mode != 'All' and stud.current_mode not in mode_groups[mode]:
85            continue
86        if level != 0 and stud.current_level != level:
87            continue
88        if getattr(stud, breakdown) not in codes:
89            # studs can have a faccode ``None``
90            continue
91        row = codes.index(getattr(stud, breakdown))
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
146    def __init__(self, session, mode, level, breakdown, author='System'):
147        super(StudentStatisticsReport, self).__init__(
148            args=[session, mode, breakdown], kwargs={'author':author})
149        self.studylevelsource = StudyLevelSource().factory
150        self.portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
151        self.session = academic_sessions_vocab.getTerm(session).title
152        self.mode = mode
153        if level == 0:
154            self.level = ''
155        else:
156            self.level = translate(self.studylevelsource.getTitle(None, int(level)),
157                'waeup.kofa', target_language=self.portal_language)
158        self.breakdown = breakdown
159        self.author = author
160        self.creation_dt_string = self.creation_dt.astimezone(
161            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
162        self.data = get_student_stats(session, mode, level, breakdown)
163
164    def create_pdf(self):
165        creator = getUtility(IPDFCreator, name='landscape')
166        table_data = tbl_data_to_table(*self.data)
167        col_widths = [None,] + [1.6*cm] * len(self.data[1]) + [None,]
168        pdf_data = [Paragraph('<b>%s</b>' % self.creation_dt_string,
169                              STYLE["Normal"]),
170                    Spacer(1, 12),]
171        pdf_data += [
172            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
173        if not self.level:
174            doc_title = translate(
175                _('${a} Students - ${b}',
176                mapping = {'a':self.mode, 'b':self.session}))
177            pdf = creator.create_pdf(
178                pdf_data, None, doc_title, self.author, doc_title + '-'
179                )
180        else:
181            doc_title = translate(
182                _('${a} Students - ${b} - ${c}',
183                mapping = {'a':self.mode, 'b': self.level, 'c':self.session}))
184            pdf = creator.create_pdf(
185                pdf_data, None, doc_title, self.author, doc_title + '-'
186                )
187        return pdf
188
189@implementer(IReportGenerator)
190class StudentStatisticsReportGenerator(grok.GlobalUtility):
191
192    title = _('Student Statistics')
193    grok.name('student_stats')
194
195    def generate(
196        self, site, session=None, mode=None,
197        level=None, breakdown=None, author=None):
198        result = StudentStatisticsReport(
199            session=session, mode=mode, level=level,
200            breakdown=breakdown, author=author)
201        return result
202
203###############################################################
204## Browser related stuff
205##
206## XXX: move to local browser module
207###############################################################
208from waeup.kofa.browser.layout import KofaPage
209from waeup.kofa.interfaces import academic_sessions_vocab
210from waeup.kofa.reports import get_generators
211from waeup.kofa.browser.breadcrumbs import Breadcrumb
212grok.templatedir('browser_templates')
213class StudentStatisticsReportGeneratorPage(KofaPage):
214
215    grok.context(StudentStatisticsReportGenerator)
216    grok.name('index.html')
217    grok.require('waeup.handleReports')
218
219    label = _('Create student statistics report')
220
221    @property
222    def generator_name(self):
223        for name, gen in get_generators():
224            if gen == self.context:
225                return name
226        return None
227
228    def update(
229        self, CREATE=None, session=None, mode=None, level=None, breakdown=None):
230        self.parent_url = self.url(self.context.__parent__)
231        self._set_session_values()
232        self._set_mode_values()
233        self._set_level_values()
234        self._set_breakdown_values()
235        if CREATE and session:
236            # create a new report job for students by session and level
237            container = self.context.__parent__
238            user_id = self.request.principal.id
239            kw = dict(
240                session=int(session),
241                mode=mode,
242                level=int(level),
243                breakdown=breakdown)
244            self.flash(_('New report is being created in background'))
245            job_id = container.start_report_job(
246                self.generator_name, user_id, kw=kw)
247            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
248            grok.getSite().logger.info(
249                '%s - report %s created: %s (session=%s, mode=%s, level=%s, breakdown=%s)' % (
250                ob_class, job_id, self.context.title,
251                session, mode, level, breakdown))
252            self.redirect(self.parent_url)
253            return
254        return
255
256    def _set_session_values(self):
257        vocab_terms = academic_sessions_vocab.by_value.values()
258        self.sessions = [(x.title, x.token) for x in vocab_terms]
259        return
260
261    def _set_mode_values(self):
262        mode_groups = getUtility(IKofaUtils).MODE_GROUPS
263        self.modes = sorted([(key, key) for key in mode_groups.keys()])
264        return
265
266    def _set_level_values(self):
267        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
268        studylevelsource = StudyLevelSource().factory
269        self.levels = [(u'All levels', 0)]
270        for code in studylevelsource.getValues(None):
271            title = translate(studylevelsource.getTitle(None, code),
272                'waeup.kofa', target_language=portal_language)
273            self.levels.append((title, code))
274        return
275
276    def _set_breakdown_values(self):
277        self.breakdowns = [('Faculties', 'faccode'), ('Departments', 'depcode')]
278        return
279
280class StudentStatisticsReportPDFView(grok.View):
281
282    grok.context(IStudentStatisticsReport)
283    grok.name('pdf')
284    grok.require('waeup.handleReports')
285    prefix = ''
286
287    def _filename(self):
288        return 'StudentStatisticsReport_%s_%s_%s_%s.pdf' % (
289            self.context.session, self.context.mode, self.context.level,
290            self.context.creation_dt_string)
291
292    def render(self):
293        filename = self._filename().replace(
294            '/', '_').replace(' ','_').replace(':', '-')
295        self.response.setHeader(
296            'Content-Type', 'application/pdf')
297        self.response.setHeader(
298            'Content-Disposition:', 'attachment; filename="%s' % filename)
299        pdf_stream = self.context.create_pdf()
300        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
301        grok.getSite().logger.info('%s - report %s downloaded: %s' % (
302            ob_class, self.context.__name__, filename))
303        return pdf_stream
304
305class StudentStatsBreadcrumb(Breadcrumb):
306    """A breadcrumb for reports.
307    """
308    grok.context(StudentStatisticsReportGenerator)
309    title = _(u'Student Statistics')
310    target = None
Note: See TracBrowser for help on using the repository browser.