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

Last change on this file since 16207 was 16186, checked in by Henrik Bettermann, 4 years ago

reg_number is no longer a required field in IApplicantBaseData.

Add entry_session to student data exporters.

  • Property svn:keywords set to Id
File size: 37.3 KB
Line 
1## $Id: export.py 16186 2020-08-04 07:49:51Z 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 payment tickets 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 bed tickets 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', 'entry_session')
331    title = _(u'Students (Data Backup)')
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
347class TrimmedDataExporter(grok.GlobalUtility, StudentExporterBase):
348    """The Student Trimmed Data Exporter first filters the set of students
349    by searching the students catalog. Then it exports a trimmed data set
350    of this set of students.
351    """
352    grok.name('trimmed')
353
354    fields = (
355        'student_id',
356        'matric_number',
357        'reg_number',
358        'firstname',
359        'middlename',
360        'lastname',
361        'sex',
362        'email',
363        'phone',
364        'nationality',
365        'date_of_birth',
366        'state',
367        'current_mode',
368        'certcode',
369        'faccode',
370        'depcode',
371        'current_level',
372        'current_session',
373        'current_verdict',
374        'entry_session')
375    title = _(u'Students (Trimmed Data)')
376
377    def mangle_value(self, value, name, context=None):
378        """The mangler prepares the history messages and adds a hash symbol at
379        the end of the phone number to avoid annoying automatic number
380        transformation by Excel or Calc."""
381        if 'phone' in name and value is not None:
382            # Append hash '#' to phone numbers to circumvent
383            # unwanted excel automatic
384            value = str('%s#' % value)
385        return super(
386            TrimmedDataExporter, self).mangle_value(
387            value, name, context=context)
388
389class StudentStudyCourseExporter(grok.GlobalUtility, StudentExporterBase):
390    """The Student Study Course Exporter first filters the set of students
391    by searching the students catalog. Then it exports the data of the current
392    study course container of each student from this set. It does
393    not export their content.
394    """
395    grok.name('studentstudycourses')
396
397    fields = tuple(sorted(iface_names(IStudentStudyCourse))) + ('student_id',)
398    title = _(u'Student Study Courses (Data Backup)')
399
400    def filter_func(self, x, **kw):
401        return get_studycourses(x)
402
403    def mangle_value(self, value, name, context=None):
404        """The mangler determines the certificate code and the student id.
405        """
406        if name == 'certificate' and value is not None:
407            # XXX: hopefully cert codes are unique site-wide
408            value = value.code
409        if name == 'student_id' and context is not None:
410            student = context.student
411            value = getattr(student, name, None)
412        return super(
413            StudentStudyCourseExporter, self).mangle_value(
414            value, name, context=context)
415
416
417class StudentStudyLevelExporter(grok.GlobalUtility, StudentExporterBase):
418    """The Student Study Level Exporter first filters the set of students
419    by searching the students catalog. Then it exports the data of the student's
420    study level container data but not their content (course tickets).
421    The exporter iterates over all objects in the students' ``studycourse``
422    containers.
423    """
424    grok.name('studentstudylevels')
425
426    fields = tuple(sorted(iface_names(
427        IStudentStudyLevel))) + (
428        'student_id', 'number_of_tickets','certcode')
429    title = _(u'Student Study Levels (Data Backup)')
430
431    def filter_func(self, x, **kw):
432        return get_levels(x, **kw)
433
434    def mangle_value(self, value, name, context=None):
435        """The mangler determines the student id, nothing else.
436        """
437        if name == 'student_id' and context is not None:
438            student = context.student
439            value = getattr(student, name, None)
440        return super(
441            StudentStudyLevelExporter, self).mangle_value(
442            value, name, context=context)
443
444class CourseTicketExporter(grok.GlobalUtility, StudentExporterBase):
445    """The Course Ticket Exporter exports course tickets. Usually,
446    the exporter first filters the set of students by searching the
447    students catalog. Then it collects and iterates over all ``studylevel``
448    containers of the filtered student set and finally
449    iterates over all items inside these containers.
450
451    If the course code is passed through, the exporter uses a different
452    catalog. It searches for students in the course tickets catalog and
453    exports those course tickets which belong to the given course code and
454    also meet level and session passed through at the same time.
455    This happens if the exporter is called at course level in the academic
456    section.
457    """
458    grok.name('coursetickets')
459
460    fields = tuple(sorted(iface_names(ICourseTicket) +
461        ['level', 'code', 'level_session'])) + ('student_id',
462        'certcode', 'display_fullname')
463    title = _(u'Course Tickets (Data Backup)')
464
465    def filter_func(self, x, **kw):
466        return get_tickets(x, **kw)
467
468    def mangle_value(self, value, name, context=None):
469        """The mangler determines the student's id and fullname.
470        """
471        if context is not None:
472            student = context.student
473            if name in ('student_id', 'display_fullname') and student is not None:
474                value = getattr(student, name, None)
475        return super(
476            CourseTicketExporter, self).mangle_value(
477            value, name, context=context)
478
479class StudentPaymentExporter(grok.GlobalUtility, StudentExporterBase):
480    """The Student Payment Exporter first filters the set of students
481    by searching the students catalog. Then it exports student payment
482    tickets by iterating over the items of the student's ``payments``
483    container. If the payment period is given, only tickets, which were
484    paid in payment period, are considered for export.
485    """
486    grok.name('studentpayments')
487
488    fields = tuple(
489        sorted(iface_names(
490            IStudentOnlinePayment, exclude_attribs=False,
491            omit=['display_item', 'certificate', 'student']))) + (
492            'student_id','state','current_session')
493    title = _(u'Student Payments (Data Backup)')
494
495    def filter_func(self, x, **kw):
496        return get_payments(x, **kw)
497
498    def mangle_value(self, value, name, context=None):
499        """The mangler determines the student's id, registration
500        state and current session.
501        """
502        if context is not None:
503            student = context.student
504            if name in ['student_id','state',
505                        'current_session'] and student is not None:
506                value = getattr(student, name, None)
507        return super(
508            StudentPaymentExporter, self).mangle_value(
509            value, name, context=context)
510
511class StudentTrimmedPaymentExporter(grok.GlobalUtility, StudentExporterBase):
512    """The Student Trimmed Payment Exporter is a slightly customized version
513    of the StudentPaymentExporter requested by Uniben.
514    """
515    grok.name('trimmedpayments')
516
517    fields = tuple(
518        sorted(iface_names(
519            IStudentOnlinePayment, exclude_attribs=False,
520            omit=['display_item', 'certificate', 'student', 'ac']))) + (
521            'student_id','faccode', 'depcode', 'state','current_session')
522    title = _(u'Student Payments (Sorted Data)')
523
524    def filter_func(self, x, **kw):
525        payments = get_payments(x, **kw)
526        return sorted([payment for payment in payments],
527            key=lambda payment: str(payment.p_category) + str(payment.student.faccode)
528                + str(payment.student.depcode) + str(payment.p_item))
529
530    def mangle_value(self, value, name, context=None):
531        """The mangler determines the student's id, registration
532        state and current session.
533        """
534        if context is not None:
535            student = context.student
536            if name in ['student_id','state', 'faccode', 'depcode',
537                        'current_session'] and student is not None:
538                value = getattr(student, name, None)
539        return super(
540            StudentTrimmedPaymentExporter, self).mangle_value(
541            value, name, context=context)
542
543class DataForLecturerExporter(grok.GlobalUtility, StudentExporterBase):
544    """The Data for Lecturer Exporter searches for students in the course
545    tickets catalog and exports those course tickets which belong to the
546    given course code, meet level and session passed through at the
547    same time, and which are editable by lecturers. This exporter can only
548    be called at course level in the academic section.
549    """
550    grok.name('lecturer')
551
552    fields = ('matric_number', 'student_id', 'display_fullname',
553              'level', 'code', 'level_session', 'score')
554
555    title = _(u'Data for Lecturer')
556
557    def filter_func(self, x, **kw):
558        tickets = get_tickets_for_lecturer(x, **kw)
559        return sorted([ticket for ticket in tickets],
560            key=lambda ticket: str(ticket.fcode) + str(ticket.dcode)
561                + str(ticket.student.matric_number))
562
563    def mangle_value(self, value, name, context=None):
564        """The mangler determines the student's id and fullname.
565        """
566        if context is not None:
567            student = context.student
568            if name in ('matric_number',
569                        'reg_number',
570                        'student_id',
571                        'display_fullname',) and student is not None:
572                value = getattr(student, name, None)
573        return super(
574            DataForLecturerExporter, self).mangle_value(
575            value, name, context=context)
576
577class OutstandingCoursesExporter(grok.GlobalUtility, StudentExporterBase):
578    """The Student Outstanding Courses Exporter first filters the set of
579    students by searching the students catalog. Then it exports students with
580    lists of outstanding courses, i.e. courses which the student has
581    missed (not registered at all), failed (registered but not passed)
582    or nottaken (registered but not taken).
583    """
584    grok.name('outstandingcourses')
585
586    fields = ('student_id', 'matric_number', 'certcode', 'display_fullname',
587              'missed', 'failed', 'nottaken')
588    title = _(u'Outstanding Courses')
589
590    def filter_func(self, x, **kw):
591        return get_outstanding(x, **kw)
592
593    def mangle_value(self, value, name, context=None):
594        """The mangler determines the student's id, fullname and certcode,
595        and it collects the lists of outstanding courses.
596        """
597        if context is not None:
598            if name in ('student_id', 'matric_number',
599                        'display_fullname', 'certcode'):
600                value = getattr(context[0], name, None)
601            elif name == 'missed':
602                value = context[1]
603            elif name == 'failed':
604                value = context[2]
605            elif name == 'nottaken':
606                value = context[3]
607        return super(
608            OutstandingCoursesExporter, self).mangle_value(
609            value, name, context=context)
610
611class UnpaidPaymentsExporter(StudentPaymentExporter):
612    """The Unpaid Payments Exporter works just like the
613    Student Payment (singular intended) Exporter  but it exports only
614    unpaid tickets. This exporter is designed for finding and finally
615    purging outdated payment tickets.
616    """
617    grok.name('unpaidpayments')
618
619    title = _(u'Unpaid Payment Tickets')
620
621    def filter_func(self, x, **kw):
622        return get_payments(x, p_states=('unpaid',) , **kw)
623
624class DataForBursaryExporter(StudentPaymentExporter):
625    """The Data for Bursary Exporter works just like the Student Payment
626    Exporter but it exports much more information about the student. It combines
627    payment and student data in one table in order to spare postprocessing of
628    two seperate export files. The exporter is primarily used by bursary
629    officers who have exclusively access to this exporter. The exporter
630    exports ``paid``, ``waived`` and ``scholarship`` payment tickets.
631    """
632    grok.name('bursary')
633
634    def filter_func(self, x, **kw):
635        return get_payments(x, p_states=('paid', 'waived', 'scholarship'), **kw)
636
637    fields = tuple(
638        sorted(iface_names(
639            IStudentOnlinePayment, exclude_attribs=False,
640            omit=['display_item', 'certificate', 'student']))) + (
641            'student_id','matric_number','reg_number',
642            'firstname', 'middlename', 'lastname',
643            'state','current_session',
644            'entry_session', 'entry_mode',
645            'faccode', 'depcode','certcode')
646    title = _(u'Payment Data for Bursary')
647
648    def mangle_value(self, value, name, context=None):
649        """The mangler fetches the student data.
650        """
651        if context is not None:
652            student = context.student
653            if name in [
654                'student_id','matric_number', 'reg_number',
655                'firstname', 'middlename', 'lastname',
656                'state', 'current_session',
657                'entry_session', 'entry_mode',
658                'faccode', 'depcode', 'certcode'] and student is not None:
659                value = getattr(student, name, None)
660        return super(
661            StudentPaymentExporter, self).mangle_value(
662            value, name, context=context)
663
664class AccommodationPaymentsExporter(DataForBursaryExporter):
665    """The Accommodation Payments Exporter works like the Data for Bursary
666    Exporter above. The exporter exports ``paid``, ``waived`` and ``scholarship``
667    payment tickets with category ``bed_allocation`` or ``hostel_maintenance``.
668    The exporter is primarily used by accommodation officers who have
669    exclusively access to this exporter.
670    """
671    grok.name('accommodationpayments')
672
673    def filter_func(self, x, **kw):
674        kw['paycat'] = 'bed_allocation'
675        payments = get_payments(x, p_states=(
676          'paid', 'waived', 'scholarship'), **kw)
677        kw['paycat'] = 'hostel_maintenance'
678        payments += get_payments(x, p_states=(
679          'paid', 'waived', 'scholarship'), **kw)
680        return payments
681
682    title = _(u'Accommodation Payments')
683
684class BedTicketExporter(grok.GlobalUtility, StudentExporterBase):
685    """The Bed Ticket Exporter first filters the set of students
686    by searching the students catalog. Then it exports bed
687    tickets by iterating over the items of the student's ``accommodation``
688    container.
689    """
690    grok.name('bedtickets')
691
692    fields = tuple(
693        sorted(iface_names(
694            IBedTicket, exclude_attribs=False,
695            omit=['display_coordinates', 'maint_payment_made']))) + (
696            'student_id', 'actual_bed_type')
697    title = _(u'Bed Tickets (Data Backup)')
698
699    def filter_func(self, x, **kw):
700        return get_bedtickets(x)
701
702    def mangle_value(self, value, name, context=None):
703        """The mangler determines the student id and the type of the bed
704        which has been booked in the ticket.
705        """
706        if context is not None:
707            student = context.student
708            if name in ['student_id'] and student is not None:
709                value = getattr(student, name, None)
710        if name == 'bed' and value is not None:
711            value = getattr(value, 'bed_id', None)
712        if name == 'actual_bed_type':
713            value = getattr(getattr(context, 'bed', None), 'bed_type', None)
714        return super(
715            BedTicketExporter, self).mangle_value(
716            value, name, context=context)
717
718class SchoolFeePaymentsOverviewExporter(StudentExporter):
719    """The School Fee Payments Overview Exporter first filters the set of students
720    by searching the students catalog. Then it exports some student base data
721    together with the total school fee amount paid in each year over a
722    predefined year range (current year - 9, ... , current year + 1).
723    """
724    grok.name('sfpaymentsoverview')
725
726    curr_year = datetime.now().year
727    year_range = range(curr_year - 11, curr_year + 1)
728    year_range_tuple = tuple([str(year) for year in year_range])
729    fields = ('student_id', 'matric_number', 'display_fullname',
730        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
731        'current_level', 'current_session', 'current_mode',
732        'entry_session', 'reg_number'
733        ) + year_range_tuple
734    title = _(u'School Fee Payments Overview')
735
736    def mangle_value(self, value, name, context=None):
737        """The mangler summarizes the school fee amounts made per year. It
738        iterates over all paid school fee payment tickets and
739        adds together the amounts paid in a year. Waived payments
740        are marked ``waived`` and scholarship payments marked `scholarship`.
741        """
742        if name in self.year_range_tuple and context is not None:
743            value = 0
744            for ticket in context['payments'].values():
745                if ticket.p_category == 'schoolfee' and \
746                    ticket.p_session == int(name):
747                    if ticket.p_state == 'waived':
748                        value = 'waived'
749                        break
750                    if ticket.p_state == 'scholarship':
751                        value = 'scholarship'
752                        break
753                    if ticket.p_state == 'paid':
754                        try:
755                            value += ticket.amount_auth
756                        except TypeError:
757                            pass
758            if value == 0:
759                value = ''
760            elif isinstance(value, float):
761                value = round(value, 2)
762        return super(
763            StudentExporter, self).mangle_value(
764            value, name, context=context)
765
766class SessionPaymentsOverviewExporter(StudentExporter):
767    """The Session Payments Overview Exporter first filters the set of students
768    by searching the students catalog. Then it exports some student base data
769    together with the total amount paid in predefined payment categories
770    over the previous three session (referring to current academic session).
771    Sample output:
772
773    header: ``...schoolfee13,schoolfee14,schoolfee15,gown13,gown14,gown15...``
774
775    data: ``...2000.0,,3000.0,,,1000.0,...``
776
777    This csv data string means that the student paid 2000.0 school fee in 2013
778    and 3000.0 in 2015. S/He furthermore paid 1000.0 for gown rental in 2015.
779    """
780    grok.name('sessionpaymentsoverview')
781
782    paycats = ('schoolfee', 'clearance', 'gown', 'transcript')
783    regular_fields = ('student_id', 'matric_number', 'display_fullname',
784        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
785        'current_level', 'current_session', 'current_mode',
786        'entry_session', 'reg_number'
787        )
788    title = _(u'Session Payments Overview')
789
790    @property
791    def paycatyears(self):
792        cas = grok.getSite()['configuration'].current_academic_session
793        paycatyears = []
794        if cas:
795            year_range = range(cas-2, cas+1)
796            year_range_tuple = tuple([str(year)[2:] for year in year_range])
797            paycatyears = [
798                cat+year for cat in self.paycats for year in year_range_tuple]
799        return paycatyears
800
801    @property
802    def fields(self):
803        return self.regular_fields + tuple(self.paycatyears)
804
805    def mangle_value(self, value, name, context=None):
806        """
807        """
808        amounts = dict()
809        for catyear in self.paycatyears:
810            amounts[catyear] = 0.0
811        if name[:-2] in self.paycats and context is not None:
812            for ticket in context['payments'].values():
813                if ticket.p_category == name[:-2]:
814                    if ticket.p_state in ('waived', 'paid'):
815                        if str(ticket.p_session)[2:] == name[-2:]:
816                            amounts[name] += ticket.amount_auth
817            if amounts[name] == 0.0:
818                value = ''
819            elif isinstance(amounts[name], float):
820                value = round(amounts[name], 2)
821        return super(
822            StudentExporter, self).mangle_value(
823            value, name, context=context)
824
825class StudyLevelsOverviewExporter(StudentExporter):
826    """The Student Study Levels Overview Exporter first filters the set of
827    students by searching the students catalog. Then it exports some student
828    base data together with the session key of registered levels.
829    Sample output:
830
831    header: ``...100,110,120,200,210,220,300...``
832
833    data: ``...2010,,,2011,2012,,2013...``
834
835    This csv data string means that level 100 was registered in session
836    2010/2011, level 200 in session 2011/2012, level 210 (200 on 1st probation)
837    in session 2012/2013 and level 300 in session 2013/2014.
838    """
839    grok.name('studylevelsoverview')
840
841    avail_levels = tuple([str(x) for x in study_levels(None)])
842
843    fields = ('student_id', ) + (
844        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
845        'entry_session', 'current_level', 'current_session',
846        ) + avail_levels
847    title = _(u'Study Levels Overview')
848
849    def mangle_value(self, value, name, context=None):
850        """The mangler checks if a given level has been registered. It returns
851        the ``level_session`` attribute of the student study level object
852        if the named level exists.
853        """
854        if name in self.avail_levels and context is not None:
855            value = ''
856            for level in context['studycourse'].values():
857                if level.level == int(name):
858                    value = '%s' % level.level_session
859                    break
860        return super(
861            StudentExporter, self).mangle_value(
862            value, name, context=context)
863
864class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
865    """Like all other exporters the Combo Card Data Exporter first filters the
866    set of students by searching the students catalog. Then it exports some
867    student base data which are neccessary to print for the Interswitch combo
868    card (identity card for students). The output contains a ``passport_path``
869    column which contains the filesystem path of the passport image file.
870    If no path is given, no passport image file exists.
871    """
872    grok.name('combocard')
873
874    fields = ('display_fullname',
875              'student_id','matric_number',
876              'certificate', 'faculty', 'department', 'passport_path')
877    title = _(u'Combo Card Data')
878
879    def mangle_value(self, value, name, context=None):
880        """The mangler determines the titles of faculty, department
881        and certificate. It also computes the path of passport image file
882        stored in the filesystem.
883        """
884        certificate = context['studycourse'].certificate
885        if name == 'certificate' and certificate is not None:
886            value = certificate.title
887        if name == 'department' and certificate is not None:
888            value = certificate.__parent__.__parent__.longtitle
889        if name == 'faculty' and certificate is not None:
890            value = certificate.__parent__.__parent__.__parent__.longtitle
891        if name == 'passport_path' and certificate is not None:
892            file_id = IFileStoreNameChooser(context).chooseName(
893                attr='passport.jpg')
894            os_path = getUtility(IExtFileStore)._pathFromFileID(file_id)
895            if not os.path.exists(os_path):
896                value = None
897            else:
898                value = '/'.join(os_path.split('/')[-4:])
899        return super(
900            ComboCardDataExporter, self).mangle_value(
901            value, name, context=context)
902
903class TranscriptDataExporter(StudentExporter):
904    """The Transcript Data Exporter first filters the set of
905    students by searching the students catalog. Then it exports student data
906    along with their transcript data.
907    """
908    grok.name('transcriptdata')
909
910    fields = ('student_id', ) + (
911        'state', 'certcode', 'faccode', 'depcode',
912        'entry_session', 'current_level', 'current_session',
913        'transcript_data')
914    title = _(u'Transcript Data')
915
916    def mangle_value(self, value, name, context=None):
917        """The mangler determines and formats the transcript data.
918        """
919        if name == 'transcript_data':
920            value = {}
921            td = context['studycourse'].getTranscriptData()[0]
922            for level in td:
923                tickets_1 = ','.join(i.code for i in level['tickets_1'])
924                tickets_2 = ','.join(i.code for i in level['tickets_2'])
925                tickets_3 = ','.join(i.code for i in level['tickets_3'])
926                value = "Level %s; 1st: %s; 2nd: %s; 3rd: %s; sgpa: %s" % (
927                    level['level_key'], tickets_1, tickets_2,
928                    tickets_3, level['sgpa'],
929                    )
930        return super(
931            TranscriptDataExporter, self).mangle_value(
932            value, name, context=context)
Note: See TracBrowser for help on using the repository browser.