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

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

Add exporters for previous study course data.

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