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

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

Allow lecturers to export all course tickets, not only those of current academic session.

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