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

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

Add exporter methods and export page to filter student data exports by entering a list of student ids.

  • Property svn:keywords set to Id
File size: 20.1 KB
RevLine 
[8057]1## $Id: export.py 12518 2015-01-27 14:48:22Z 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():
477                if ticket.p_state == 'paid' and \
478                    ticket.p_category == 'schoolfee' and \
479                    ticket.p_session == int(name):
[11662]480                    try:
481                        value += ticket.amount_auth
482                    except TypeError:
483                        pass
[11661]484            if value == 0:
485                value = ''
[9574]486        return super(
[12079]487            StudentExporter, self).mangle_value(
[9734]488            value, name, context=context)
[9744]489
[12079]490class StudentStudyLevelsOverviewExporter(StudentExporter):
[9744]491    """Exporter for students with study level overview.
492    """
493    grok.name('studylevelsoverview')
494
[9787]495    avail_levels = tuple([str(x) for x in study_levels(None)])
[9744]496
497    #: Fieldnames considered by this exporter
498    fields = ('student_id', ) + (
499        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
[9761]500        'entry_session', 'current_level', 'current_session',
[9787]501        ) + avail_levels
[9744]502
503    #: The title under which this exporter will be displayed
504    title = _(u'Student Study Levels Overview')
505
506    def mangle_value(self, value, name, context=None):
[9787]507        if name in self.avail_levels and context is not None:
[9744]508            value = ''
509            for level in context['studycourse'].values():
510                if level.level == int(name):
[9761]511                    #value = '%s|%s|%s|%s' % (
512                    #    level.level_session,
513                    #    len(level),
514                    #    level.validated_by,
515                    #    level.level_verdict)
516                    value = '%s' % level.level_session
[9744]517                    break
518        return super(
[12079]519            StudentExporter, self).mangle_value(
[9744]520            value, name, context=context)
[9936]521
522class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
523    """Exporter for Interswitch Combo Card Data.
524    """
525    grok.name('combocard')
526
527    #: Fieldnames considered by this exporter
528    fields = ('display_fullname',
[9937]529              'student_id','matric_number',
530              'certificate', 'faculty', 'department', 'passport_path')
[9936]531
532    #: The title under which this exporter will be displayed
533    title = _(u'Combo Card Data')
534
535    def mangle_value(self, value, name, context=None):
536        certificate = context['studycourse'].certificate
537        if name == 'certificate' and certificate is not None:
538            value = certificate.title
539        if name == 'department' and certificate is not None:
[10650]540            value = certificate.__parent__.__parent__.longtitle
[9936]541        if name == 'faculty' and certificate is not None:
[10650]542            value = certificate.__parent__.__parent__.__parent__.longtitle
[9937]543        if name == 'passport_path' and certificate is not None:
544            file_id = IFileStoreNameChooser(context).chooseName(attr='passport.jpg')
545            os_path = getUtility(IExtFileStore)._pathFromFileID(file_id)
546            if not os.path.exists(os_path):
547                value = None
548            else:
549                value = '/'.join(os_path.split('/')[-4:])
[9936]550        return super(
551            ComboCardDataExporter, self).mangle_value(
552            value, name, context=context)
Note: See TracBrowser for help on using the repository browser.