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

Last change on this file since 15918 was 15918, checked in by Henrik Bettermann, 5 years ago

Add session_levelfilter to StudentStudyLevelExporter.

Adjust tests.

  • Property svn:keywords set to Id
File size: 33.1 KB
Line 
1## $Id: export.py 15918 2020-01-13 13:06:54Z 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.university.interfaces import ICertificateCourse, ICourse
28from waeup.kofa.students.catalog import StudentsQuery, CourseTicketsQuery
29from waeup.kofa.students.interfaces import (
30    IStudent, IStudentStudyCourse, IStudentStudyLevel, ICourseTicket,
31    IStudentOnlinePayment, ICSVStudentExporter, IBedTicket)
32from waeup.kofa.students.vocabularies import study_levels
33from waeup.kofa.utils.batching import ExporterBase
34from waeup.kofa.utils.helpers import iface_names, to_timezone
35
36
37def get_students(site, stud_filter=StudentsQuery()):
38    """Get all students registered in catalog in `site`.
39    """
40    return stud_filter.query()
41
42def get_studycourses(students):
43    """Get studycourses of `students`.
44    """
45    return [x.get('studycourse', None) for x in students
46            if x is not None]
47
48def get_levels(students, **kw):
49    """Get all studylevels of `students`.
50    """
51    levels = []
52    level_session = kw.get('level_session', None)
53    for course in get_studycourses(students):
54        for level in course.values():
55            if level_session not in ('all', None) and \
56                int(level_session) != level.level_session:
57                continue
58            levels.append(level)
59    return levels
60
61def get_tickets(students, **kw):
62    """Get course tickets of `students`.
63    If code is passed through, filter course tickets
64    which belong to this course code and meet level=level
65    and level_session=level_session.
66    If not, but ct_level, ct_session and ct_semester
67    are passed through, filter course tickets
68    which meet level==ct_level, level_session==ct_session
69    and semester==ct_semester.
70    """
71    tickets = []
72    code = kw.get('code', None)
73    level = kw.get('level', None)
74    session = kw.get('session', None)
75    ct_level = kw.get('ct_level', None)
76    ct_session = kw.get('ct_session', None)
77    ct_semester = kw.get('ct_semester', None)
78    if code is None:
79        for level_obj in get_levels(students, **kw):
80            for ticket in level_obj.values():
81                if ct_level not in ('all', None):
82                    if level_obj.level in (10, 999, None)  \
83                        and int(ct_level) != level_obj.level:
84                        continue
85                    if level_obj.level not in range(
86                        int(ct_level), int(ct_level)+100, 10):
87                        continue
88                if ct_session not in ('all', None) and \
89                    int(ct_session) != level_obj.level_session:
90                    continue
91                if ct_semester not in ('all', None) and \
92                    int(ct_semester) != ticket.semester:
93                    continue
94                tickets.append(ticket)
95    else:
96        for level_obj in get_levels(students, **kw):
97            for ticket in level_obj.values():
98                if ticket.code != code:
99                    continue
100                if level not in ('all', None):
101                    if level_obj.level in (10, 999, None)  \
102                        and int(level) != level_obj.level:
103                        continue
104                    if level_obj.level not in range(
105                        int(level), int(level)+100, 10):
106                        continue
107                if session not in ('all', None) and \
108                    int(session) != level_obj.level_session:
109                    continue
110                tickets.append(ticket)
111    return tickets
112
113def get_tickets_for_lecturer(students, **kw):
114    """Get course tickets of `students`.
115    Filter course tickets which belong to this course code and
116    which are editable by lecturers.
117    """
118    tickets = []
119    code = kw.get('code', None)
120    level_session = kw.get('session', None)
121    level = kw.get('level', None)
122    for level_obj in get_levels(students, **kw):
123        for ticket in level_obj.values():
124            if ticket.code != code:
125                continue
126            if not ticket.editable_by_lecturer:
127                continue
128            if level not in ('all', None):
129                if level_obj.level in (10, 999, None)  \
130                    and int(level) != level_obj.level:
131                    continue
132                if level_obj.level not in range(int(level), int(level)+100, 10):
133                    continue
134            if level_session not in ('all', None) and \
135                int(level_session) != level_obj.level_session:
136                continue
137            tickets.append(ticket)
138    return tickets
139
140def get_outstanding(students, **kw):
141    """Get students with outstanding certificate courses.
142    """
143    students_wo = []
144    for student in students:
145        certificate = getattr(
146            student.get('studycourse', None), 'certificate', None)
147        if certificate:
148            allticketcodes = []
149            failedticketcodes = '' # taken but failed
150            nottakenticketcodes = '' # registered but not taken
151            missedticketcodes = '' # not registered
152            for level in student['studycourse'].values():
153                failedticketcodes += level.passed_params[4]
154                nottakenticketcodes += level.passed_params[5]
155                for ticket in level.values():
156                    allticketcodes.append(ticket.code)
157            for certcourse in certificate.values():
158                if certcourse.getCourseCode() not in allticketcodes:
159                    missedticketcodes += '%s ' % certcourse.__name__
160            student_wo = (student, missedticketcodes,
161                          failedticketcodes, nottakenticketcodes)
162            students_wo.append(student_wo)
163    return students_wo
164
165def get_payments(students, p_states=None, **kw):
166    """Get all payments of `students` within given payment_date period.
167    """
168    date_format = '%d/%m/%Y'
169    payments = []
170    p_start = kw.get('payments_start')
171    p_end = kw.get('payments_end')
172    paycat = kw.get('paycat')
173    paysession = kw.get('paysession')
174    for student in students:
175        for payment in student.get('payments', {}).values():
176            if p_start and p_end:
177                if not payment.payment_date:
178                    continue
179                payments_start = datetime.strptime(p_start, date_format)
180                payments_end = datetime.strptime(p_end, date_format)
181                tz = getUtility(IKofaUtils).tzinfo
182                payments_start = tz.localize(payments_start)
183                payments_end = tz.localize(payments_end) + timedelta(days=1)
184                payment_date = to_timezone(payment.payment_date, tz)
185                if payment_date < payments_start or payment_date > payments_end:
186                    continue
187            if p_states and not payment.p_state in p_states:
188                continue
189            if paycat not in ('all', None) and payment.p_category != paycat:
190                continue
191            if paysession not in ('all', None) \
192                and payment.p_session != int(paysession):
193                continue
194            payments.append(payment)
195    return payments
196
197def get_bedtickets(students):
198    """Get all bedtickets of `students`.
199    """
200    tickets = []
201    for student in students:
202        for ticket in student.get('accommodation', {}).values():
203            tickets.append(ticket)
204    return tickets
205
206class StudentExporterBase(ExporterBase):
207    """Exporter for students or related objects.
208    This is a baseclass.
209    """
210    grok.baseclass()
211    grok.implements(ICSVStudentExporter)
212    grok.provides(ICSVStudentExporter)
213
214    def filter_func(self, x, **kw):
215        return x
216
217    def get_filtered(self, site, **kw):
218        """Get students from a catalog filtered by keywords.
219        students_catalog is the default catalog. The keys must be valid
220        catalog index names.
221        Returns a simple empty list, a list with `Student`
222        objects or a catalog result set with `Student`
223        objects.
224
225        .. seealso:: `waeup.kofa.students.catalog.StudentsCatalog`
226
227        """
228        # Pass only given keywords to create FilteredCatalogQuery objects.
229        # This way we avoid
230        # trouble with `None` value ambivalences and queries are also
231        # faster (normally less indexes to ask). Drawback is, that
232        # developers must look into catalog to see what keywords are
233        # valid.
234        if kw.get('catalog', None) == 'coursetickets':
235            coursetickets = CourseTicketsQuery(**kw).query()
236            students = []
237            for ticket in coursetickets:
238                students.append(ticket.student)
239            return list(set(students))
240        # Payments can be filtered by payment date and payment category.
241        # These parameters are not keys of the catalog and must thus be
242        # removed from kw.
243        try:
244            del kw['payments_start']
245            del kw['payments_end']
246            del kw['paycat']
247            del kw['paysession']
248        except KeyError:
249            pass
250        # Coursetickets can be filtered by level and session.
251        # These parameters are not keys of the catalog and must thus be
252        # removed from kw.
253        try:
254            del kw['ct_level']
255            del kw['ct_session']
256            del kw['ct_semester']
257        except KeyError:
258            pass
259        # Studylevels can be filtered by level_session.
260        # This parameter is not a key of the catalog and must thus be
261        # removed from kw.
262        try:
263            del kw['level_session']
264        except KeyError:
265            pass
266        query = StudentsQuery(**kw)
267        return query.query()
268
269    def get_selected(self, site, selected):
270        """Get set of selected students.
271        Returns a simple empty list or a list with `Student`
272        objects.
273        """
274        students = []
275        students_container = site.get('students', {})
276        for id in selected:
277            student = students_container.get(id, None)
278            if student is None:
279                # try matric number
280                result = list(StudentsQuery(matric_number=id).query())
281                if result:
282                    student = result[0]
283                else:
284                    continue
285            students.append(student)
286        return students
287
288    def export(self, values, filepath=None):
289        """Export `values`, an iterable, as CSV file.
290        If `filepath` is ``None``, a raw string with CSV data is returned.
291        """
292        writer, outfile = self.get_csv_writer(filepath)
293        for value in values:
294            self.write_item(value, writer)
295        return self.close_outfile(filepath, outfile)
296
297    def export_all(self, site, filepath=None):
298        """Export students into filepath as CSV data.
299        If `filepath` is ``None``, a raw string with CSV data is returned.
300        """
301        return self.export(self.filter_func(get_students(site)), filepath)
302
303    def export_student(self, student, filepath=None):
304        return self.export(self.filter_func([student]), filepath=filepath)
305
306    def export_filtered(self, site, filepath=None, **kw):
307        """Export items denoted by `kw`.
308        If `filepath` is ``None``, a raw string with CSV data should
309        be returned.
310        """
311        data = self.get_filtered(site, **kw)
312        return self.export(self.filter_func(data, **kw), filepath=filepath)
313
314    def export_selected(self,site, filepath=None, **kw):
315        """Export data for selected set of students.
316        """
317        selected = kw.get('selected', [])
318        data = self.get_selected(site, selected)
319        return self.export(self.filter_func(data, **kw), filepath=filepath)
320
321
322class StudentExporter(grok.GlobalUtility, StudentExporterBase):
323    """The Student Exporter first filters the set of students by searching the
324    students catalog. Then it exports student base data of this set of students.
325    """
326    grok.name('students')
327
328    fields = tuple(sorted(iface_names(IStudent))) + (
329        'password', 'state', 'history', 'certcode', 'is_postgrad',
330        'current_level', 'current_session')
331    title = _(u'Students')
332
333    def mangle_value(self, value, name, context=None):
334        """The mangler prepares the history messages and adds a hash symbol at
335        the end of the phone number to avoid annoying automatic number
336        transformation by Excel or Calc."""
337        if name == 'history':
338            value = value.messages
339        if 'phone' in name and value is not None:
340            # Append hash '#' to phone numbers to circumvent
341            # unwanted excel automatic
342            value = str('%s#' % value)
343        return super(
344            StudentExporter, self).mangle_value(
345            value, name, context=context)
346
347
348class StudentStudyCourseExporter(grok.GlobalUtility, StudentExporterBase):
349    """The Student Study Course Exporter first filters the set of students
350    by searching the students catalog. Then it exports the data of the current
351    study course container of each student from this set. It does
352    not export their content.
353    """
354    grok.name('studentstudycourses')
355
356    fields = tuple(sorted(iface_names(IStudentStudyCourse))) + ('student_id',)
357    title = _(u'Student Study Courses')
358
359    def filter_func(self, x, **kw):
360        return get_studycourses(x)
361
362    def mangle_value(self, value, name, context=None):
363        """The mangler determines the certificate code and the student id.
364        """
365        if name == 'certificate' and value is not None:
366            # XXX: hopefully cert codes are unique site-wide
367            value = value.code
368        if name == 'student_id' and context is not None:
369            student = context.student
370            value = getattr(student, name, None)
371        return super(
372            StudentStudyCourseExporter, self).mangle_value(
373            value, name, context=context)
374
375
376class StudentStudyLevelExporter(grok.GlobalUtility, StudentExporterBase):
377    """The Student Study Level Exporter first filters the set of students
378    by searching the students catalog. Then it exports the data of the student's
379    study level container data but not their content (course tickets).
380    The exporter iterates over all objects in the students' ``studycourse``
381    containers.
382    """
383    grok.name('studentstudylevels')
384
385    fields = tuple(sorted(iface_names(
386        IStudentStudyLevel))) + (
387        'student_id', 'number_of_tickets','certcode')
388    title = _(u'Student Study Levels')
389
390    def filter_func(self, x, **kw):
391        return get_levels(x, **kw)
392
393    def mangle_value(self, value, name, context=None):
394        """The mangler determines the student id, nothing else.
395        """
396        if name == 'student_id' and context is not None:
397            student = context.student
398            value = getattr(student, name, None)
399        return super(
400            StudentStudyLevelExporter, self).mangle_value(
401            value, name, context=context)
402
403class CourseTicketExporter(grok.GlobalUtility, StudentExporterBase):
404    """The Course Ticket Exporter exports course tickets. Usually,
405    the exporter first filters the set of students by searching the
406    students catalog. Then it collects and iterates over all ``studylevel``
407    containers of the filtered student set and finally
408    iterates over all items inside these containers.
409
410    If the course code is passed through, the exporter uses a different
411    catalog. It searches for students in the course tickets catalog and
412    exports those course tickets which belong to the given course code and
413    also meet level and session passed through at the same time.
414    This happens if the exporter is called at course level in the academic
415    section.
416    """
417    grok.name('coursetickets')
418
419    fields = tuple(sorted(iface_names(ICourseTicket) +
420        ['level', 'code', 'level_session'])) + ('student_id',
421        'certcode', 'display_fullname')
422    title = _(u'Course Tickets')
423
424    def filter_func(self, x, **kw):
425        return get_tickets(x, **kw)
426
427    def mangle_value(self, value, name, context=None):
428        """The mangler determines the student's id and fullname.
429        """
430        if context is not None:
431            student = context.student
432            if name in ('student_id', 'display_fullname') and student is not None:
433                value = getattr(student, name, None)
434        return super(
435            CourseTicketExporter, self).mangle_value(
436            value, name, context=context)
437
438class DataForLecturerExporter(grok.GlobalUtility, StudentExporterBase):
439    """The Data for Lecturer Exporter searches for students in the course
440    tickets catalog and exports those course tickets which belong to the
441    given course code, meet level and session passed through at the
442    same time, and which are editable by lecturers. This exporter can only
443    be called at course level in the academic section.
444    """
445    grok.name('lecturer')
446
447    fields = ('matric_number', 'student_id', 'display_fullname',
448              'level', 'code', 'level_session', 'score')
449
450    title = _(u'Data for Lecturer')
451
452    def filter_func(self, x, **kw):
453        return get_tickets_for_lecturer(x, **kw)
454
455    def mangle_value(self, value, name, context=None):
456        """The mangler determines the student's id and fullname.
457        """
458        if context is not None:
459            student = context.student
460            if name in ('matric_number',
461                        'reg_number',
462                        'student_id',
463                        'display_fullname',) and student is not None:
464                value = getattr(student, name, None)
465        return super(
466            DataForLecturerExporter, self).mangle_value(
467            value, name, context=context)
468
469class StudentOutstandingCoursesExporter(grok.GlobalUtility, StudentExporterBase):
470    """The Student Outstanding Courses Exporter first filters the set of
471    students by searching the students catalog. Then it exports students with
472    lists of outstanding courses, i.e. courses which the student has
473    missed (not registered at all), failed (registered but not passed)
474    or nottaken (registered but not taken).
475    """
476    grok.name('studentoutstandingcourses')
477
478    fields = ('student_id', 'certcode', 'display_fullname','missed',
479              'failed', 'nottaken')
480    title = _(u'Student Outstanding Courses')
481
482    def filter_func(self, x, **kw):
483        return get_outstanding(x, **kw)
484
485    def mangle_value(self, value, name, context=None):
486        """The mangler determines the student's id, fullname and certcode,
487        and it collects the lists of outstanding courses.
488        """
489        if context is not None:
490            if name in ('student_id', 'display_fullname', 'certcode'):
491                value = getattr(context[0], name, None)
492            elif name == 'missed':
493                value = context[1]
494            elif name == 'failed':
495                value = context[2]
496            elif name == 'nottaken':
497                value = context[3]
498        return super(
499            StudentOutstandingCoursesExporter, self).mangle_value(
500            value, name, context=context)
501
502class StudentPaymentExporter(grok.GlobalUtility, StudentExporterBase):
503    """The Student Payment Exporter first filters the set of students
504    by searching the students catalog. Then it exports student payment
505    tickets by iterating over the items of the student's ``payments``
506    container. If the payment period is given, only tickets, which were
507    paid in payment period, are considered for export.
508    """
509    grok.name('studentpayments')
510
511    fields = tuple(
512        sorted(iface_names(
513            IStudentOnlinePayment, exclude_attribs=False,
514            omit=['display_item', 'certificate', 'student']))) + (
515            'student_id','state','current_session')
516    title = _(u'Student Payments')
517
518    def filter_func(self, x, **kw):
519        return get_payments(x, **kw)
520
521    def mangle_value(self, value, name, context=None):
522        """The mangler determines the student's id, registration
523        state and current session.
524        """
525        if context is not None:
526            student = context.student
527            if name in ['student_id','state',
528                        'current_session'] and student is not None:
529                value = getattr(student, name, None)
530        return super(
531            StudentPaymentExporter, self).mangle_value(
532            value, name, context=context)
533
534class StudentUnpaidPaymentExporter(StudentPaymentExporter):
535    """The Student Unpaid Payment Exporter works just like the
536    Student Payments Exporter but it exports only unpaid tickets.
537    This exporter is designed for finding and finally purging outdated
538    payment ticket.
539    """
540    grok.name('studentunpaidpayments')
541
542    title = _(u'Student Unpaid Payments')
543
544    def filter_func(self, x, **kw):
545        return get_payments(x, p_states=('unpaid',) , **kw)
546
547class DataForBursaryExporter(StudentPaymentExporter):
548    """The Data for Bursary Exporter works just like the Student Payment
549    Exporter but it exports much more information about the student. It combines
550    payment and student data in one table in order to spare postprocessing of
551    two seperate export files. The exporter is primarily used by bursary
552    officers who have exclusively access to this exporter. The exporter
553    exports ``paid``, ``waived`` and ``scholarship`` payment tickets.
554    """
555    grok.name('bursary')
556
557    def filter_func(self, x, **kw):
558        return get_payments(x, p_states=('paid', 'waived', 'scholarship'), **kw)
559
560    fields = tuple(
561        sorted(iface_names(
562            IStudentOnlinePayment, exclude_attribs=False,
563            omit=['display_item', 'certificate', 'student']))) + (
564            'student_id','matric_number','reg_number',
565            'firstname', 'middlename', 'lastname',
566            'state','current_session',
567            'entry_session', 'entry_mode',
568            'faccode', 'depcode','certcode')
569    title = _(u'Payment Data for Bursary')
570
571    def mangle_value(self, value, name, context=None):
572        """The mangler fetches the student data.
573        """
574        if context is not None:
575            student = context.student
576            if name in [
577                'student_id','matric_number', 'reg_number',
578                'firstname', 'middlename', 'lastname',
579                'state', 'current_session',
580                'entry_session', 'entry_mode',
581                'faccode', 'depcode', 'certcode'] and student is not None:
582                value = getattr(student, name, None)
583        return super(
584            StudentPaymentExporter, self).mangle_value(
585            value, name, context=context)
586
587class AccommodationPaymentsExporter(DataForBursaryExporter):
588    """The Accommodation Payments Exporter works like the Data for Bursary
589    Exporter above. The exporter exports ``paid``, ``waived`` and ``scholarship``
590    payment tickets with category ``bed_allocation`` or ``hostel_maintenance``.
591    The exporter is primarily used by accommodation officers who have
592    exclusively access to this exporter.
593    """
594    grok.name('accommodationpayments')
595
596    def filter_func(self, x, **kw):
597        kw['paycat'] = 'bed_allocation'
598        payments = get_payments(x, p_states=(
599          'paid', 'waived', 'scholarship'), **kw)
600        kw['paycat'] = 'hostel_maintenance'
601        payments += get_payments(x, p_states=(
602          'paid', 'waived', 'scholarship'), **kw)
603        return payments
604
605    title = _(u'Accommodation Payments')
606
607class BedTicketExporter(grok.GlobalUtility, StudentExporterBase):
608    """The Bed Ticket Exporter first filters the set of students
609    by searching the students catalog. Then it exports bed
610    tickets by iterating over the items of the student's ``accommodation``
611    container.
612    """
613    grok.name('bedtickets')
614
615    fields = tuple(
616        sorted(iface_names(
617            IBedTicket, exclude_attribs=False,
618            omit=['display_coordinates', 'maint_payment_made']))) + (
619            'student_id', 'actual_bed_type')
620    title = _(u'Bed Tickets')
621
622    def filter_func(self, x, **kw):
623        return get_bedtickets(x)
624
625    def mangle_value(self, value, name, context=None):
626        """The mangler determines the student id and the type of the bed
627        which has been booked in the ticket.
628        """
629        if context is not None:
630            student = context.student
631            if name in ['student_id'] and student is not None:
632                value = getattr(student, name, None)
633        if name == 'bed' and value is not None:
634            value = getattr(value, 'bed_id', None)
635        if name == 'actual_bed_type':
636            value = getattr(getattr(context, 'bed', None), 'bed_type', None)
637        return super(
638            BedTicketExporter, self).mangle_value(
639            value, name, context=context)
640
641class SchoolFeePaymentsOverviewExporter(StudentExporter):
642    """The School Fee Payments Overview Exporter first filters the set of students
643    by searching the students catalog. Then it exports some student base data
644    together with the total school fee amount paid in each year over a
645    predefined year range (current year - 9, ... , current year + 1).
646    """
647    grok.name('sfpaymentsoverview')
648
649    curr_year = datetime.now().year
650    year_range = range(curr_year - 11, curr_year + 1)
651    year_range_tuple = tuple([str(year) for year in year_range])
652    fields = ('student_id', 'matric_number', 'display_fullname',
653        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
654        'current_level', 'current_session', 'current_mode',
655        'entry_session', 'reg_number'
656        ) + year_range_tuple
657    title = _(u'Student School Fee Payments Overview')
658
659    def mangle_value(self, value, name, context=None):
660        """The mangler summarizes the school fee amounts made per year. It
661        iterates over all paid school fee payment tickets and
662        adds together the amounts paid in a year. Waived payments
663        are marked ``waived`` and scholarship payments marked `scholarship`.
664        """
665        if name in self.year_range_tuple and context is not None:
666            value = 0
667            for ticket in context['payments'].values():
668                if ticket.p_category == 'schoolfee' and \
669                    ticket.p_session == int(name):
670                    if ticket.p_state == 'waived':
671                        value = 'waived'
672                        break
673                    if ticket.p_state == 'scholarship':
674                        value = 'scholarship'
675                        break
676                    if ticket.p_state == 'paid':
677                        try:
678                            value += ticket.amount_auth
679                        except TypeError:
680                            pass
681            if value == 0:
682                value = ''
683            elif isinstance(value, float):
684                value = round(value, 2)
685        return super(
686            StudentExporter, self).mangle_value(
687            value, name, context=context)
688
689class SessionPaymentsOverviewExporter(StudentExporter):
690    """The Session Payments Overview Exporter first filters the set of students
691    by searching the students catalog. Then it exports some student base data
692    together with the total amount paid in predefined payment categories
693    over the previous three session (referring to current academic session).
694    Sample output:
695
696    header: ``...schoolfee13,schoolfee14,schoolfee15,gown13,gown14,gown15...``
697
698    data: ``...2000.0,,3000.0,,,1000.0,...``
699
700    This csv data string means that the student paid 2000.0 school fee in 2013
701    and 3000.0 in 2015. S/He furthermore paid 1000.0 for gown rental in 2015.
702    """
703    grok.name('sessionpaymentsoverview')
704
705    paycats = ('schoolfee', 'clearance', 'gown', 'transcript')
706    regular_fields = ('student_id', 'matric_number', 'display_fullname',
707        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
708        'current_level', 'current_session', 'current_mode',
709        'entry_session', 'reg_number'
710        )
711    title = _(u'Session Payments Overview')
712
713    @property
714    def paycatyears(self):
715        cas = grok.getSite()['configuration'].current_academic_session
716        paycatyears = []
717        if cas:
718            year_range = range(cas-2, cas+1)
719            year_range_tuple = tuple([str(year)[2:] for year in year_range])
720            paycatyears = [
721                cat+year for cat in self.paycats for year in year_range_tuple]
722        return paycatyears
723
724    @property
725    def fields(self):
726        return self.regular_fields + tuple(self.paycatyears)
727
728    def mangle_value(self, value, name, context=None):
729        """
730        """
731        amounts = dict()
732        for catyear in self.paycatyears:
733            amounts[catyear] = 0.0
734        if name[:-2] in self.paycats and context is not None:
735            for ticket in context['payments'].values():
736                if ticket.p_category == name[:-2]:
737                    if ticket.p_state in ('waived', 'paid'):
738                        if str(ticket.p_session)[2:] == name[-2:]:
739                            amounts[name] += ticket.amount_auth
740            if amounts[name] == 0.0:
741                value = ''
742            elif isinstance(amounts[name], float):
743                value = round(amounts[name], 2)
744        return super(
745            StudentExporter, self).mangle_value(
746            value, name, context=context)
747
748class StudentStudyLevelsOverviewExporter(StudentExporter):
749    """The Student Study Levels Overview Exporter first filters the set of
750    students by searching the students catalog. Then it exports some student
751    base data together with the session key of registered levels.
752    Sample output:
753
754    header: ``...100,110,120,200,210,220,300...``
755
756    data: ``...2010,,,2011,2012,,2013...``
757
758    This csv data string means that level 100 was registered in session
759    2010/2011, level 200 in session 2011/2012, level 210 (200 on 1st probation)
760    in session 2012/2013 and level 300 in session 2013/2014.
761    """
762    grok.name('studylevelsoverview')
763
764    avail_levels = tuple([str(x) for x in study_levels(None)])
765
766    fields = ('student_id', ) + (
767        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
768        'entry_session', 'current_level', 'current_session',
769        ) + avail_levels
770    title = _(u'Student Study Levels Overview')
771
772    def mangle_value(self, value, name, context=None):
773        """The mangler checks if a given level has been registered. It returns
774        the ``level_session`` attribute of the student study level object
775        if the named level exists.
776        """
777        if name in self.avail_levels and context is not None:
778            value = ''
779            for level in context['studycourse'].values():
780                if level.level == int(name):
781                    value = '%s' % level.level_session
782                    break
783        return super(
784            StudentExporter, self).mangle_value(
785            value, name, context=context)
786
787class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
788    """Like all other exporters the Combo Card Data Exporter first filters the
789    set of students by searching the students catalog. Then it exports some
790    student base data which are neccessary to print for the Interswitch combo
791    card (identity card for students). The output contains a ``passport_path``
792    column which contains the filesystem path of the passport image file.
793    If no path is given, no passport image file exists.
794    """
795    grok.name('combocard')
796
797    fields = ('display_fullname',
798              'student_id','matric_number',
799              'certificate', 'faculty', 'department', 'passport_path')
800    title = _(u'Combo Card Data')
801
802    def mangle_value(self, value, name, context=None):
803        """The mangler determines the titles of faculty, department
804        and certificate. It also computes the path of passport image file
805        stored in the filesystem.
806        """
807        certificate = context['studycourse'].certificate
808        if name == 'certificate' and certificate is not None:
809            value = certificate.title
810        if name == 'department' and certificate is not None:
811            value = certificate.__parent__.__parent__.longtitle
812        if name == 'faculty' and certificate is not None:
813            value = certificate.__parent__.__parent__.__parent__.longtitle
814        if name == 'passport_path' and certificate is not None:
815            file_id = IFileStoreNameChooser(context).chooseName(
816                attr='passport.jpg')
817            os_path = getUtility(IExtFileStore)._pathFromFileID(file_id)
818            if not os.path.exists(os_path):
819                value = None
820            else:
821                value = '/'.join(os_path.split('/')[-4:])
822        return super(
823            ComboCardDataExporter, self).mangle_value(
824            value, name, context=context)
Note: See TracBrowser for help on using the repository browser.