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

Last change on this file since 12590 was 12568, checked in by Henrik Bettermann, 10 years ago

Add new payment state 'waived'. This state can only be set by import
and is only used for the payments overview exporter to
mark sessions with waived fees.

  • Property svn:keywords set to Id
File size: 20.2 KB
RevLine 
[8057]1## $Id: export.py 12568 2015-02-07 11:26:19Z 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
[9574]22from datetime import datetime
[9937]23from zope.component import getUtility
[11757]24from waeup.kofa.interfaces import (
25    IExtFileStore, IFileStoreNameChooser, IKofaUtils)
[7944]26from waeup.kofa.interfaces import MessageFactory as _
[9843]27from waeup.kofa.students.catalog import StudentsQuery, CourseTicketsQuery
[8015]28from waeup.kofa.students.interfaces import (
[8371]29    IStudent, IStudentStudyCourse, IStudentStudyLevel, ICourseTicket,
[9427]30    IStudentOnlinePayment, ICSVStudentExporter, IBedTicket)
[9787]31from waeup.kofa.students.vocabularies import study_levels
[7944]32from waeup.kofa.utils.batching import ExporterBase
[11757]33from waeup.kofa.utils.helpers import iface_names, to_timezone
[7944]34
[8400]35
[9787]36def get_students(site, stud_filter=StudentsQuery()):
[8414]37    """Get all students registered in catalog in `site`.
[7944]38    """
[9787]39    return stud_filter.query()
[8414]40
41def get_studycourses(students):
42    """Get studycourses of `students`.
43    """
44    return [x.get('studycourse', None) for x in students
45            if x is not None]
46
47def get_levels(students):
48    """Get all studylevels of `students`.
49    """
50    levels = []
51    for course in get_studycourses(students):
52        for level in course.values():
53            levels.append(level)
54    return levels
55
[9844]56def get_tickets(students, **kw):
57    """Get course tickets of `students`.
58
[10017]59    If code is passed through, filter course tickets
60    which belong to this course code and meets level
61    and level_session.
[8414]62    """
63    tickets = []
[9844]64    code = kw.get('code', None)
[10017]65    level = kw.get('level', None)
66    level_session = kw.get('level_session', None)
[9844]67    if code is None:
[10017]68        for level_obj in get_levels(students):
69            for ticket in level_obj.values():
[9844]70                tickets.append(ticket)
71    else:
[10017]72        for level_obj in get_levels(students):
73            for ticket in level_obj.values():
74                if ticket.code != code:
75                    continue
[11483]76                if level is not None:
77                    level = int(level)
78                    if level_obj.level in (10, 999, None)  \
79                        and int(level) != level_obj.level:
80                        continue
81                    if level_obj.level not in range(level, level+100, 10):
82                        continue
[10017]83                if level_session is not None and \
84                    int(level_session) != level_obj.level_session:
85                    continue
86                tickets.append(ticket)
[8414]87    return tickets
88
[11730]89def get_payments(students, paid=False, **kw):
90    """Get all payments of `students` within given payment_date period.
[8414]91
[10296]92    """
[11730]93    date_format = '%d/%m/%Y'
[10296]94    payments = []
[11730]95    payments_start = kw.get('payments_start')
96    payments_end = kw.get('payments_end')
97    if payments_start and payments_end:
98        # Payment period given
99        payments_start = datetime.strptime(payments_start, date_format)
100        payments_end = datetime.strptime(payments_end, date_format)
[11757]101        tz = getUtility(IKofaUtils).tzinfo
102        payments_start = tz.localize(payments_start)
103        payments_end = tz.localize(payments_end)
[11730]104        if paid:
105            # Only paid tickets in payment period are considered
106            for student in students:
107                for payment in student.get('payments', {}).values():
[11757]108                    if payment.payment_date and payment.p_state == 'paid':
109                        payment_date = to_timezone(payment.payment_date, tz)
110                        if payment_date > payments_start and \
111                            payment_date < payments_end:
112                            payments.append(payment)
[11730]113        else:
114            # All tickets in payment period are considered
115            for student in students:
116                for payment in student.get('payments', {}).values():
[11757]117                    if payment.payment_date:
118                        payment_date = to_timezone(payment.payment_date, tz)
119                        if payment_date > payments_start and \
120                            payment_date < payments_end:
121                            payments.append(payment)
[11730]122    else:
123        # Payment period not given
124        if paid:
125            # Only paid tickets are considered
126            for student in students:
127                for payment in student.get('payments', {}).values():
128                    if payment.p_state == 'paid':
129                        payments.append(payment)
130        else:
131            # All tickets are considered
132            for student in students:
133                for payment in student.get('payments', {}).values():
134                    payments.append(payment)
[10296]135    return payments
136
[9427]137def get_bedtickets(students):
138    """Get all bedtickets of `students`.
139    """
140    tickets = []
141    for student in students:
142        for ticket in student.get('accommodation', {}).values():
143            tickets.append(ticket)
144    return tickets
[8414]145
146class StudentExporterBase(ExporterBase):
147    """Exporter for students or related objects.
148
149    This is a baseclass.
150    """
151    grok.baseclass()
[8411]152    grok.implements(ICSVStudentExporter)
153    grok.provides(ICSVStudentExporter)
[8414]154
[9844]155    def filter_func(self, x, **kw):
[9802]156        return x
[9797]157
[9801]158    def get_filtered(self, site, **kw):
[9843]159        """Get students from a catalog filtered by keywords.
[9801]160
[9843]161        students_catalog is the default catalog. The keys must be valid
[9933]162        catalog index names.
[9843]163        Returns a simple empty list, a list with `Student`
164        objects or a catalog result set with `Student`
[9801]165        objects.
166
167        .. seealso:: `waeup.kofa.students.catalog.StudentsCatalog`
168
169        """
[9843]170        # Pass only given keywords to create FilteredCatalogQuery objects.
171        # This way we avoid
[9801]172        # trouble with `None` value ambivalences and queries are also
173        # faster (normally less indexes to ask). Drawback is, that
174        # developers must look into catalog to see what keywords are
175        # valid.
[9845]176        if kw.get('catalog', None) == 'coursetickets':
[9843]177            coursetickets = CourseTicketsQuery(**kw).query()
178            students = []
179            for ticket in coursetickets:
180                students.append(ticket.student)
181            return list(set(students))
[11730]182        # Payments can be filtered by payment_date. The period boundaries
183        # are not keys of the catalog and must thus be removed from kw.
184        try:
185            del kw['payments_start']
186            del kw['payments_end']
187        except KeyError:
188            pass
[9801]189        query = StudentsQuery(**kw)
[9797]190        return query.query()
191
[12518]192    def get_selected(self, site, selected):
193        """Get set of selected students.
194
195        Returns a simple empty list or a list with `Student`
196        objects.
197        """
198        students = []
199        students_container = site.get('students', {})
200        for id in selected:
201            student = students_container.get(id, None)
202            if student:
203                students.append(student)
204        return students
205
[8414]206    def export(self, values, filepath=None):
207        """Export `values`, an iterable, as CSV file.
208
209        If `filepath` is ``None``, a raw string with CSV data is returned.
210        """
211        writer, outfile = self.get_csv_writer(filepath)
212        for value in values:
213            self.write_item(value, writer)
214        return self.close_outfile(filepath, outfile)
215
[9797]216    def export_all(self, site, filepath=None):
217        """Export students into filepath as CSV data.
218
219        If `filepath` is ``None``, a raw string with CSV data is returned.
[9763]220        """
[9802]221        return self.export(self.filter_func(get_students(site)), filepath)
[8414]222
[9797]223    def export_student(self, student, filepath=None):
224        return self.export(self.filter_func([student]), filepath=filepath)
[9734]225
[9802]226    def export_filtered(self, site, filepath=None, **kw):
227        """Export items denoted by `kw`.
[9797]228
[9802]229        If `filepath` is ``None``, a raw string with CSV data should
230        be returned.
231        """
232        data = self.get_filtered(site, **kw)
[9844]233        return self.export(self.filter_func(data, **kw), filepath=filepath)
[9802]234
[12518]235    def export_selected(self,site, filepath=None, **kw):
236        """Export data for selected set of students.
237        """
238        selected = kw.get('selected', [])
239        data = self.get_selected(site, selected)
240        return self.export(self.filter_func(data, **kw), filepath=filepath)
[9802]241
[12518]242
[12079]243class StudentExporter(grok.GlobalUtility, StudentExporterBase):
[8414]244    """Exporter for Students.
245    """
[7944]246    grok.name('students')
247
248    #: Fieldnames considered by this exporter
[9936]249    fields = tuple(sorted(iface_names(IStudent))) + (
[9253]250        'password', 'state', 'history', 'certcode', 'is_postgrad',
251        'current_level', 'current_session')
[7944]252
253    #: The title under which this exporter will be displayed
254    title = _(u'Students')
255
[8493]256    def mangle_value(self, value, name, context=None):
257        if name == 'history':
258            value = value.messages
[8971]259        if name == 'phone' and value is not None:
260            # Append hash '#' to phone numbers to circumvent
261            # unwanted excel automatic
[8947]262            value = str('%s#' % value)
[8493]263        return super(
[12079]264            StudentExporter, self).mangle_value(
[8493]265            value, name, context=context)
266
[7944]267
[8414]268class StudentStudyCourseExporter(grok.GlobalUtility, StudentExporterBase):
[7994]269    """Exporter for StudentStudyCourses.
270    """
271    grok.name('studentstudycourses')
272
273    #: Fieldnames considered by this exporter
[8493]274    fields = tuple(sorted(iface_names(IStudentStudyCourse))) + ('student_id',)
[7994]275
276    #: The title under which this exporter will be displayed
277    title = _(u'Student Study Courses')
278
[9844]279    def filter_func(self, x, **kw):
[9797]280        return get_studycourses(x)
281
[7994]282    def mangle_value(self, value, name, context=None):
[8493]283        """Treat location values special.
[7994]284        """
285        if name == 'certificate' and value is not None:
286            # XXX: hopefully cert codes are unique site-wide
287            value = value.code
[8493]288        if name == 'student_id' and context is not None:
[8736]289            student = context.student
[8493]290            value = getattr(student, name, None)
[7994]291        return super(
292            StudentStudyCourseExporter, self).mangle_value(
293            value, name, context=context)
294
295
[8414]296class StudentStudyLevelExporter(grok.GlobalUtility, StudentExporterBase):
[8015]297    """Exporter for StudentStudyLevels.
298    """
299    grok.name('studentstudylevels')
300
301    #: Fieldnames considered by this exporter
[8493]302    fields = tuple(sorted(iface_names(
[9253]303        IStudentStudyLevel) + ['level'])) + (
304        'student_id', 'number_of_tickets','certcode')
[8015]305
306    #: The title under which this exporter will be displayed
307    title = _(u'Student Study Levels')
308
[9844]309    def filter_func(self, x, **kw):
[9802]310        return get_levels(x)
311
[8015]312    def mangle_value(self, value, name, context=None):
[8493]313        """Treat location values special.
[8015]314        """
[8493]315        if name == 'student_id' and context is not None:
[8736]316            student = context.student
[8493]317            value = getattr(student, name, None)
[8015]318        return super(
319            StudentStudyLevelExporter, self).mangle_value(
320            value, name, context=context)
321
[8414]322class CourseTicketExporter(grok.GlobalUtility, StudentExporterBase):
[8342]323    """Exporter for CourseTickets.
324    """
325    grok.name('coursetickets')
326
327    #: Fieldnames considered by this exporter
[8493]328    fields = tuple(sorted(iface_names(ICourseTicket) +
[11484]329        ['level', 'code', 'level_session'])) + ('student_id',
330        'certcode', 'display_fullname')
[8342]331
332    #: The title under which this exporter will be displayed
333    title = _(u'Course Tickets')
334
[9844]335    def filter_func(self, x, **kw):
336        return get_tickets(x, **kw)
[9802]337
[8342]338    def mangle_value(self, value, name, context=None):
339        """Treat location values special.
340        """
341        if context is not None:
[8736]342            student = context.student
[11484]343            if name in ('student_id', 'display_fullname') and student is not None:
[8342]344                value = getattr(student, name, None)
[11484]345            #if name == 'level':
346            #    value = getattr(context, 'level', lambda: None)
347            #if name == 'level_session':
348            #    value = getattr(context, 'level_session', lambda: None)
[8342]349        return super(
350            CourseTicketExporter, self).mangle_value(
351            value, name, context=context)
352
353
[9832]354class StudentPaymentsExporter(grok.GlobalUtility, StudentExporterBase):
[8371]355    """Exporter for OnlinePayment instances.
356    """
357    grok.name('studentpayments')
358
359    #: Fieldnames considered by this exporter
360    fields = tuple(
[8493]361        sorted(iface_names(
[9984]362            IStudentOnlinePayment, exclude_attribs=False,
363            omit=['display_item']))) + (
[10232]364            'student_id','state','current_session')
[8371]365
366    #: The title under which this exporter will be displayed
[8576]367    title = _(u'Student Payments')
[8371]368
[9844]369    def filter_func(self, x, **kw):
[11730]370        return get_payments(x, **kw)
[9802]371
[8371]372    def mangle_value(self, value, name, context=None):
373        """Treat location values special.
374        """
375        if context is not None:
[8736]376            student = context.student
[10232]377            if name in ['student_id','state',
378                        'current_session'] and student is not None:
[8371]379                value = getattr(student, name, None)
380        return super(
[9832]381            StudentPaymentsExporter, self).mangle_value(
[8371]382            value, name, context=context)
383
[10233]384class DataForBursaryExporter(StudentPaymentsExporter):
385    """Exporter for OnlinePayment instances.
386    """
387    grok.name('bursary')
388
[10296]389    def filter_func(self, x, **kw):
[11730]390        return get_payments(x, paid=True, **kw)
[10296]391
[10233]392    #: Fieldnames considered by this exporter
393    fields = tuple(
394        sorted(iface_names(
395            IStudentOnlinePayment, exclude_attribs=False,
396            omit=['display_item']))) + (
[11702]397            'student_id','matric_number','reg_number',
[10236]398            'firstname', 'middlename', 'lastname',
399            'state','current_session',
400            'entry_session', 'entry_mode',
[11702]401            'faccode', 'depcode','certcode')
[10233]402
403    #: The title under which this exporter will be displayed
404    title = _(u'Payment Data for Bursary')
405
406    def mangle_value(self, value, name, context=None):
407        """Treat location values special.
408        """
409        if context is not None:
410            student = context.student
[10236]411            if name in [
[11702]412                'student_id','matric_number', 'reg_number',
[10236]413                'firstname', 'middlename', 'lastname',
[11702]414                'state', 'current_session',
[10236]415                'entry_session', 'entry_mode',
[11702]416                'faccode', 'depcode', 'certcode'] and student is not None:
[10233]417                value = getattr(student, name, None)
418        return super(
419            StudentPaymentsExporter, self).mangle_value(
420            value, name, context=context)
421
[9427]422class BedTicketsExporter(grok.GlobalUtility, StudentExporterBase):
423    """Exporter for BedTicket instances.
424    """
425    grok.name('bedtickets')
426
427    #: Fieldnames considered by this exporter
428    fields = tuple(
429        sorted(iface_names(
[9984]430            IBedTicket, exclude_attribs=False,
431            omit=['display_coordinates']))) + (
432            'student_id', 'actual_bed_type')
[9427]433
434    #: The title under which this exporter will be displayed
435    title = _(u'Bed Tickets')
436
[9844]437    def filter_func(self, x, **kw):
[9802]438        return get_bedtickets(x)
439
[9427]440    def mangle_value(self, value, name, context=None):
441        """Treat location values and others special.
442        """
443        if context is not None:
444            student = context.student
445            if name in ['student_id'] and student is not None:
446                value = getattr(student, name, None)
447        if name == 'bed' and value is not None:
448            value = getattr(value, 'bed_id', None)
449        if name == 'actual_bed_type':
450            value = getattr(getattr(context, 'bed', None), 'bed_type')
451        return super(
452            BedTicketsExporter, self).mangle_value(
453            value, name, context=context)
454
[12079]455class StudentPaymentsOverviewExporter(StudentExporter):
[9574]456    """Exporter for students with payment overview.
457    """
458    grok.name('paymentsoverview')
459
460    curr_year = datetime.now().year
461    year_range = range(curr_year - 9, curr_year + 1)
462    year_range_tuple = tuple([str(year) for year in year_range])
463
464    #: Fieldnames considered by this exporter
[9983]465    fields = ('student_id', 'matric_number', 'display_fullname',
[9574]466        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
[9807]467        'current_level', 'current_session', 'current_mode',
[9574]468        ) + year_range_tuple
469
470    #: The title under which this exporter will be displayed
471    title = _(u'Student Payments Overview')
472
473    def mangle_value(self, value, name, context=None):
474        if name in self.year_range_tuple and context is not None:
[11661]475            value = 0
[9574]476            for ticket in context['payments'].values():
[12568]477                if ticket.p_category == 'schoolfee' and \
[9574]478                    ticket.p_session == int(name):
[12568]479                    if ticket.p_state == 'waived':
480                        value = 'waived'
481                        break
482                    if ticket.p_state == 'paid':
483                        try:
484                            value += ticket.amount_auth
485                        except TypeError:
486                            pass
[11661]487            if value == 0:
488                value = ''
[9574]489        return super(
[12079]490            StudentExporter, self).mangle_value(
[9734]491            value, name, context=context)
[9744]492
[12079]493class StudentStudyLevelsOverviewExporter(StudentExporter):
[9744]494    """Exporter for students with study level overview.
495    """
496    grok.name('studylevelsoverview')
497
[9787]498    avail_levels = tuple([str(x) for x in study_levels(None)])
[9744]499
500    #: Fieldnames considered by this exporter
501    fields = ('student_id', ) + (
502        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
[9761]503        'entry_session', 'current_level', 'current_session',
[9787]504        ) + avail_levels
[9744]505
506    #: The title under which this exporter will be displayed
507    title = _(u'Student Study Levels Overview')
508
509    def mangle_value(self, value, name, context=None):
[9787]510        if name in self.avail_levels and context is not None:
[9744]511            value = ''
512            for level in context['studycourse'].values():
513                if level.level == int(name):
[9761]514                    #value = '%s|%s|%s|%s' % (
515                    #    level.level_session,
516                    #    len(level),
517                    #    level.validated_by,
518                    #    level.level_verdict)
519                    value = '%s' % level.level_session
[9744]520                    break
521        return super(
[12079]522            StudentExporter, self).mangle_value(
[9744]523            value, name, context=context)
[9936]524
525class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
526    """Exporter for Interswitch Combo Card Data.
527    """
528    grok.name('combocard')
529
530    #: Fieldnames considered by this exporter
531    fields = ('display_fullname',
[9937]532              'student_id','matric_number',
533              'certificate', 'faculty', 'department', 'passport_path')
[9936]534
535    #: The title under which this exporter will be displayed
536    title = _(u'Combo Card Data')
537
538    def mangle_value(self, value, name, context=None):
539        certificate = context['studycourse'].certificate
540        if name == 'certificate' and certificate is not None:
541            value = certificate.title
542        if name == 'department' and certificate is not None:
[10650]543            value = certificate.__parent__.__parent__.longtitle
[9936]544        if name == 'faculty' and certificate is not None:
[10650]545            value = certificate.__parent__.__parent__.__parent__.longtitle
[9937]546        if name == 'passport_path' and certificate is not None:
547            file_id = IFileStoreNameChooser(context).chooseName(attr='passport.jpg')
548            os_path = getUtility(IExtFileStore)._pathFromFileID(file_id)
549            if not os.path.exists(os_path):
550                value = None
551            else:
552                value = '/'.join(os_path.split('/')[-4:])
[9936]553        return super(
554            ComboCardDataExporter, self).mangle_value(
555            value, name, context=context)
Note: See TracBrowser for help on using the repository browser.