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

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

Sort export of "Data for Lecturer" by fcode, dcode and matric_number.

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