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

Last change on this file since 17860 was 17641, checked in by Henrik Bettermann, 14 months ago

Catch AttributeError?.

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