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

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

Add StudentOutstandingCoursesExporter.

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