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

Last change on this file since 17574 was 17557, checked in by Henrik Bettermann, 15 months ago

Allow export of matric_number (not applied in base package).

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