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

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

Add semester filter to Fix CourseTicketExporter.

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