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

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

Make deepcopy first before removing items from the list. Otherwise items will be skipped in the for loop.

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