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

Last change on this file since 16671 was 16666, checked in by Henrik Bettermann, 3 years ago

Do r16662 right.

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