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

Last change on this file since 14539 was 14443, checked in by Henrik Bettermann, 8 years ago

Get set of selected students also from list of matric numbers.

  • Property svn:keywords set to Id
File size: 25.7 KB
Line 
1## $Id: export.py 14443 2017-01-23 15:58:02Z 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##
18"""Exporters for student related stuff.
19"""
20import os
21import grok
22from datetime import datetime, timedelta
23from zope.component import getUtility
24from waeup.kofa.interfaces import (
25    IExtFileStore, IFileStoreNameChooser, IKofaUtils)
26from waeup.kofa.interfaces import MessageFactory as _
27from waeup.kofa.students.catalog import StudentsQuery, CourseTicketsQuery
28from waeup.kofa.students.interfaces import (
29    IStudent, IStudentStudyCourse, IStudentStudyLevel, ICourseTicket,
30    IStudentOnlinePayment, ICSVStudentExporter, IBedTicket)
31from waeup.kofa.students.vocabularies import study_levels
32from waeup.kofa.utils.batching import ExporterBase
33from waeup.kofa.utils.helpers import iface_names, to_timezone
34
35
36def get_students(site, stud_filter=StudentsQuery()):
37    """Get all students registered in catalog in `site`.
38    """
39    return stud_filter.query()
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
56def get_tickets(students, **kw):
57    """Get course tickets of `students`.
58    If code is passed through, filter course tickets
59    which belong to this course code and meets level
60    and level_session.
61    """
62    tickets = []
63    code = kw.get('code', None)
64    level = kw.get('level', None)
65    level_session = kw.get('session', None)
66    if code is None:
67        for level_obj in get_levels(students):
68            for ticket in level_obj.values():
69                tickets.append(ticket)
70    else:
71        for level_obj in get_levels(students):
72            for ticket in level_obj.values():
73                if ticket.code != code:
74                    continue
75                if level is not None:
76                    level = int(level)
77                    if level_obj.level in (10, 999, None)  \
78                        and int(level) != level_obj.level:
79                        continue
80                    if level_obj.level not in range(level, level+100, 10):
81                        continue
82                if level_session is not None and \
83                    int(level_session) != level_obj.level_session:
84                    continue
85                tickets.append(ticket)
86    return tickets
87
88def get_tickets_for_lecturer(students, **kw):
89    """Get course tickets of `students`.
90    Filter course tickets which belong to this course code and
91    which are editable by lecturers.
92    """
93    tickets = []
94    code = kw.get('code', None)
95    level_session = kw.get('session', None)
96    level = kw.get('level', None)
97    for level_obj in get_levels(students):
98        for ticket in level_obj.values():
99            if ticket.code != code:
100                continue
101            if not ticket.editable_by_lecturer:
102                continue
103            if level is not None:
104                level = int(level)
105                if level_obj.level in (10, 999, None)  \
106                    and int(level) != level_obj.level:
107                    continue
108                if level_obj.level not in range(level, level+100, 10):
109                    continue
110            if level_session is not None and \
111                int(level_session) != level_obj.level_session:
112                continue
113            tickets.append(ticket)
114    return tickets
115
116def get_payments(students, p_state=None, **kw):
117    """Get all payments of `students` within given payment_date period.
118    """
119    date_format = '%d/%m/%Y'
120    payments = []
121    payments_start = kw.get('payments_start')
122    payments_end = kw.get('payments_end')
123    if payments_start and payments_end:
124        # Payment period given
125        payments_start = datetime.strptime(payments_start, date_format)
126        payments_end = datetime.strptime(payments_end, date_format)
127        tz = getUtility(IKofaUtils).tzinfo
128        payments_start = tz.localize(payments_start)
129        payments_end = tz.localize(payments_end) + timedelta(days=1)
130        if p_state:
131            # Only paid or unpaid tickets in payment period are considered
132            for student in students:
133                for payment in student.get('payments', {}).values():
134                    if payment.payment_date and payment.p_state == p_state:
135                        payment_date = to_timezone(payment.payment_date, tz)
136                        if payment_date > payments_start and \
137                            payment_date < payments_end:
138                            payments.append(payment)
139        else:
140            # All tickets in payment period are considered
141            for student in students:
142                for payment in student.get('payments', {}).values():
143                    if payment.payment_date:
144                        payment_date = to_timezone(payment.payment_date, tz)
145                        if payment_date > payments_start and \
146                            payment_date < payments_end:
147                            payments.append(payment)
148    else:
149        # Payment period not given
150        if p_state:
151            # Only paid tickets are considered
152            for student in students:
153                for payment in student.get('payments', {}).values():
154                    if payment.p_state == p_state:
155                        payments.append(payment)
156        else:
157            # All tickets are considered
158            for student in students:
159                for payment in student.get('payments', {}).values():
160                    payments.append(payment)
161    return payments
162
163def get_bedtickets(students):
164    """Get all bedtickets of `students`.
165    """
166    tickets = []
167    for student in students:
168        for ticket in student.get('accommodation', {}).values():
169            tickets.append(ticket)
170    return tickets
171
172class StudentExporterBase(ExporterBase):
173    """Exporter for students or related objects.
174    This is a baseclass.
175    """
176    grok.baseclass()
177    grok.implements(ICSVStudentExporter)
178    grok.provides(ICSVStudentExporter)
179
180    def filter_func(self, x, **kw):
181        return x
182
183    def get_filtered(self, site, **kw):
184        """Get students from a catalog filtered by keywords.
185        students_catalog is the default catalog. The keys must be valid
186        catalog index names.
187        Returns a simple empty list, a list with `Student`
188        objects or a catalog result set with `Student`
189        objects.
190
191        .. seealso:: `waeup.kofa.students.catalog.StudentsCatalog`
192
193        """
194        # Pass only given keywords to create FilteredCatalogQuery objects.
195        # This way we avoid
196        # trouble with `None` value ambivalences and queries are also
197        # faster (normally less indexes to ask). Drawback is, that
198        # developers must look into catalog to see what keywords are
199        # valid.
200        if kw.get('catalog', None) == 'coursetickets':
201            coursetickets = CourseTicketsQuery(**kw).query()
202            students = []
203            for ticket in coursetickets:
204                students.append(ticket.student)
205            return list(set(students))
206        # Payments can be filtered by payment_date. The period boundaries
207        # are not keys of the catalog and must thus be removed from kw.
208        try:
209            del kw['payments_start']
210            del kw['payments_end']
211        except KeyError:
212            pass
213        query = StudentsQuery(**kw)
214        return query.query()
215
216    def get_selected(self, site, selected):
217        """Get set of selected students.
218        Returns a simple empty list or a list with `Student`
219        objects.
220        """
221        students = []
222        students_container = site.get('students', {})
223        for id in selected:
224            student = students_container.get(id, None)
225            if student is None:
226                # try matric number
227                result = list(StudentsQuery(matric_number=id).query())
228                if result:
229                    student = result[0]
230                else:
231                    continue
232            students.append(student)
233        return students
234
235    def export(self, values, filepath=None):
236        """Export `values`, an iterable, as CSV file.
237        If `filepath` is ``None``, a raw string with CSV data is returned.
238        """
239        writer, outfile = self.get_csv_writer(filepath)
240        for value in values:
241            self.write_item(value, writer)
242        return self.close_outfile(filepath, outfile)
243
244    def export_all(self, site, filepath=None):
245        """Export students into filepath as CSV data.
246        If `filepath` is ``None``, a raw string with CSV data is returned.
247        """
248        return self.export(self.filter_func(get_students(site)), filepath)
249
250    def export_student(self, student, filepath=None):
251        return self.export(self.filter_func([student]), filepath=filepath)
252
253    def export_filtered(self, site, filepath=None, **kw):
254        """Export items denoted by `kw`.
255        If `filepath` is ``None``, a raw string with CSV data should
256        be returned.
257        """
258        data = self.get_filtered(site, **kw)
259        return self.export(self.filter_func(data, **kw), filepath=filepath)
260
261    def export_selected(self,site, filepath=None, **kw):
262        """Export data for selected set of students.
263        """
264        selected = kw.get('selected', [])
265        data = self.get_selected(site, selected)
266        return self.export(self.filter_func(data, **kw), filepath=filepath)
267
268
269class StudentExporter(grok.GlobalUtility, StudentExporterBase):
270    """The Student Exporter first filters the set of students by searching the
271    students catalog. Then it exports student base data of this set of students.
272    """
273    grok.name('students')
274
275    fields = tuple(sorted(iface_names(IStudent))) + (
276        'password', 'state', 'history', 'certcode', 'is_postgrad',
277        'current_level', 'current_session')
278    title = _(u'Students')
279
280    def mangle_value(self, value, name, context=None):
281        """The mangler prepares the history messages and adds a hash symbol at
282        the end of the phone number to avoid annoying automatic number
283        transformation by Excel or Calc."""
284        if name == 'history':
285            value = value.messages
286        if name == 'phone' and value is not None:
287            # Append hash '#' to phone numbers to circumvent
288            # unwanted excel automatic
289            value = str('%s#' % value)
290        return super(
291            StudentExporter, self).mangle_value(
292            value, name, context=context)
293
294
295class StudentStudyCourseExporter(grok.GlobalUtility, StudentExporterBase):
296    """The Student Study Course Exporter first filters the set of students
297    by searching the students catalog. Then it exports the data of the current
298    study course container of each student from this set. It does
299    not export their content.
300    """
301    grok.name('studentstudycourses')
302
303    fields = tuple(sorted(iface_names(IStudentStudyCourse))) + ('student_id',)
304    title = _(u'Student Study Courses')
305
306    def filter_func(self, x, **kw):
307        return get_studycourses(x)
308
309    def mangle_value(self, value, name, context=None):
310        """The mangler determines the certificate code and the student id.
311        """
312        if name == 'certificate' and value is not None:
313            # XXX: hopefully cert codes are unique site-wide
314            value = value.code
315        if name == 'student_id' and context is not None:
316            student = context.student
317            value = getattr(student, name, None)
318        return super(
319            StudentStudyCourseExporter, self).mangle_value(
320            value, name, context=context)
321
322
323class StudentStudyLevelExporter(grok.GlobalUtility, StudentExporterBase):
324    """The Student Study Level Exporter first filters the set of students
325    by searching the students catalog. Then it exports the data of the student's
326    study level container data but not their content (course tickets).
327    The exporter iterates over all objects in the students' ``studycourse``
328    containers.
329    """
330    grok.name('studentstudylevels')
331
332    fields = tuple(sorted(iface_names(
333        IStudentStudyLevel))) + (
334        'student_id', 'number_of_tickets','certcode')
335    title = _(u'Student Study Levels')
336
337    def filter_func(self, x, **kw):
338        return get_levels(x)
339
340    def mangle_value(self, value, name, context=None):
341        """The mangler determines the student id, nothing else.
342        """
343        if name == 'student_id' and context is not None:
344            student = context.student
345            value = getattr(student, name, None)
346        return super(
347            StudentStudyLevelExporter, self).mangle_value(
348            value, name, context=context)
349
350class CourseTicketExporter(grok.GlobalUtility, StudentExporterBase):
351    """The Course Ticket Exporter exports course tickets. Usually,
352    the exporter first filters the set of students by searching the
353    students catalog. Then it collects and iterates over all ``studylevel``
354    containers of the filtered student set and finally
355    iterates over all items inside these containers.
356
357    If the course code is passed through, the exporter uses a different
358    catalog. It searches for students in the course tickets catalog and
359    exports those course tickets which belong to the given course code and
360    also meet level and session passed through at the same time.
361    This happens if the exporter is called at course level in the academic
362    section.
363    """
364    grok.name('coursetickets')
365
366    fields = tuple(sorted(iface_names(ICourseTicket) +
367        ['level', 'code', 'level_session'])) + ('student_id',
368        'certcode', 'display_fullname')
369    title = _(u'Course Tickets')
370
371    def filter_func(self, x, **kw):
372        return get_tickets(x, **kw)
373
374    def mangle_value(self, value, name, context=None):
375        """The mangler determines the student's id and fullname.
376        """
377        if context is not None:
378            student = context.student
379            if name in ('student_id', 'display_fullname') and student is not None:
380                value = getattr(student, name, None)
381        return super(
382            CourseTicketExporter, self).mangle_value(
383            value, name, context=context)
384
385class DataForLecturerExporter(grok.GlobalUtility, StudentExporterBase):
386    """
387    """
388    grok.name('lecturer')
389
390    fields = ('matric_number', 'student_id', 'display_fullname',
391              'level', 'code', 'level_session', 'score')
392
393    title = _(u'Data for Lecturer')
394
395    def filter_func(self, x, **kw):
396        return get_tickets_for_lecturer(x, **kw)
397
398    def mangle_value(self, value, name, context=None):
399        """The mangler determines the student's id and fullname.
400        """
401        if context is not None:
402            student = context.student
403            if name in ('matric_number',
404                        'reg_number',
405                        'student_id',
406                        'display_fullname',) and student is not None:
407                value = getattr(student, name, None)
408        return super(
409            DataForLecturerExporter, self).mangle_value(
410            value, name, context=context)
411
412class StudentPaymentExporter(grok.GlobalUtility, StudentExporterBase):
413    """The Student Payment Exporter first filters the set of students
414    by searching the students catalog. Then it exports student payment
415    tickets by iterating over the items of the student's ``payments``
416    container. If the payment period is given only tickets, which were
417    paid in payment period, are considered for export.
418    """
419    grok.name('studentpayments')
420
421    fields = tuple(
422        sorted(iface_names(
423            IStudentOnlinePayment, exclude_attribs=False,
424            omit=['display_item', 'certificate', 'student']))) + (
425            'student_id','state','current_session')
426    title = _(u'Student Payments')
427
428    def filter_func(self, x, **kw):
429        return get_payments(x, **kw)
430
431    def mangle_value(self, value, name, context=None):
432        """The mangler determines the student's id, registration
433        state and current session.
434        """
435        if context is not None:
436            student = context.student
437            if name in ['student_id','state',
438                        'current_session'] and student is not None:
439                value = getattr(student, name, None)
440        return super(
441            StudentPaymentExporter, self).mangle_value(
442            value, name, context=context)
443
444class StudentUnpaidPaymentExporter(StudentPaymentExporter):
445    """The Student Unpaid Payment Exporter works just like the
446    Student Payments Exporter but it exports only unpaid tickets.
447    This exporter is designed for finding and finally purging outdated
448    payment ticket.
449    """
450    grok.name('studentunpaidpayments')
451
452    title = _(u'Student Unpaid Payments')
453
454    def filter_func(self, x, **kw):
455        return get_payments(x, p_state='unpaid', **kw)
456
457class DataForBursaryExporter(StudentPaymentExporter):
458    """The DataForBursary Exporter works just like the Student Payments Exporter
459    but it exports much more information about the student. It combines
460    payment and student data in one table in order to spare postprocessing of
461    two seperate export files. The exporter is primarily used by bursary
462    officers who have exclusively access to this exporter.
463    """
464    grok.name('bursary')
465
466    def filter_func(self, x, **kw):
467        return get_payments(x, p_state='paid', **kw)
468
469    fields = tuple(
470        sorted(iface_names(
471            IStudentOnlinePayment, exclude_attribs=False,
472            omit=['display_item', 'certificate', 'student']))) + (
473            'student_id','matric_number','reg_number',
474            'firstname', 'middlename', 'lastname',
475            'state','current_session',
476            'entry_session', 'entry_mode',
477            'faccode', 'depcode','certcode')
478    title = _(u'Payment Data for Bursary')
479
480    def mangle_value(self, value, name, context=None):
481        """The mangler fetches the student data.
482        """
483        if context is not None:
484            student = context.student
485            if name in [
486                'student_id','matric_number', 'reg_number',
487                'firstname', 'middlename', 'lastname',
488                'state', 'current_session',
489                'entry_session', 'entry_mode',
490                'faccode', 'depcode', 'certcode'] and student is not None:
491                value = getattr(student, name, None)
492        return super(
493            StudentPaymentExporter, self).mangle_value(
494            value, name, context=context)
495
496class BedTicketExporter(grok.GlobalUtility, StudentExporterBase):
497    """The Bed Ticket Exporter first filters the set of students
498    by searching the students catalog. Then it exports bed
499    tickets by iterating over the items of the student's ``accommodation``
500    container.
501    """
502    grok.name('bedtickets')
503
504    fields = tuple(
505        sorted(iface_names(
506            IBedTicket, exclude_attribs=False,
507            omit=['display_coordinates', 'maint_payment_made']))) + (
508            'student_id', 'actual_bed_type')
509    title = _(u'Bed Tickets')
510
511    def filter_func(self, x, **kw):
512        return get_bedtickets(x)
513
514    def mangle_value(self, value, name, context=None):
515        """The mangler determines the student id and the type of the bed
516        which has been booked in the ticket.
517        """
518        if context is not None:
519            student = context.student
520            if name in ['student_id'] and student is not None:
521                value = getattr(student, name, None)
522        if name == 'bed' and value is not None:
523            value = getattr(value, 'bed_id', None)
524        if name == 'actual_bed_type':
525            value = getattr(getattr(context, 'bed', None), 'bed_type', None)
526        return super(
527            BedTicketExporter, self).mangle_value(
528            value, name, context=context)
529
530class StudentPaymentsOverviewExporter(StudentExporter):
531    """The Student Payments Overview Exporter first filters the set of students
532    by searching the students catalog. Then it exports some student base data
533    together with the total school fee amount paid in each year over a
534    predefined year range (current year - 9, ... , current year + 1).
535    """
536    grok.name('paymentsoverview')
537
538    curr_year = datetime.now().year
539    year_range = range(curr_year - 10, curr_year + 1)
540    year_range_tuple = tuple([str(year) for year in year_range])
541
542    fields = ('student_id', 'matric_number', 'display_fullname',
543        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
544        'current_level', 'current_session', 'current_mode',
545        'entry_session', 'reg_number'
546        ) + year_range_tuple
547    title = _(u'Student Payments Overview')
548
549    def mangle_value(self, value, name, context=None):
550        """The mangler summarizes the school fee amounts made per year. It
551        iterates over all paid school fee payment tickets and
552        adds together the amounts paid in a year. Waived payments
553        are marked ``waived``.
554        """
555        if name in self.year_range_tuple and context is not None:
556            value = 0
557            for ticket in context['payments'].values():
558                if ticket.p_category == 'schoolfee' and \
559                    ticket.p_session == int(name):
560                    if ticket.p_state == 'waived':
561                        value = 'waived'
562                        break
563                    if ticket.p_state == 'paid':
564                        try:
565                            value += ticket.amount_auth
566                        except TypeError:
567                            pass
568            if value == 0:
569                value = ''
570            elif isinstance(value, float):
571                value = round(value, 2)
572        return super(
573            StudentExporter, self).mangle_value(
574            value, name, context=context)
575
576class StudentStudyLevelsOverviewExporter(StudentExporter):
577    """The Student Study Levels Overview Exporter first filters the set of
578    students by searching the students catalog. Then it exports some student
579    base data together with the session key of registered levels.
580    Sample output:
581
582    header: ``...100,110,120,200,210,220,300...``
583
584    data: ``...2010,,,2011,2012,,2013...``
585
586    This csv data string means that level 100 was registered in session
587    2010/2011, level 200 in session 2011/2012, level 210 (200 on 1st probation)
588    in session 2012/2013 and level 300 in session 2013/2014.
589    """
590    grok.name('studylevelsoverview')
591
592    avail_levels = tuple([str(x) for x in study_levels(None)])
593
594    fields = ('student_id', ) + (
595        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
596        'entry_session', 'current_level', 'current_session',
597        ) + avail_levels
598    title = _(u'Student Study Levels Overview')
599
600    def mangle_value(self, value, name, context=None):
601        """The mangler checks if a given level has been registered. It returns
602        the ``level_session`` attribute of the student study level object
603        if the named level exists.
604        """
605        if name in self.avail_levels and context is not None:
606            value = ''
607            for level in context['studycourse'].values():
608                if level.level == int(name):
609                    value = '%s' % level.level_session
610                    break
611        return super(
612            StudentExporter, self).mangle_value(
613            value, name, context=context)
614
615class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
616    """Like all other exporters the Combo Card Data Exporter first filters the
617    set of students by searching the students catalog. Then it exports some
618    student base data which are neccessary to print for the Interswitch combo
619    card (identity card for students). The output contains a ``passport_path``
620    column which contains the filesystem path of the passport image file.
621    If no path is given, no passport image file exists.
622    """
623    grok.name('combocard')
624
625    fields = ('display_fullname',
626              'student_id','matric_number',
627              'certificate', 'faculty', 'department', 'passport_path')
628    title = _(u'Combo Card Data')
629
630    def mangle_value(self, value, name, context=None):
631        """The mangler determines the titles of faculty, department
632        and certificate. It also computes the path of passport image file
633        stored in the filesystem.
634        """
635        certificate = context['studycourse'].certificate
636        if name == 'certificate' and certificate is not None:
637            value = certificate.title
638        if name == 'department' and certificate is not None:
639            value = certificate.__parent__.__parent__.longtitle
640        if name == 'faculty' and certificate is not None:
641            value = certificate.__parent__.__parent__.__parent__.longtitle
642        if name == 'passport_path' and certificate is not None:
643            file_id = IFileStoreNameChooser(context).chooseName(
644                attr='passport.jpg')
645            os_path = getUtility(IExtFileStore)._pathFromFileID(file_id)
646            if not os.path.exists(os_path):
647                value = None
648            else:
649                value = '/'.join(os_path.split('/')[-4:])
650        return super(
651            ComboCardDataExporter, self).mangle_value(
652            value, name, context=context)
Note: See TracBrowser for help on using the repository browser.