source: main/waeup.kofa/trunk/src/waeup/kofa/students/export.py @ 13885

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

Fix mangle_value method of DataForLecturerExporter?.

  • Property svn:keywords set to Id
File size: 24.3 KB
Line 
1## $Id: export.py 13885 2016-06-07 09:25:02Z henrik $
2##
3## Copyright (C) 2011 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##
18"""Exporters for student related stuff.
19"""
20import os
21import grok
22from datetime import datetime, timedelta
23from zope.component import getUtility
24from waeup.kofa.interfaces import (
25    IExtFileStore, IFileStoreNameChooser, IKofaUtils)
26from waeup.kofa.interfaces import MessageFactory as _
27from waeup.kofa.students.catalog import StudentsQuery, CourseTicketsQuery
28from waeup.kofa.students.interfaces import (
29    IStudent, IStudentStudyCourse, IStudentStudyLevel, ICourseTicket,
30    IStudentOnlinePayment, ICSVStudentExporter, IBedTicket)
31from waeup.kofa.students.vocabularies import study_levels
32from waeup.kofa.utils.batching import ExporterBase
33from waeup.kofa.utils.helpers import iface_names, to_timezone
34
35
36def get_students(site, stud_filter=StudentsQuery()):
37    """Get all students registered in catalog in `site`.
38    """
39    return stud_filter.query()
40
41def get_studycourses(students):
42    """Get studycourses of `students`.
43    """
44    return [x.get('studycourse', None) for x in students
45            if x is not None]
46
47def get_levels(students):
48    """Get all studylevels of `students`.
49    """
50    levels = []
51    for course in get_studycourses(students):
52        for level in course.values():
53            levels.append(level)
54    return levels
55
56def get_tickets(students, **kw):
57    """Get course tickets of `students`.
58    If code is passed through, filter course tickets
59    which belong to this course code and meets level
60    and level_session.
61    """
62    tickets = []
63    code = kw.get('code', None)
64    level = kw.get('level', None)
65    level_session = kw.get('session', None)
66    if code is None:
67        for level_obj in get_levels(students):
68            for ticket in level_obj.values():
69                tickets.append(ticket)
70    else:
71        for level_obj in get_levels(students):
72            for ticket in level_obj.values():
73                if ticket.code != code:
74                    continue
75                if level is not None:
76                    level = int(level)
77                    if level_obj.level in (10, 999, None)  \
78                        and int(level) != level_obj.level:
79                        continue
80                    if level_obj.level not in range(level, level+100, 10):
81                        continue
82                if level_session is not None and \
83                    int(level_session) != level_obj.level_session:
84                    continue
85                tickets.append(ticket)
86    return tickets
87
88def get_payments(students, p_state=None, **kw):
89    """Get all payments of `students` within given payment_date period.
90    """
91    date_format = '%d/%m/%Y'
92    payments = []
93    payments_start = kw.get('payments_start')
94    payments_end = kw.get('payments_end')
95    if payments_start and payments_end:
96        # Payment period given
97        payments_start = datetime.strptime(payments_start, date_format)
98        payments_end = datetime.strptime(payments_end, date_format)
99        tz = getUtility(IKofaUtils).tzinfo
100        payments_start = tz.localize(payments_start)
101        payments_end = tz.localize(payments_end) + timedelta(days=1)
102        if p_state:
103            # Only paid or unpaid tickets in payment period are considered
104            for student in students:
105                for payment in student.get('payments', {}).values():
106                    if payment.payment_date and payment.p_state == p_state:
107                        payment_date = to_timezone(payment.payment_date, tz)
108                        if payment_date > payments_start and \
109                            payment_date < payments_end:
110                            payments.append(payment)
111        else:
112            # All tickets in payment period are considered
113            for student in students:
114                for payment in student.get('payments', {}).values():
115                    if payment.payment_date:
116                        payment_date = to_timezone(payment.payment_date, tz)
117                        if payment_date > payments_start and \
118                            payment_date < payments_end:
119                            payments.append(payment)
120    else:
121        # Payment period not given
122        if p_state:
123            # Only paid tickets are considered
124            for student in students:
125                for payment in student.get('payments', {}).values():
126                    if payment.p_state == p_state:
127                        payments.append(payment)
128        else:
129            # All tickets are considered
130            for student in students:
131                for payment in student.get('payments', {}).values():
132                    payments.append(payment)
133    return payments
134
135def get_bedtickets(students):
136    """Get all bedtickets of `students`.
137    """
138    tickets = []
139    for student in students:
140        for ticket in student.get('accommodation', {}).values():
141            tickets.append(ticket)
142    return tickets
143
144class StudentExporterBase(ExporterBase):
145    """Exporter for students or related objects.
146    This is a baseclass.
147    """
148    grok.baseclass()
149    grok.implements(ICSVStudentExporter)
150    grok.provides(ICSVStudentExporter)
151
152    def filter_func(self, x, **kw):
153        return x
154
155    def get_filtered(self, site, **kw):
156        """Get students from a catalog filtered by keywords.
157        students_catalog is the default catalog. The keys must be valid
158        catalog index names.
159        Returns a simple empty list, a list with `Student`
160        objects or a catalog result set with `Student`
161        objects.
162
163        .. seealso:: `waeup.kofa.students.catalog.StudentsCatalog`
164
165        """
166        # Pass only given keywords to create FilteredCatalogQuery objects.
167        # This way we avoid
168        # trouble with `None` value ambivalences and queries are also
169        # faster (normally less indexes to ask). Drawback is, that
170        # developers must look into catalog to see what keywords are
171        # valid.
172        if kw.get('catalog', None) == 'coursetickets':
173            coursetickets = CourseTicketsQuery(**kw).query()
174            students = []
175            for ticket in coursetickets:
176                students.append(ticket.student)
177            return list(set(students))
178        # Payments can be filtered by payment_date. The period boundaries
179        # are not keys of the catalog and must thus be removed from kw.
180        try:
181            del kw['payments_start']
182            del kw['payments_end']
183        except KeyError:
184            pass
185        query = StudentsQuery(**kw)
186        return query.query()
187
188    def get_selected(self, site, selected):
189        """Get set of selected students.
190        Returns a simple empty list or a list with `Student`
191        objects.
192        """
193        students = []
194        students_container = site.get('students', {})
195        for id in selected:
196            student = students_container.get(id, None)
197            if student:
198                students.append(student)
199        return students
200
201    def export(self, values, filepath=None):
202        """Export `values`, an iterable, as CSV file.
203        If `filepath` is ``None``, a raw string with CSV data is returned.
204        """
205        writer, outfile = self.get_csv_writer(filepath)
206        for value in values:
207            self.write_item(value, writer)
208        return self.close_outfile(filepath, outfile)
209
210    def export_all(self, site, filepath=None):
211        """Export students into filepath as CSV data.
212        If `filepath` is ``None``, a raw string with CSV data is returned.
213        """
214        return self.export(self.filter_func(get_students(site)), filepath)
215
216    def export_student(self, student, filepath=None):
217        return self.export(self.filter_func([student]), filepath=filepath)
218
219    def export_filtered(self, site, filepath=None, **kw):
220        """Export items denoted by `kw`.
221        If `filepath` is ``None``, a raw string with CSV data should
222        be returned.
223        """
224        data = self.get_filtered(site, **kw)
225        return self.export(self.filter_func(data, **kw), filepath=filepath)
226
227    def export_selected(self,site, filepath=None, **kw):
228        """Export data for selected set of students.
229        """
230        selected = kw.get('selected', [])
231        data = self.get_selected(site, selected)
232        return self.export(self.filter_func(data, **kw), filepath=filepath)
233
234
235class StudentExporter(grok.GlobalUtility, StudentExporterBase):
236    """The Student Exporter first filters the set of students by searching the
237    students catalog. Then it exports student base data of this set of students.
238    """
239    grok.name('students')
240
241    fields = tuple(sorted(iface_names(IStudent))) + (
242        'password', 'state', 'history', 'certcode', 'is_postgrad',
243        'current_level', 'current_session')
244    title = _(u'Students')
245
246    def mangle_value(self, value, name, context=None):
247        """The mangler prepares the history messages and adds a hash symbol at
248        the end of the phone number to avoid annoying automatic number
249        transformation by Excel or Calc."""
250        if name == 'history':
251            value = value.messages
252        if name == 'phone' and value is not None:
253            # Append hash '#' to phone numbers to circumvent
254            # unwanted excel automatic
255            value = str('%s#' % value)
256        return super(
257            StudentExporter, self).mangle_value(
258            value, name, context=context)
259
260
261class StudentStudyCourseExporter(grok.GlobalUtility, StudentExporterBase):
262    """The Student Study Course Exporter first filters the set of students
263    by searching the students catalog. Then it exports the data of the current
264    study course container of each student from this set. It does
265    not export their content.
266    """
267    grok.name('studentstudycourses')
268
269    fields = tuple(sorted(iface_names(IStudentStudyCourse))) + ('student_id',)
270    title = _(u'Student Study Courses')
271
272    def filter_func(self, x, **kw):
273        return get_studycourses(x)
274
275    def mangle_value(self, value, name, context=None):
276        """The mangler determines the certificate code and the student id.
277        """
278        if name == 'certificate' and value is not None:
279            # XXX: hopefully cert codes are unique site-wide
280            value = value.code
281        if name == 'student_id' and context is not None:
282            student = context.student
283            value = getattr(student, name, None)
284        return super(
285            StudentStudyCourseExporter, self).mangle_value(
286            value, name, context=context)
287
288
289class StudentStudyLevelExporter(grok.GlobalUtility, StudentExporterBase):
290    """The Student Study Level Exporter first filters the set of students
291    by searching the students catalog. Then it exports the data of the student's
292    study level container data but not their content (course tickets).
293    The exporter iterates over all objects in the students' ``studycourse``
294    containers.
295    """
296    grok.name('studentstudylevels')
297
298    fields = tuple(sorted(iface_names(
299        IStudentStudyLevel))) + (
300        'student_id', 'number_of_tickets','certcode')
301    title = _(u'Student Study Levels')
302
303    def filter_func(self, x, **kw):
304        return get_levels(x)
305
306    def mangle_value(self, value, name, context=None):
307        """The mangler determines the student id, nothing else.
308        """
309        if name == 'student_id' and context is not None:
310            student = context.student
311            value = getattr(student, name, None)
312        return super(
313            StudentStudyLevelExporter, self).mangle_value(
314            value, name, context=context)
315
316class CourseTicketExporter(grok.GlobalUtility, StudentExporterBase):
317    """The Course Ticket Exporter exports course tickets. Usually,
318    the exporter first filters the set of students by searching the
319    students catalog. Then it collects and iterates over all ``studylevel``
320    containers of the filtered student set and finally
321    iterates over all items inside these containers.
322
323    If the course code is passed through, the exporter uses a different
324    catalog. It searches for students in the course tickets catalog and
325    exports those course tickets which belong to the given course code and
326    also meet level and session passed through at the same time.
327    This happens if the exporter is called at course level in the academic
328    section.
329    """
330    grok.name('coursetickets')
331
332    fields = tuple(sorted(iface_names(ICourseTicket) +
333        ['level', 'code', 'level_session'])) + ('student_id',
334        'certcode', 'display_fullname')
335    title = _(u'Course Tickets')
336
337    def filter_func(self, x, **kw):
338        return get_tickets(x, **kw)
339
340    def mangle_value(self, value, name, context=None):
341        """The mangler determines the student's id and fullname.
342        """
343        if context is not None:
344            student = context.student
345            if name in ('student_id', 'display_fullname') and student is not None:
346                value = getattr(student, name, None)
347        return super(
348            CourseTicketExporter, self).mangle_value(
349            value, name, context=context)
350
351class DataForLecturerExporter(CourseTicketExporter):
352    """
353    """
354    grok.name('lecturer')
355
356    fields = ('matric_number', 'reg_number', 'student_id', 'display_fullname',
357              'level', 'code', 'level_session', 'score')
358
359    title = _(u'Data for Lecturer')
360
361    def mangle_value(self, value, name, context=None):
362        """The mangler determines the student's id and fullname.
363        """
364        if context is not None:
365            student = context.student
366            if name in ('matric_number',
367                        'reg_number',
368                        'student_id',
369                        'display_fullname',) and student is not None:
370                value = getattr(student, name, None)
371        return super(
372            CourseTicketExporter, self).mangle_value(
373            value, name, context=context)
374
375class StudentPaymentExporter(grok.GlobalUtility, StudentExporterBase):
376    """The Student Payment Exporter first filters the set of students
377    by searching the students catalog. Then it exports student payment
378    tickets by iterating over the items of the student's ``payments``
379    container. If the payment period is given only tickets, which were
380    paid in payment period, are considered for export.
381    """
382    grok.name('studentpayments')
383
384    fields = tuple(
385        sorted(iface_names(
386            IStudentOnlinePayment, exclude_attribs=False,
387            omit=['display_item', 'certificate', 'student']))) + (
388            'student_id','state','current_session')
389    title = _(u'Student Payments')
390
391    def filter_func(self, x, **kw):
392        return get_payments(x, **kw)
393
394    def mangle_value(self, value, name, context=None):
395        """The mangler determines the student's id, registration
396        state and current session.
397        """
398        if context is not None:
399            student = context.student
400            if name in ['student_id','state',
401                        'current_session'] and student is not None:
402                value = getattr(student, name, None)
403        return super(
404            StudentPaymentExporter, self).mangle_value(
405            value, name, context=context)
406
407class StudentUnpaidPaymentExporter(StudentPaymentExporter):
408    """The Student Unpaid Payment Exporter works just like the
409    Student Payments Exporter but it exports only unpaid tickets.
410    This exporter is designed for finding and finally purging outdated
411    payment ticket.
412    """
413    grok.name('studentunpaidpayments')
414
415    title = _(u'Student Unpaid Payments')
416
417    def filter_func(self, x, **kw):
418        return get_payments(x, p_state='unpaid', **kw)
419
420class DataForBursaryExporter(StudentPaymentExporter):
421    """The DataForBursary Exporter works just like the Student Payments Exporter
422    but it exports much more information about the student. It combines
423    payment and student data in one table in order to spare postprocessing of
424    two seperate export files. The exporter is primarily used by bursary
425    officers who have exclusively access to this exporter.
426    """
427    grok.name('bursary')
428
429    def filter_func(self, x, **kw):
430        return get_payments(x, p_state='paid', **kw)
431
432    fields = tuple(
433        sorted(iface_names(
434            IStudentOnlinePayment, exclude_attribs=False,
435            omit=['display_item', 'certificate', 'student']))) + (
436            'student_id','matric_number','reg_number',
437            'firstname', 'middlename', 'lastname',
438            'state','current_session',
439            'entry_session', 'entry_mode',
440            'faccode', 'depcode','certcode')
441    title = _(u'Payment Data for Bursary')
442
443    def mangle_value(self, value, name, context=None):
444        """The mangler fetches the student data.
445        """
446        if context is not None:
447            student = context.student
448            if name in [
449                'student_id','matric_number', 'reg_number',
450                'firstname', 'middlename', 'lastname',
451                'state', 'current_session',
452                'entry_session', 'entry_mode',
453                'faccode', 'depcode', 'certcode'] and student is not None:
454                value = getattr(student, name, None)
455        return super(
456            StudentPaymentExporter, self).mangle_value(
457            value, name, context=context)
458
459class BedTicketExporter(grok.GlobalUtility, StudentExporterBase):
460    """The Bed Ticket Exporter first filters the set of students
461    by searching the students catalog. Then it exports bed
462    tickets by iterating over the items of the student's ``accommodation``
463    container.
464    """
465    grok.name('bedtickets')
466
467    fields = tuple(
468        sorted(iface_names(
469            IBedTicket, exclude_attribs=False,
470            omit=['display_coordinates', 'maint_payment_made']))) + (
471            'student_id', 'actual_bed_type')
472    title = _(u'Bed Tickets')
473
474    def filter_func(self, x, **kw):
475        return get_bedtickets(x)
476
477    def mangle_value(self, value, name, context=None):
478        """The mangler determines the student id and the type of the bed
479        which has been booked in the ticket.
480        """
481        if context is not None:
482            student = context.student
483            if name in ['student_id'] and student is not None:
484                value = getattr(student, name, None)
485        if name == 'bed' and value is not None:
486            value = getattr(value, 'bed_id', None)
487        if name == 'actual_bed_type':
488            value = getattr(getattr(context, 'bed', None), 'bed_type')
489        return super(
490            BedTicketExporter, self).mangle_value(
491            value, name, context=context)
492
493class StudentPaymentsOverviewExporter(StudentExporter):
494    """The Student Payments Overview Exporter first filters the set of students
495    by searching the students catalog. Then it exports some student base data
496    together with the total school fee amount paid in each year over a
497    predefined year range (current year - 9, ... , current year + 1).
498    """
499    grok.name('paymentsoverview')
500
501    curr_year = datetime.now().year
502    year_range = range(curr_year - 10, curr_year + 1)
503    year_range_tuple = tuple([str(year) for year in year_range])
504
505    fields = ('student_id', 'matric_number', 'display_fullname',
506        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
507        'current_level', 'current_session', 'current_mode',
508        'entry_session', 'reg_number'
509        ) + year_range_tuple
510    title = _(u'Student Payments Overview')
511
512    def mangle_value(self, value, name, context=None):
513        """The mangler summarizes the school fee amounts made per year. It
514        iterates over all paid school fee payment tickets and
515        adds together the amounts paid in a year. Waived payments
516        are marked ``waived``.
517        """
518        if name in self.year_range_tuple and context is not None:
519            value = 0
520            for ticket in context['payments'].values():
521                if ticket.p_category == 'schoolfee' and \
522                    ticket.p_session == int(name):
523                    if ticket.p_state == 'waived':
524                        value = 'waived'
525                        break
526                    if ticket.p_state == 'paid':
527                        try:
528                            value += ticket.amount_auth
529                        except TypeError:
530                            pass
531            if value == 0:
532                value = ''
533        return super(
534            StudentExporter, self).mangle_value(
535            value, name, context=context)
536
537class StudentStudyLevelsOverviewExporter(StudentExporter):
538    """The Student Study Levels Overview Exporter first filters the set of
539    students by searching the students catalog. Then it exports some student
540    base data together with the session key of registered levels.
541    Sample output:
542
543    header: ``...100,110,120,200,210,220,300...``
544
545    data: ``...2010,,,2011,2012,,2013...``
546
547    This csv data string means that level 100 was registered in session
548    2010/2011, level 200 in session 2011/2012, level 210 (200 on 1st probation)
549    in session 2012/2013 and level 300 in session 2013/2014.
550    """
551    grok.name('studylevelsoverview')
552
553    avail_levels = tuple([str(x) for x in study_levels(None)])
554
555    fields = ('student_id', ) + (
556        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
557        'entry_session', 'current_level', 'current_session',
558        ) + avail_levels
559    title = _(u'Student Study Levels Overview')
560
561    def mangle_value(self, value, name, context=None):
562        """The mangler checks if a given level has been registered. It returns
563        the ``level_session`` attribute of the student study level object
564        if the named level exists.
565        """
566        if name in self.avail_levels and context is not None:
567            value = ''
568            for level in context['studycourse'].values():
569                if level.level == int(name):
570                    value = '%s' % level.level_session
571                    break
572        return super(
573            StudentExporter, self).mangle_value(
574            value, name, context=context)
575
576class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
577    """Like all other exporters the Combo Card Data Exporter first filters the
578    set of students by searching the students catalog. Then it exports some
579    student base data which are neccessary to print for the Interswitch combo
580    card (identity card for students). The output contains a ``passport_path``
581    column which contains the filesystem path of the passport image file.
582    If no path is given, no passport image file exists.
583    """
584    grok.name('combocard')
585
586    fields = ('display_fullname',
587              'student_id','matric_number',
588              'certificate', 'faculty', 'department', 'passport_path')
589    title = _(u'Combo Card Data')
590
591    def mangle_value(self, value, name, context=None):
592        """The mangler determines the titles of faculty, department
593        and certificate. It also computes the path of passport image file
594        stored in the filesystem.
595        """
596        certificate = context['studycourse'].certificate
597        if name == 'certificate' and certificate is not None:
598            value = certificate.title
599        if name == 'department' and certificate is not None:
600            value = certificate.__parent__.__parent__.longtitle
601        if name == 'faculty' and certificate is not None:
602            value = certificate.__parent__.__parent__.__parent__.longtitle
603        if name == 'passport_path' and certificate is not None:
604            file_id = IFileStoreNameChooser(context).chooseName(
605                attr='passport.jpg')
606            os_path = getUtility(IExtFileStore)._pathFromFileID(file_id)
607            if not os.path.exists(os_path):
608                value = None
609            else:
610                value = '/'.join(os_path.split('/')[-4:])
611        return super(
612            ComboCardDataExporter, self).mangle_value(
613            value, name, context=context)
Note: See TracBrowser for help on using the repository browser.