source: main/waeup.aaue/trunk/src/waeup/aaue/students/utils.py @ 18138

Last change on this file since 18138 was 18138, checked in by Henrik Bettermann, 34 hours ago

Add 25 school fees.
Fix tests.
Change format of CGPA.

  • Property svn:keywords set to Id
File size: 27.5 KB
Line 
1## $Id: utils.py 18138 2025-07-22 12:00:44Z 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##
18import grok
19import os
20import csv
21from time import time
22from zope.component import createObject, queryUtility
23from zope.catalog.interfaces import ICatalog
24from waeup.kofa.interfaces import (
25    ADMITTED, CLEARANCE, REQUESTED, CLEARED, RETURNING, PAID,
26    REGISTERED, VALIDATED, academic_sessions_vocab)
27from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
28from waeup.kofa.accesscodes import create_accesscode
29from waeup.kofa.students.utils import trans
30from waeup.aaue.interfaces import MessageFactory as _
31
32MINIMUM_UNITS_THRESHOLD = 15
33
34SCHOOLFEES = dict()
35SFEECHANGES = (12, 13, 14, 15, 20, 21, 22, 23, 24, 25)
36
37for year in SFEECHANGES:
38    schoolfees_path = os.path.join(
39        os.path.dirname(__file__), 'schoolfees_%s.csv' %year)
40    reader = csv.DictReader(open(schoolfees_path, 'rb'))
41    SCHOOLFEES[year] = {item['code']:(item['tuition'], item.values()) for item in reader}
42
43acceptancefees_path = os.path.join(
44    os.path.dirname(__file__), 'acceptancefees.csv')
45reader = csv.DictReader(open(acceptancefees_path, 'rb'))
46ACCEPTANCEFEES = {item['code']:(item['acceptance'], item.values()) for item in reader}
47
48class CustomStudentsUtils(NigeriaStudentsUtils):
49    """A collection of customized methods.
50
51    """
52
53    # PORTRAIT_CHANGE_STATES = (ADMITTED, CLEARANCE, REQUESTED, CLEARED)
54
55    def allowPortraitChange(self, student):
56        #if student.is_fresh:
57        #    return True
58        return False
59
60    def GPABoundaries(self, faccode=None, depcode=None,
61                            certcode=None, student=None):
62        if student and student.current_mode.startswith('dp'):
63            return ((1.5, 'IRNS / NER / NYV'),
64                    (2.4, 'Pass'),
65                    (3.5, 'Merit'),
66                    (4.5, 'Credit'),
67                    (5, 'Distinction'))
68        elif student:
69            return ((1, 'FRNS / NER / NYV'),
70                    (1.5, 'Pass'),
71                    (2.4, '3rd Class Honours'),
72                    (3.5, '2nd Class Honours Lower Division'),
73                    (4.5, '2nd Class Honours Upper Division'),
74                    (5, '1st Class Honours'))
75        # Session Results Presentations depend on certificate
76        results = None
77        if certcode:
78            cat = queryUtility(ICatalog, name='certificates_catalog')
79            results = list(
80                cat.searchResults(code=(certcode, certcode)))
81        if results and results[0].study_mode.startswith('dp'):
82            return ((1.5, 'IRNS / NER / NYV'),
83                    (2.4, 'Pass'),
84                    (3.5, 'Merit'),
85                    (4.5, 'Credit'),
86                    (5, 'Distinction'))
87        else:
88            return ((1, 'FRNS / NER / NYV'),
89                    (1.5, 'Pass'),
90                    (2.4, '3rd Class Honours'),
91                    (3.5, '2nd Class Honours Lower Division'),
92                    (4.5, '2nd Class Honours Upper Division'),
93                    (5, '1st Class Honours'))
94
95    def getClassFromCGPA(self, gpa, student):
96        gpa_boundaries = self.GPABoundaries(student=student)
97
98        try:
99            certificate = getattr(student['studycourse'],'certificate',None)
100            end_level = getattr(certificate, 'end_level', None)
101            final_level = max([studylevel.level for studylevel in student['studycourse'].values()])
102            if end_level and final_level >= end_level:
103                dummy, repeat = divmod(final_level, 100)
104                if gpa <= 5.1 and repeat == 20:
105                    # Irrespective of the CGPA of a student, if the He/She has
106                    # 3rd Extension, such student will be graduated with a "Pass".
107                    return 1, gpa_boundaries[1][1]
108        except ValueError:
109            pass
110
111        if gpa < gpa_boundaries[0][0]:
112            # FRNS / Fail
113            return 0, gpa_boundaries[0][1]
114        if student.entry_session < 2013 or \
115            student.current_mode.startswith('dp'):
116            if gpa < gpa_boundaries[1][0]:
117                # Pass
118                return 1, gpa_boundaries[1][1]
119        else:
120            if gpa < gpa_boundaries[1][0]:
121                # FRNS
122                # Pass degree has been phased out in 2013 for non-diploma
123                # students
124                return 0, gpa_boundaries[0][1]
125        if gpa < gpa_boundaries[2][0]:
126            # 3rd / Merit
127            return 2, gpa_boundaries[2][1]
128        if gpa < gpa_boundaries[3][0]:
129            # 2nd L / Credit
130            return 3, gpa_boundaries[3][1]
131        if gpa < gpa_boundaries[4][0]:
132            # 2nd U / Distinction
133            return 4, gpa_boundaries[4][1]
134        if gpa <= gpa_boundaries[5][0]:
135            # 1st
136            return 5, gpa_boundaries[5][1]
137        return
138
139    def getDegreeClassNumber(self, level_obj):
140        """Get degree class number (used for SessionResultsPresentation
141        reports).
142        """
143        certificate = getattr(level_obj.__parent__,'certificate', None)
144        end_level = getattr(certificate, 'end_level', None)
145        if level_obj.level_verdict in ('FRNS', 'NER', 'NYV'):
146            return 0
147        if end_level and level_obj.level >= end_level:
148            if level_obj.level > end_level:
149                # spill-over level
150                if level_obj.gpa_params[1] == 0:
151                    # no credits taken
152                    return 0
153            elif level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
154                # credits taken below limit
155                return 0
156            failed_courses = level_obj.passed_params[4]
157            not_taken_courses = level_obj.passed_params[5]
158            if '_m' in failed_courses:
159                return 0
160            if len(not_taken_courses) \
161                and not not_taken_courses == 'Nil':
162                return 0
163        elif level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
164            # credits taken below limit
165            return 0
166        # use gpa_boundaries above
167        return self.getClassFromCGPA(
168            level_obj.cumulative_params[0], level_obj.student)[0]
169
170    def increaseMatricInteger(self, student):
171        """Increase counter for matric numbers.
172        This counter can be a centrally stored attribute or an attribute of
173        faculties, departments or certificates. In the base package the counter
174        is as an attribute of the site configuration container.
175        """
176        if student.current_mode in ('ug_pt', 'de_pt', 'ug_dsh', 'de_dsh'):
177            grok.getSite()['configuration'].next_matric_integer += 1
178            return
179        elif student.is_postgrad:
180            grok.getSite()['configuration'].next_matric_integer_3 += 1
181            return
182        elif student.current_mode in ('dp_ft',):
183            grok.getSite()['configuration'].next_matric_integer_4 += 1
184            return
185        grok.getSite()['configuration'].next_matric_integer_2 += 1
186        return
187
188    def _concessionalPaymentMade(self, student):
189        if len(student['payments']):
190            for ticket in student['payments'].values():
191                if ticket.p_state == 'paid' and \
192                    ticket.p_category == 'concessional':
193                    return True
194        return False
195
196    def constructMatricNumber(self, student):
197        faccode = student.faccode
198        depcode = student.depcode
199        certcode = student.certcode
200        degree = getattr(
201            getattr(student.get('studycourse', None), 'certificate', None),
202                'degree', None)
203        year = unicode(student.entry_session)[2:]
204        if not student.state in (PAID, ) or not student.is_fresh or \
205            student.current_mode in ('found', 'ijmbe'):
206            return _('Matriculation number cannot be set.'), None
207        #if student.current_mode not in ('mug_ft', 'mde_ft') and \
208        #    not self._concessionalPaymentMade(student):
209        #    return _('Matriculation number cannot be set.'), None
210        if student.is_postgrad:
211            next_integer = grok.getSite()['configuration'].next_matric_integer_3
212            if not degree or next_integer == 0:
213                return _('Matriculation number cannot be set.'), None
214            if student.faccode in ('IOE'):
215                return None, "AAU/SPS/%s/%s/%s/%05d" % (
216                    faccode, year, degree, next_integer)
217            return None, "AAU/SPS/%s/%s/%s/%s/%05d" % (
218                faccode, depcode, year, degree, next_integer)
219        if student.current_mode in ('ug_dsh', 'de_dsh'):
220            next_integer = grok.getSite()['configuration'].next_matric_integer
221            if next_integer == 0:
222                return _('Matriculation number cannot be set.'), None
223            return None, "DSH/%s/%s/%s/%05d" % (
224                faccode, depcode, year, next_integer)
225        if student.current_mode in ('ug_pt', 'de_pt'):
226            next_integer = grok.getSite()['configuration'].next_matric_integer
227            if next_integer == 0:
228                return _('Matriculation number cannot be set.'), None
229            return None, "PTP/%s/%s/%s/%05d" % (
230                faccode, depcode, year, next_integer)
231        if student.current_mode in ('dp_ft',):
232            next_integer = grok.getSite()['configuration'].next_matric_integer_4
233            if next_integer == 0:
234                return _('Matriculation number cannot be set.'), None
235            return None, "IOE/DIP/%s/%05d" % (year, next_integer)
236        next_integer = grok.getSite()['configuration'].next_matric_integer_2
237        if next_integer == 0:
238            return _('Matriculation number cannot be set.'), None
239        if student.faccode in ('FBM', 'FCS', 'FMLS'):
240            return None, "CMS/%s/%s/%s/%05d" % (
241                faccode, depcode, year, next_integer)
242        return None, "%s/%s/%s/%05d" % (faccode, depcode, year, next_integer)
243
244    def getReturningData(self, student):
245        """ This method defines what happens after school fee payment
246        of returning students depending on the student's senate verdict.
247        """
248        prev_level = student['studycourse'].current_level
249        cur_verdict = student['studycourse'].current_verdict
250        if cur_verdict in ('A','B','L','M','N','Z',):
251            # Successful student
252            new_level = divmod(int(prev_level),100)[0]*100 + 100
253        elif cur_verdict == 'C':
254            # Student on probation
255            new_level = int(prev_level) + 10
256        else:
257            # Student is somehow in an undefined state.
258            # Level has to be set manually.
259            new_level = prev_level
260        new_session = student['studycourse'].current_session + 1
261        return new_session, new_level
262
263    def _isPaymentDisabled(self, p_session, category, student):
264        academic_session = self._getSessionConfiguration(p_session)
265        if category.startswith('schoolfee'):
266            if 'all_returning' in academic_session.payment_disabled and \
267            student.state == RETURNING:
268                return True
269            if 'sf_all' in academic_session.payment_disabled:
270                return True
271            if 'sf_pg' in academic_session.payment_disabled and \
272                student.is_postgrad:
273                return True
274            if 'sf_ug_pt' in academic_session.payment_disabled and \
275                student.current_mode in ('ug_pt', 'de_pt'):
276                return True
277            if 'sf_found' in academic_session.payment_disabled and \
278                student.current_mode == 'found':
279                return True
280        if category.startswith('clearance') and \
281            'cl_regular' in academic_session.payment_disabled and \
282            student.current_mode in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
283            return True
284        if category == 'hostel_maintenance' and \
285            'maint_all' in academic_session.payment_disabled:
286            return True
287        return False
288
289    def setPaymentDetails(self, category, student,
290            previous_session=None, previous_level=None, combi=[]):
291        """Create Payment object and set the payment data of a student for
292        the payment category specified.
293
294        """
295        details = {}
296        p_item = u''
297        amount = 0.0
298        error = u''
299        if previous_session:
300            if previous_session < student['studycourse'].entry_session:
301                return _('The previous session must not fall below '
302                         'your entry session.'), None
303            if category == 'schoolfee':
304                # School fee is always paid for the following session
305                if previous_session > student['studycourse'].current_session:
306                    return _('This is not a previous session.'), None
307            else:
308                if previous_session > student['studycourse'].current_session - 1:
309                    return _('This is not a previous session.'), None
310            p_session = previous_session
311            p_level = previous_level
312            p_current = False
313        else:
314            p_session = student['studycourse'].current_session
315            p_level = student['studycourse'].current_level
316            p_current = True
317        academic_session = self._getSessionConfiguration(p_session)
318        if academic_session == None:
319            return _(u'Session configuration object is not available.'), None
320        # Determine fee.
321        if category == 'transfer':
322            amount = academic_session.transfer_fee
323        elif category == 'transcript_local':
324            amount = academic_session.transcript_fee_local
325        elif category == 'transcript_inter':
326            amount = academic_session.transcript_fee_inter
327        elif category == 'bed_allocation':
328            acco_details = self.getAccommodationDetails(student)
329            p_session = acco_details['booking_session']
330            p_item = acco_details['bt']
331            amount = academic_session.booking_fee
332        elif category == 'hostel_maintenance':
333            amount = 0.0
334            booking_session = grok.getSite()['hostels'].accommodation_session
335            bedticket = student['accommodation'].get(str(booking_session), None)
336            if bedticket is not None and bedticket.bed is not None:
337                p_session = booking_session
338                p_item = bedticket.display_coordinates
339                if bedticket.bed.__parent__.maint_fee > 0:
340                    amount = bedticket.bed.__parent__.maint_fee
341                else:
342                    # fallback
343                    amount = academic_session.maint_fee
344            else:
345                return _(u'No bed allocated.'), None
346        elif student.current_mode == 'found' and category not in (
347            'schoolfee', 'clearance', 'late_registration'):
348            return _('Not allowed.'), None
349        elif category.startswith('clearance'):
350            if student.state not in (ADMITTED, CLEARANCE, REQUESTED, CLEARED):
351                return _(u'Acceptance Fee payments not allowed.'), None
352            try:
353                certificate = student['studycourse'].certificate
354                p_item = certificate.code
355            except (AttributeError, TypeError):
356                return _('Study course data are incomplete.'), None
357            if student.current_mode in (
358                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
359                'transfer', 'mug_ft', 'mde_ft') \
360                and category != 'clearance_incl':
361                    return _("Additional fees must be included."), None
362            elif student.current_mode == 'special_pg_ft':
363                if category != 'clearance':
364                    return _("No additional fees required."), None
365            try:
366                acceptancefees = ACCEPTANCEFEES[student.certcode]
367            except KeyError:
368                return _('Acceptance fees not yet fixed.'), None
369            if category == 'clearance_incl':
370                for item in acceptancefees[1]:
371                    try:
372                        amount += int(item)
373                    except:
374                        pass
375            else:
376                amount = float(acceptancefees[0])
377        elif category == 'late_registration':
378            if student.is_postgrad:
379                amount = academic_session.late_pg_registration_fee
380            else:
381                amount = academic_session.late_registration_fee
382        elif category == 'ict':
383            if student.is_fresh:
384                amount = 2200
385            else:
386                amount = 1200
387        elif category.startswith('schoolfee'):
388            try:
389                certificate = student['studycourse'].certificate
390                p_item = certificate.code
391            except (AttributeError, TypeError):
392                return _('Study course data are incomplete.'), None
393            try:
394                if student.entry_session < 2013:
395                    schoolfees = SCHOOLFEES[12][student.certcode]
396                elif student.entry_session < 2014:
397                    schoolfees = SCHOOLFEES[13][student.certcode]
398                elif student.entry_session < 2015:
399                    schoolfees = SCHOOLFEES[14][student.certcode]
400                elif student.entry_session < 2020:
401                    schoolfees = SCHOOLFEES[15][student.certcode]
402                elif student.entry_session < 2021:
403                    schoolfees = SCHOOLFEES[20][student.certcode]
404                elif student.entry_session < 2022:
405                    schoolfees = SCHOOLFEES[21][student.certcode]
406                elif student.entry_session < 2023:
407                    schoolfees = SCHOOLFEES[22][student.certcode]
408                elif student.entry_session < 2024:
409                    schoolfees = SCHOOLFEES[23][student.certcode]
410                elif student.entry_session < 2025:
411                    schoolfees = SCHOOLFEES[24][student.certcode]
412                else:
413                    schoolfees = SCHOOLFEES[25][student.certcode]
414            except KeyError:
415                return _('School fees not yet fixed.'), None
416            #if student.is_postgrad and category != 'schoolfee':
417            #    return _("No additional fees required."), None
418            if not previous_session and student.current_mode in (
419                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
420                'transfer', 'mug_ft', 'mde_ft') \
421                and not category in (
422                'schoolfee_incl', 'schoolfee_1', 'schoolfee_2'):
423                    return _("You must choose a payment which includes "
424                             "additional fees."), None
425            if category in ('schoolfee_1', 'schoolfee_2'):
426                #if student.current_mode == 'ug_pt':
427                #    return _("Part-time students are not allowed "
428                #             "to pay by instalments."), None
429                if student.entry_session < 2015:
430                    return _("You are not allowed "
431                             "to pay by instalments."), None
432            additional = 0.0
433            for item in schoolfees[1]:
434                try:
435                    additional += int(item)
436                except:
437                    # 100_ indicates first year payment only
438                    if student.state == CLEARED:
439                        try:
440                            additional += int(item.split('_')[1])
441                        except:
442                            pass
443            amount = float(schoolfees[0])
444            additional -= amount
445            if previous_session:
446                # Students can pay for previous sessions in all
447                # workflow states.  Fresh students are excluded by the
448                # update method of the PreviousPaymentAddFormPage.
449               # Cut school fee by 50%
450                if category in ('schoolfee_1', 'schoolfee_2') and amount:
451                    amount /= 2
452                pass
453            elif student.state == CLEARED:
454                # Cut school fee by 50%
455                if category in ('schoolfee_1', 'schoolfee_2') and amount:
456                    amount /= 2
457            elif student.state == RETURNING and category != 'schoolfee_2':
458                if not student.father_name:
459                    return _("Personal data form is not properly filled."), None
460                # In case of returning school fee payment the payment session
461                # and level contain the values of the session the student
462                # has paid for.
463                p_session, p_level = self.getReturningData(student)
464                try:
465                    academic_session = grok.getSite()[
466                        'configuration'][str(p_session)]
467                except KeyError:
468                    return _(u'Session configuration object is not available.'), None
469                # Cut school fee by 50%
470                if category == 'schoolfee_1' and amount:
471                    amount /= 2
472            elif category == 'schoolfee_2' and amount:
473                amount /= 2
474            else:
475                return _('Wrong state.'), None
476            if amount in (0.0, None):
477                return _(u'Amount could not be determined.'), None
478            # Add additional fees
479            if category in ('schoolfee_incl', 'schoolfee_1'):
480                amount += additional
481            # Add non-indigenous fee and session specific penalty fees
482            if student.is_postgrad:
483                amount += academic_session.penalty_pg
484                if student.lga and not student.lga.startswith('edo') \
485                    and student.entry_session < 2022:
486                    amount += 20000.0
487            else:
488                amount += academic_session.penalty_ug
489        elif not student.is_postgrad:
490            fee_name = category + '_fee'
491            amount = getattr(academic_session, fee_name, 0.0)
492        if amount in (0.0, None):
493            return _(u'Amount could not be determined.'), None
494        # Create ticket.
495        for key in student['payments'].keys():
496            ticket = student['payments'][key]
497            if ticket.p_state == 'paid' and\
498               ticket.p_category == category and \
499               not ticket.p_category.startswith('transcript') and \
500               ticket.p_item == p_item and \
501               ticket.p_session == p_session:
502                  return _('This type of payment has already been made.'), None
503            # Additional condition in AAUE
504            if category in ('schoolfee', 'schoolfee_incl', 'schoolfee_1'):
505                if ticket.p_state == 'paid' and \
506                   ticket.p_category in ('schoolfee',
507                                         'schoolfee_incl',
508                                         'schoolfee_1') and \
509                   ticket.p_item == p_item and \
510                   ticket.p_session == p_session:
511                      return _(
512                          'Another school fee payment for this '
513                          'session has already been made.'), None
514
515        if self._isPaymentDisabled(p_session, category, student):
516            return _('This category of payments has been disabled.'), None
517        payment = createObject(u'waeup.StudentOnlinePayment')
518        timestamp = ("%d" % int(time()*10000))[1:]
519        payment.p_id = "p%s" % timestamp
520        payment.p_category = category
521        payment.p_item = p_item
522        payment.p_session = p_session
523        payment.p_level = p_level
524        payment.p_current = p_current
525        payment.amount_auth = amount
526        return None, payment
527
528    def _admissionText(self, student, portal_language):
529        inst_name = grok.getSite()['configuration'].name
530        entry_session = student['studycourse'].entry_session
531        entry_session = academic_sessions_vocab.getTerm(entry_session).title
532        text = trans(_(
533            'This is to inform you that you have been offered provisional'
534            ' admission into ${a} for the ${b} academic session as follows:',
535            mapping = {'a': inst_name, 'b': entry_session}),
536            portal_language)
537        return text
538
539    def warnCreditsOOR(self, studylevel, course=None):
540        studycourse = studylevel.__parent__
541        certificate = getattr(studycourse,'certificate', None)
542        current_level = studycourse.current_level
543        if None in (current_level, certificate):
544            return
545        end_level = certificate.end_level
546        if current_level >= end_level:
547            limit = 52
548        else:
549            limit = 48
550        if course and studylevel.total_credits + course.credits > limit:
551            return  _('Maximum credits exceeded.')
552        elif studylevel.total_credits > limit:
553            return _('Maximum credits exceeded.')
554        return
555
556    def deactivated_warnCreditsOOR(self, studylevel, course=None):
557        if course and studylevel.total_credits + course.credits > 48:
558            return _('Maximum credits exceeded.')
559        elif studylevel.total_credits > 48:
560            return _('Maximum credits exceeded.')
561        return
562
563    def getBedCoordinates(self, bedticket):
564        """Return descriptive bed coordinates.
565        This method can be used to customize the `display_coordinates`
566        property method in order to  display a
567        customary description of the bed space.
568        """
569        bc = bedticket.bed_coordinates.split(',')
570        if len(bc) == 4:
571            return bc[0]
572        return bedticket.bed_coordinates
573
574    ACCOMMODATION_SPAN = 999
575
576    def getAccommodationDetails(self, student):
577        """Determine the accommodation data of a student.
578        We are usimng the base package method again.
579        """
580        d = {}
581        d['error'] = u''
582        hostels = grok.getSite()['hostels']
583        d['booking_session'] = hostels.accommodation_session
584        d['allowed_states'] = hostels.accommodation_states
585        d['startdate'] = hostels.startdate
586        d['enddate'] = hostels.enddate
587        d['expired'] = hostels.expired
588        # Determine bed type
589        bt = 'all'
590        if student.sex == 'f':
591            sex = 'female'
592        else:
593            sex = 'male'
594        special_handling = 'regular'
595        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
596        return d
597
598    def checkAccommodationRequirements(self, student, acc_details):
599        msg = super(CustomStudentsUtils, self).checkAccommodationRequirements(
600            student, acc_details)
601        if msg:
602            return msg
603        if student.current_mode not in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
604            return _('You are not eligible to book accommodation.')
605        return
606
607    # AAUE prefix
608    STUDENT_ID_PREFIX = u'E'
609
610    STUDENT_EXPORTER_NAMES = (
611            'students',
612            'studentstudycourses',
613            'studentstudycourses_1',
614            'studentstudylevels',
615            #'studentstudylevels_1',
616            'coursetickets',
617            #'coursetickets_1',
618            'studentpayments',
619            'bedtickets',
620            'unpaidpayments',
621            'sfpaymentsoverview',
622            'studylevelsoverview',
623            'combocard',
624            'bursary',
625            'levelreportdata',
626            'outstandingcourses',
627            'sessionpaymentsoverview',
628            'accommodationpayments',
629            'transcriptdata',
630            'trimmedpayments',
631            'trimmed',
632            'outstandingcourses_2'
633            )
634
635    # Maximum size of upload files in kB
636    MAX_KB = 500
Note: See TracBrowser for help on using the repository browser.