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

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

Improve export and reimport of previous study course data.

  • Property svn:keywords set to Id
File size: 41.1 KB
RevLine 
[8057]1## $Id: export.py 16831 2022-02-24 10:23:10Z 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
[11484]545            if name in ('student_id', 'display_fullname') and student is not None:
[8342]546                value = getattr(student, name, None)
[16831]547        if name == 'previous':
548            return self.previous
[8342]549        return super(
550            CourseTicketExporter, self).mangle_value(
551            value, name, context=context)
552
[16827]553class FirstCourseTicketExporter(CourseTicketExporter):
554    """The First Course Ticket Exporter exports course tickets of
555    first study courses if the student was transferred.
556    """
557    grok.name('coursetickets_1')
[16831]558    previous = 1
[16827]559    title = _(u'First Course Course Tickets (Data Backup)')
560
561    def filter_func(self, x, **kw):
562        return get_tickets(x, 1, **kw)
563
564class SecondCourseTicketExporter(CourseTicketExporter):
565    """The Second Course Ticket Exporter exports course tickets of
566    second study courses if the student was transferred twice.
567    """
568    grok.name('coursetickets_2')
[16831]569    previous = 2
[16827]570    title = _(u'Second Course Course Tickets (Data Backup)')
571
572    def filter_func(self, x, **kw):
573        return get_tickets(x, 2, **kw)
574
[15924]575class StudentPaymentExporter(grok.GlobalUtility, StudentExporterBase):
576    """The Student Payment Exporter first filters the set of students
577    by searching the students catalog. Then it exports student payment
578    tickets by iterating over the items of the student's ``payments``
579    container. If the payment period is given, only tickets, which were
580    paid in payment period, are considered for export.
581    """
582    grok.name('studentpayments')
583
584    fields = tuple(
585        sorted(iface_names(
586            IStudentOnlinePayment, exclude_attribs=False,
[16431]587            omit=['display_item', 'certificate', 'student', 'p_option']))) + (
[15924]588            'student_id','state','current_session')
589    title = _(u'Student Payments (Data Backup)')
590
591    def filter_func(self, x, **kw):
592        return get_payments(x, **kw)
593
594    def mangle_value(self, value, name, context=None):
595        """The mangler determines the student's id, registration
596        state and current session.
597        """
598        if context is not None:
599            student = context.student
[16351]600            if name in ['student_id','state','entry_session',
[15924]601                        'current_session'] and student is not None:
602                value = getattr(student, name, None)
603        return super(
604            StudentPaymentExporter, self).mangle_value(
605            value, name, context=context)
606
[15979]607class StudentTrimmedPaymentExporter(grok.GlobalUtility, StudentExporterBase):
608    """The Student Trimmed Payment Exporter is a slightly customized version
609    of the StudentPaymentExporter requested by Uniben.
610    """
611    grok.name('trimmedpayments')
612
613    fields = tuple(
614        sorted(iface_names(
615            IStudentOnlinePayment, exclude_attribs=False,
[16431]616            omit=['display_item', 'certificate', 'student', 'ac', 'p_option']))) + (
[15979]617            'student_id','faccode', 'depcode', 'state','current_session')
618    title = _(u'Student Payments (Sorted Data)')
619
620    def filter_func(self, x, **kw):
621        payments = get_payments(x, **kw)
622        return sorted([payment for payment in payments],
[15996]623            key=lambda payment: str(payment.p_category) + str(payment.student.faccode)
[15979]624                + str(payment.student.depcode) + str(payment.p_item))
625
626    def mangle_value(self, value, name, context=None):
627        """The mangler determines the student's id, registration
628        state and current session.
629        """
630        if context is not None:
631            student = context.student
632            if name in ['student_id','state', 'faccode', 'depcode',
633                        'current_session'] and student is not None:
634                value = getattr(student, name, None)
635        return super(
636            StudentTrimmedPaymentExporter, self).mangle_value(
637            value, name, context=context)
638
[13894]639class DataForLecturerExporter(grok.GlobalUtility, StudentExporterBase):
[15049]640    """The Data for Lecturer Exporter searches for students in the course
641    tickets catalog and exports those course tickets which belong to the
642    given course code, meet level and session passed through at the
[16412]643    same time, and which are editable by lecturers (disabled on 10/03/21).
644    This exporter can only be called at course level in the academic section.
[13766]645    """
646    grok.name('lecturer')
[8342]647
[13894]648    fields = ('matric_number', 'student_id', 'display_fullname',
[13766]649              'level', 'code', 'level_session', 'score')
650
651    title = _(u'Data for Lecturer')
652
[13894]653    def filter_func(self, x, **kw):
[16006]654        tickets = get_tickets_for_lecturer(x, **kw)
655        return sorted([ticket for ticket in tickets],
656            key=lambda ticket: str(ticket.fcode) + str(ticket.dcode)
657                + str(ticket.student.matric_number))
[13894]658
[13766]659    def mangle_value(self, value, name, context=None):
660        """The mangler determines the student's id and fullname.
661        """
662        if context is not None:
663            student = context.student
[13885]664            if name in ('matric_number',
665                        'reg_number',
666                        'student_id',
667                        'display_fullname',) and student is not None:
[13766]668                value = getattr(student, name, None)
669        return super(
[13894]670            DataForLecturerExporter, self).mangle_value(
[13766]671            value, name, context=context)
672
[15924]673class OutstandingCoursesExporter(grok.GlobalUtility, StudentExporterBase):
[15873]674    """The Student Outstanding Courses Exporter first filters the set of
675    students by searching the students catalog. Then it exports students with
676    lists of outstanding courses, i.e. courses which the student has
677    missed (not registered at all), failed (registered but not passed)
678    or nottaken (registered but not taken).
679    """
[15920]680    grok.name('outstandingcourses')
[15873]681
[15963]682    fields = ('student_id', 'matric_number', 'certcode', 'display_fullname',
683              'missed', 'failed', 'nottaken')
[15920]684    title = _(u'Outstanding Courses')
[15873]685
686    def filter_func(self, x, **kw):
687        return get_outstanding(x, **kw)
688
689    def mangle_value(self, value, name, context=None):
690        """The mangler determines the student's id, fullname and certcode,
691        and it collects the lists of outstanding courses.
692        """
693        if context is not None:
[15963]694            if name in ('student_id', 'matric_number',
695                        'display_fullname', 'certcode'):
[15873]696                value = getattr(context[0], name, None)
697            elif name == 'missed':
[16662]698                value = ' '.join(context[1])
[15873]699            elif name == 'failed':
[16662]700                value = ' '.join(context[2])
[15873]701            elif name == 'nottaken':
[16662]702                value = ' '.join(context[3])
[15873]703        return super(
[15924]704            OutstandingCoursesExporter, self).mangle_value(
[15873]705            value, name, context=context)
706
[15924]707class UnpaidPaymentsExporter(StudentPaymentExporter):
708    """The Unpaid Payments Exporter works just like the
709    Student Payment (singular intended) Exporter  but it exports only
710    unpaid tickets. This exporter is designed for finding and finally
711    purging outdated payment tickets.
[8371]712    """
[15920]713    grok.name('unpaidpayments')
[12971]714
[15920]715    title = _(u'Unpaid Payment Tickets')
[12971]716
717    def filter_func(self, x, **kw):
[14761]718        return get_payments(x, p_states=('unpaid',) , **kw)
[12971]719
[12865]720class DataForBursaryExporter(StudentPaymentExporter):
[15278]721    """The Data for Bursary Exporter works just like the Student Payment
722    Exporter but it exports much more information about the student. It combines
[12862]723    payment and student data in one table in order to spare postprocessing of
724    two seperate export files. The exporter is primarily used by bursary
[14761]725    officers who have exclusively access to this exporter. The exporter
[15792]726    exports ``paid``, ``waived`` and ``scholarship`` payment tickets.
[10233]727    """
728    grok.name('bursary')
729
[10296]730    def filter_func(self, x, **kw):
[15792]731        return get_payments(x, p_states=('paid', 'waived', 'scholarship'), **kw)
[10296]732
[10233]733    fields = tuple(
734        sorted(iface_names(
735            IStudentOnlinePayment, exclude_attribs=False,
[16431]736            omit=['display_item', 'certificate', 'student', 'p_option']))) + (
[11702]737            'student_id','matric_number','reg_number',
[16329]738            'firstname', 'middlename', 'lastname','sex',
[10236]739            'state','current_session',
740            'entry_session', 'entry_mode',
[13943]741            'faccode', 'depcode','certcode')
[10233]742    title = _(u'Payment Data for Bursary')
743
744    def mangle_value(self, value, name, context=None):
[12862]745        """The mangler fetches the student data.
[10233]746        """
747        if context is not None:
748            student = context.student
[10236]749            if name in [
[11702]750                'student_id','matric_number', 'reg_number',
[16333]751                'firstname', 'middlename', 'lastname','sex',
[11702]752                'state', 'current_session',
[10236]753                'entry_session', 'entry_mode',
[11702]754                'faccode', 'depcode', 'certcode'] and student is not None:
[10233]755                value = getattr(student, name, None)
756        return super(
[12865]757            StudentPaymentExporter, self).mangle_value(
[10233]758            value, name, context=context)
759
[15277]760class AccommodationPaymentsExporter(DataForBursaryExporter):
[15278]761    """The Accommodation Payments Exporter works like the Data for Bursary
[15792]762    Exporter above. The exporter exports ``paid``, ``waived`` and ``scholarship``
763    payment tickets with category ``bed_allocation`` or ``hostel_maintenance``.
[15278]764    The exporter is primarily used by accommodation officers who have
765    exclusively access to this exporter.
[15277]766    """
767    grok.name('accommodationpayments')
768
769    def filter_func(self, x, **kw):
770        kw['paycat'] = 'bed_allocation'
[15792]771        payments = get_payments(x, p_states=(
772          'paid', 'waived', 'scholarship'), **kw)
[15277]773        kw['paycat'] = 'hostel_maintenance'
[15792]774        payments += get_payments(x, p_states=(
775          'paid', 'waived', 'scholarship'), **kw)
[15277]776        return payments
777
778    title = _(u'Accommodation Payments')
779
[12865]780class BedTicketExporter(grok.GlobalUtility, StudentExporterBase):
781    """The Bed Ticket Exporter first filters the set of students
[12862]782    by searching the students catalog. Then it exports bed
783    tickets by iterating over the items of the student's ``accommodation``
784    container.
[9427]785    """
786    grok.name('bedtickets')
787
788    fields = tuple(
789        sorted(iface_names(
[9984]790            IBedTicket, exclude_attribs=False,
[13314]791            omit=['display_coordinates', 'maint_payment_made']))) + (
[9984]792            'student_id', 'actual_bed_type')
[15920]793    title = _(u'Bed Tickets (Data Backup)')
[9427]794
[9844]795    def filter_func(self, x, **kw):
[9802]796        return get_bedtickets(x)
797
[9427]798    def mangle_value(self, value, name, context=None):
[12862]799        """The mangler determines the student id and the type of the bed
800        which has been booked in the ticket.
[9427]801        """
802        if context is not None:
803            student = context.student
804            if name in ['student_id'] and student is not None:
805                value = getattr(student, name, None)
806        if name == 'bed' and value is not None:
807            value = getattr(value, 'bed_id', None)
808        if name == 'actual_bed_type':
[14395]809            value = getattr(getattr(context, 'bed', None), 'bed_type', None)
[9427]810        return super(
[12865]811            BedTicketExporter, self).mangle_value(
[9427]812            value, name, context=context)
813
[15047]814class SchoolFeePaymentsOverviewExporter(StudentExporter):
815    """The School Fee Payments Overview Exporter first filters the set of students
[12862]816    by searching the students catalog. Then it exports some student base data
817    together with the total school fee amount paid in each year over a
818    predefined year range (current year - 9, ... , current year + 1).
[9574]819    """
[15047]820    grok.name('sfpaymentsoverview')
[9574]821
822    curr_year = datetime.now().year
[14596]823    year_range = range(curr_year - 11, curr_year + 1)
[9574]824    year_range_tuple = tuple([str(year) for year in year_range])
[9983]825    fields = ('student_id', 'matric_number', 'display_fullname',
[9574]826        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
[13641]827        'current_level', 'current_session', 'current_mode',
828        'entry_session', 'reg_number'
[9574]829        ) + year_range_tuple
[15920]830    title = _(u'School Fee Payments Overview')
[9574]831
832    def mangle_value(self, value, name, context=None):
[12862]833        """The mangler summarizes the school fee amounts made per year. It
834        iterates over all paid school fee payment tickets and
835        adds together the amounts paid in a year. Waived payments
[15792]836        are marked ``waived`` and scholarship payments marked `scholarship`.
[12862]837        """
[9574]838        if name in self.year_range_tuple and context is not None:
[11661]839            value = 0
[9574]840            for ticket in context['payments'].values():
[12568]841                if ticket.p_category == 'schoolfee' and \
[9574]842                    ticket.p_session == int(name):
[12568]843                    if ticket.p_state == 'waived':
844                        value = 'waived'
845                        break
[15792]846                    if ticket.p_state == 'scholarship':
847                        value = 'scholarship'
848                        break
[12568]849                    if ticket.p_state == 'paid':
850                        try:
851                            value += ticket.amount_auth
852                        except TypeError:
853                            pass
[11661]854            if value == 0:
855                value = ''
[14367]856            elif isinstance(value, float):
857                value = round(value, 2)
[9574]858        return super(
[12079]859            StudentExporter, self).mangle_value(
[9734]860            value, name, context=context)
[9744]861
[15051]862class SessionPaymentsOverviewExporter(StudentExporter):
863    """The Session Payments Overview Exporter first filters the set of students
864    by searching the students catalog. Then it exports some student base data
[15060]865    together with the total amount paid in predefined payment categories
866    over the previous three session (referring to current academic session).
867    Sample output:
868
869    header: ``...schoolfee13,schoolfee14,schoolfee15,gown13,gown14,gown15...``
870
871    data: ``...2000.0,,3000.0,,,1000.0,...``
872
[15062]873    This csv data string means that the student paid 2000.0 school fee in 2013
874    and 3000.0 in 2015. S/He furthermore paid 1000.0 for gown rental in 2015.
[15051]875    """
876    grok.name('sessionpaymentsoverview')
877
878    paycats = ('schoolfee', 'clearance', 'gown', 'transcript')
[15060]879    regular_fields = ('student_id', 'matric_number', 'display_fullname',
[15051]880        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
881        'current_level', 'current_session', 'current_mode',
882        'entry_session', 'reg_number'
[15060]883        )
[15051]884    title = _(u'Session Payments Overview')
885
[15060]886    @property
887    def paycatyears(self):
888        cas = grok.getSite()['configuration'].current_academic_session
889        paycatyears = []
890        if cas:
[15918]891            year_range = range(cas-2, cas+1)
[15060]892            year_range_tuple = tuple([str(year)[2:] for year in year_range])
893            paycatyears = [
894                cat+year for cat in self.paycats for year in year_range_tuple]
895        return paycatyears
896
897    @property
898    def fields(self):
899        return self.regular_fields + tuple(self.paycatyears)
900
[15051]901    def mangle_value(self, value, name, context=None):
902        """
903        """
[15060]904        amounts = dict()
905        for catyear in self.paycatyears:
906            amounts[catyear] = 0.0
907        if name[:-2] in self.paycats and context is not None:
[15051]908            for ticket in context['payments'].values():
[15060]909                if ticket.p_category == name[:-2]:
[15051]910                    if ticket.p_state in ('waived', 'paid'):
[15060]911                        if str(ticket.p_session)[2:] == name[-2:]:
912                            amounts[name] += ticket.amount_auth
913            if amounts[name] == 0.0:
914                value = ''
915            elif isinstance(amounts[name], float):
916                value = round(amounts[name], 2)
[15051]917        return super(
918            StudentExporter, self).mangle_value(
919            value, name, context=context)
920
[15921]921class StudyLevelsOverviewExporter(StudentExporter):
[12862]922    """The Student Study Levels Overview Exporter first filters the set of
923    students by searching the students catalog. Then it exports some student
924    base data together with the session key of registered levels.
925    Sample output:
926
927    header: ``...100,110,120,200,210,220,300...``
928
929    data: ``...2010,,,2011,2012,,2013...``
930
931    This csv data string means that level 100 was registered in session
932    2010/2011, level 200 in session 2011/2012, level 210 (200 on 1st probation)
933    in session 2012/2013 and level 300 in session 2013/2014.
[9744]934    """
935    grok.name('studylevelsoverview')
936
[9787]937    avail_levels = tuple([str(x) for x in study_levels(None)])
[9744]938
939    fields = ('student_id', ) + (
940        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
[9761]941        'entry_session', 'current_level', 'current_session',
[9787]942        ) + avail_levels
[15920]943    title = _(u'Study Levels Overview')
[9744]944
945    def mangle_value(self, value, name, context=None):
[12862]946        """The mangler checks if a given level has been registered. It returns
947        the ``level_session`` attribute of the student study level object
948        if the named level exists.
949        """
[9787]950        if name in self.avail_levels and context is not None:
[9744]951            value = ''
952            for level in context['studycourse'].values():
953                if level.level == int(name):
[9761]954                    value = '%s' % level.level_session
[9744]955                    break
956        return super(
[12079]957            StudentExporter, self).mangle_value(
[9744]958            value, name, context=context)
[9936]959
960class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
[12862]961    """Like all other exporters the Combo Card Data Exporter first filters the
962    set of students by searching the students catalog. Then it exports some
963    student base data which are neccessary to print for the Interswitch combo
964    card (identity card for students). The output contains a ``passport_path``
965    column which contains the filesystem path of the passport image file.
966    If no path is given, no passport image file exists.
[9936]967    """
968    grok.name('combocard')
969
970    fields = ('display_fullname',
[9937]971              'student_id','matric_number',
972              'certificate', 'faculty', 'department', 'passport_path')
[9936]973    title = _(u'Combo Card Data')
974
975    def mangle_value(self, value, name, context=None):
[12862]976        """The mangler determines the titles of faculty, department
977        and certificate. It also computes the path of passport image file
978        stored in the filesystem.
979        """
[9936]980        certificate = context['studycourse'].certificate
981        if name == 'certificate' and certificate is not None:
982            value = certificate.title
983        if name == 'department' and certificate is not None:
[10650]984            value = certificate.__parent__.__parent__.longtitle
[9936]985        if name == 'faculty' and certificate is not None:
[10650]986            value = certificate.__parent__.__parent__.__parent__.longtitle
[9937]987        if name == 'passport_path' and certificate is not None:
[12862]988            file_id = IFileStoreNameChooser(context).chooseName(
989                attr='passport.jpg')
[9937]990            os_path = getUtility(IExtFileStore)._pathFromFileID(file_id)
991            if not os.path.exists(os_path):
992                value = None
993            else:
994                value = '/'.join(os_path.split('/')[-4:])
[9936]995        return super(
996            ComboCardDataExporter, self).mangle_value(
[15920]997            value, name, context=context)
998
[15921]999class TranscriptDataExporter(StudentExporter):
1000    """The Transcript Data Exporter first filters the set of
[15920]1001    students by searching the students catalog. Then it exports student data
1002    along with their transcript data.
1003    """
1004    grok.name('transcriptdata')
1005
1006    fields = ('student_id', ) + (
1007        'state', 'certcode', 'faccode', 'depcode',
1008        'entry_session', 'current_level', 'current_session',
1009        'transcript_data')
1010    title = _(u'Transcript Data')
1011
1012    def mangle_value(self, value, name, context=None):
[15924]1013        """The mangler determines and formats the transcript data.
[15920]1014        """
1015        if name == 'transcript_data':
1016            value = {}
1017            td = context['studycourse'].getTranscriptData()[0]
1018            for level in td:
1019                tickets_1 = ','.join(i.code for i in level['tickets_1'])
1020                tickets_2 = ','.join(i.code for i in level['tickets_2'])
1021                tickets_3 = ','.join(i.code for i in level['tickets_3'])
1022                value = "Level %s; 1st: %s; 2nd: %s; 3rd: %s; sgpa: %s" % (
1023                    level['level_key'], tickets_1, tickets_2,
1024                    tickets_3, level['sgpa'],
1025                    )
1026        return super(
[15921]1027            TranscriptDataExporter, self).mangle_value(
[9936]1028            value, name, context=context)
Note: See TracBrowser for help on using the repository browser.