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

Last change on this file since 14899 was 14899, checked in by Henrik Bettermann, 7 years ago

Implement GPA classification for diploma students.

  • Property svn:keywords set to Id
File size: 23.9 KB
Line 
1## $Id: utils.py 14899 2017-11-16 06:51:01Z 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
19from time import time
20from zope.component import createObject
21from waeup.kofa.interfaces import (
22    ADMITTED, CLEARANCE, REQUESTED, CLEARED, RETURNING, PAID,
23    academic_sessions_vocab)
24from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
25from waeup.kofa.accesscodes import create_accesscode
26from waeup.kofa.students.utils import trans
27from waeup.aaue.interswitch.browser import gateway_net_amt, GATEWAY_AMT
28from waeup.aaue.interfaces import MessageFactory as _
29
30MINIMUM_UNITS_THRESHOLD = 15
31
32class CustomStudentsUtils(NigeriaStudentsUtils):
33    """A collection of customized methods.
34
35    """
36
37    PORTRAIT_CHANGE_STATES = (ADMITTED,)
38
39    gpa_boundaries_diploma = ((1.5, 'Fail'),
40                              (1.5, 'Fail'),
41                              (2.4, 'Pass'),
42                              (3.5, 'Merit'),
43                              (4.5, 'Credit'),
44                              (5, 'Distinction'))
45
46    gpa_boundaries = ((1, 'FRNS / NER / NYV'),
47                      (1.5, 'Pass'),
48                      (2.4, '3rd Class Honours (Diploma: Pass)'),
49                      (3.5, '2nd Class Honours Lower Division (Diploma: Merit)'),
50                      (4.5, '2nd Class Honours Upper Division (Diploma: Credit)'),
51                      (5, '1st Class Honours (Diploma: Distinction)'))
52
53    def getClassFromCGPA(self, gpa, student):
54        if student.current_mode.startswith('dp'):
55            gpa_boundaries = self.gpa_boundaries_diploma
56
57        else:
58            gpa_boundaries = self.gpa_boundaries
59        if gpa < gpa_boundaries[0][0]:
60            # FRNS / Fail
61            return 0, gpa_boundaries[0][1]
62        if student.entry_session < 2013 and \
63            not student.current_mode.startswith('dp'):
64            if gpa < gpa_boundaries[1][0]:
65                # Pass
66                return 1, gpa_boundaries[1][1]
67        else:
68            if gpa < gpa_boundaries[1][0]:
69                # FRNS (Pass degree has been phased out in 2013)
70                return 0, gpa_boundaries[0][1]
71        if gpa < gpa_boundaries[2][0]:
72            # 3rd / Pass
73            return 2, gpa_boundaries[2][1]
74        if gpa < gpa_boundaries[3][0]:
75            # 2nd L / Merit
76            return 3, gpa_boundaries[3][1]
77        if gpa < gpa_boundaries[4][0]:
78            # 2nd U / Credit
79            return 4, gpa_boundaries[4][1]
80        if gpa <= gpa_boundaries[5][0]:
81            # 1st / Distinction
82            return 5, gpa_boundaries[5][1]
83        return 'N/A'
84
85    def getDegreeClassNumber(self, level_obj):
86        """Get degree class number (used for SessionResultsPresentation
87        reports).
88        """
89        certificate = getattr(level_obj.__parent__,'certificate', None)
90        end_level = getattr(certificate, 'end_level', None)
91        if end_level and level_obj.level >= end_level:
92            if level_obj.level > end_level:
93                # spill-over level
94                if level_obj.gpa_params[1] == 0:
95                    # no credits taken
96                    return 0
97            elif level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
98                # credits taken below limit
99                return 0
100            failed_courses = level_obj.passed_params[4]
101            not_taken_courses = level_obj.passed_params[5]
102            if '_m' in failed_courses:
103                return 0
104            if len(not_taken_courses) \
105                and not not_taken_courses == 'Nil':
106                return 0
107        elif level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
108            # credits taken below limit
109            return 0
110        if level_obj.level_verdict in ('FRNS', 'NER', 'NYV'):
111            return 0
112        # use gpa_boundaries above
113        return self.getClassFromCGPA(
114            level_obj.cumulative_params[0], level_obj.student)[0]
115
116    def increaseMatricInteger(self, student):
117        """Increase counter for matric numbers.
118        This counter can be a centrally stored attribute or an attribute of
119        faculties, departments or certificates. In the base package the counter
120        is as an attribute of the site configuration container.
121        """
122        if student.current_mode in ('ug_pt', 'de_pt'):
123            grok.getSite()['configuration'].next_matric_integer += 1
124            return
125        elif student.is_postgrad:
126            grok.getSite()['configuration'].next_matric_integer_3 += 1
127            return
128        elif student.current_mode in ('dp_ft',):
129            grok.getSite()['configuration'].next_matric_integer_4 += 1
130            return
131        grok.getSite()['configuration'].next_matric_integer_2 += 1
132        return
133
134    def _concessionalPaymentMade(self, student):
135        if len(student['payments']):
136            for ticket in student['payments'].values():
137                if ticket.p_state == 'paid' and \
138                    ticket.p_category == 'concessional':
139                    return True
140        return False
141
142    def constructMatricNumber(self, student):
143        faccode = student.faccode
144        depcode = student.depcode
145        certcode = student.certcode
146        degree = getattr(
147            getattr(student.get('studycourse', None), 'certificate', None),
148                'degree', None)
149        year = unicode(student.entry_session)[2:]
150        if not student.state in (PAID, ) or not student.is_fresh or \
151            student.current_mode in ('found', 'ijmbe'):
152            return _('Matriculation number cannot be set.'), None
153        #if student.current_mode not in ('mug_ft', 'mde_ft') and \
154        #    not self._concessionalPaymentMade(student):
155        #    return _('Matriculation number cannot be set.'), None
156        if student.is_postgrad:
157            next_integer = grok.getSite()['configuration'].next_matric_integer_3
158            if not degree or next_integer == 0:
159                return _('Matriculation number cannot be set.'), None
160            if student.faccode in ('IOE'):
161                return None, "AAU/SPS/%s/%s/%s/%05d" % (
162                    faccode, year, degree, next_integer)
163            return None, "AAU/SPS/%s/%s/%s/%s/%05d" % (
164                faccode, depcode, year, degree, next_integer)
165        if student.current_mode in ('ug_pt', 'de_pt'):
166            next_integer = grok.getSite()['configuration'].next_matric_integer
167            if next_integer == 0:
168                return _('Matriculation number cannot be set.'), None
169            return None, "PTP/%s/%s/%s/%05d" % (
170                faccode, depcode, year, next_integer)
171        if student.current_mode in ('dp_ft',):
172            next_integer = grok.getSite()['configuration'].next_matric_integer_4
173            if next_integer == 0:
174                return _('Matriculation number cannot be set.'), None
175            return None, "IOE/DIP/%s/%05d" % (year, next_integer)
176        next_integer = grok.getSite()['configuration'].next_matric_integer_2
177        if next_integer == 0:
178            return _('Matriculation number cannot be set.'), None
179        if student.faccode in ('FBM', 'FCS'):
180            return None, "CMS/%s/%s/%s/%05d" % (
181                faccode, depcode, year, next_integer)
182        return None, "%s/%s/%s/%05d" % (faccode, depcode, year, next_integer)
183
184    def getReturningData(self, student):
185        """ This method defines what happens after school fee payment
186        of returning students depending on the student's senate verdict.
187        """
188        prev_level = student['studycourse'].current_level
189        cur_verdict = student['studycourse'].current_verdict
190        if cur_verdict in ('A','B','C', 'L','M','N','Z',):
191            # Successful student
192            new_level = divmod(int(prev_level),100)[0]*100 + 100
193        #elif cur_verdict == 'C':
194        #    # Student on probation
195        #    new_level = int(prev_level) + 10
196        else:
197            # Student is somehow in an undefined state.
198            # Level has to be set manually.
199            new_level = prev_level
200        new_session = student['studycourse'].current_session + 1
201        return new_session, new_level
202
203    def _isPaymentDisabled(self, p_session, category, student):
204        academic_session = self._getSessionConfiguration(p_session)
205        if category.startswith('schoolfee'):
206            if 'sf_all' in academic_session.payment_disabled:
207                return True
208            if 'sf_pg' in academic_session.payment_disabled and \
209                student.is_postgrad:
210                return True
211            if 'sf_ug_pt' in academic_session.payment_disabled and \
212                student.current_mode in ('ug_pt', 'de_pt'):
213                return True
214            if 'sf_found' in academic_session.payment_disabled and \
215                student.current_mode == 'found':
216                return True
217        if category.startswith('clearance') and \
218            'cl_regular' in academic_session.payment_disabled and \
219            student.current_mode in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
220            return True
221        if category == 'hostel_maintenance' and \
222            'maint_all' in academic_session.payment_disabled:
223            return True
224        return False
225
226    def setPaymentDetails(self, category, student,
227            previous_session=None, previous_level=None):
228        """Create Payment object and set the payment data of a student for
229        the payment category specified.
230
231        """
232        details = {}
233        p_item = u''
234        amount = 0.0
235        error = u''
236        if previous_session:
237            if previous_session < student['studycourse'].entry_session:
238                return _('The previous session must not fall below '
239                         'your entry session.'), None
240            if category == 'schoolfee':
241                # School fee is always paid for the following session
242                if previous_session > student['studycourse'].current_session:
243                    return _('This is not a previous session.'), None
244            else:
245                if previous_session > student['studycourse'].current_session - 1:
246                    return _('This is not a previous session.'), None
247            p_session = previous_session
248            p_level = previous_level
249            p_current = False
250        else:
251            p_session = student['studycourse'].current_session
252            p_level = student['studycourse'].current_level
253            p_current = True
254        academic_session = self._getSessionConfiguration(p_session)
255        if academic_session == None:
256            return _(u'Session configuration object is not available.'), None
257        # Determine fee.
258        if category == 'transfer':
259            amount = academic_session.transfer_fee
260        elif category == 'transcript_local':
261            amount = academic_session.transcript_fee_local
262        elif category == 'transcript_inter':
263            amount = academic_session.transcript_fee_inter
264        elif category == 'bed_allocation':
265            amount = academic_session.booking_fee
266        elif category == 'restitution':
267            if student.entry_session >= 2016 \
268                or student.current_mode not in ('ug_ft', 'dp_ft'):
269                return _(u'Restitution fee payment not required.'), None
270            amount = academic_session.restitution_fee
271        elif category == 'hostel_maintenance':
272            amount = 0.0
273            bedticket = student['accommodation'].get(
274                str(student.current_session), None)
275            if bedticket is not None and bedticket.bed is not None:
276                p_item = bedticket.display_coordinates
277                if bedticket.bed.__parent__.maint_fee > 0:
278                    amount = bedticket.bed.__parent__.maint_fee
279                else:
280                    # fallback
281                    amount = academic_session.maint_fee
282            else:
283                return _(u'No bed allocated.'), None
284        elif student.current_mode == 'found' and category not in (
285            'schoolfee', 'clearance', 'late_registration'):
286            return _('Not allowed.'), None
287        elif category.startswith('clearance'):
288            if student.state not in (ADMITTED, CLEARANCE, REQUESTED, CLEARED):
289                return _(u'Acceptance Fee payments not allowed.'), None
290            if student.current_mode in (
291                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
292                'transfer', 'mug_ft', 'mde_ft') \
293                and category != 'clearance_incl':
294                    return _("Additional fees must be included."), None
295            if student.current_mode == 'ijmbe':
296                amount = academic_session.clearance_fee_ijmbe
297            elif student.faccode == 'FP':
298                amount = academic_session.clearance_fee_fp
299            elif student.current_mode.endswith('_pt'):
300                if student.is_postgrad:
301                    amount = academic_session.clearance_fee_pg_pt
302                else:
303                    amount = academic_session.clearance_fee_ug_pt
304            elif student.faccode == 'FCS':
305                # Students in clinical medical sciences pay the medical
306                # acceptance fee
307                amount = academic_session.clearance_fee_med
308            elif student.is_postgrad:  # and not part-time
309                if category != 'clearance':
310                    return _("No additional fees required."), None
311                amount = academic_session.clearance_fee_pg
312            else:
313                amount = academic_session.clearance_fee
314            p_item = student['studycourse'].certificate.code
315            if amount in (0.0, None):
316                return _(u'Amount could not be determined.'), None
317            # Add Matric Gown Fee and Lapel Fee
318            if category == 'clearance_incl':
319                amount += gateway_net_amt(academic_session.matric_gown_fee) + \
320                    gateway_net_amt(academic_session.lapel_fee)
321        elif category == 'late_registration':
322            if student.is_postgrad:
323                amount = academic_session.late_pg_registration_fee
324            else:
325                amount = academic_session.late_registration_fee
326        elif category.startswith('schoolfee'):
327            try:
328                certificate = student['studycourse'].certificate
329                p_item = certificate.code
330            except (AttributeError, TypeError):
331                return _('Study course data are incomplete.'), None
332            if student.is_postgrad and category != 'schoolfee':
333                return _("No additional fees required."), None
334            if not previous_session and student.current_mode in (
335                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
336                'transfer', 'mug_ft', 'mde_ft') \
337                and not category in (
338                'schoolfee_incl', 'schoolfee_1', 'schoolfee_2'):
339                    return _("You must choose a payment which includes "
340                             "additional fees."), None
341            if category in ('schoolfee_1', 'schoolfee_2'):
342                if student.current_mode == 'ug_pt':
343                    return _("Part-time students are not allowed "
344                             "to pay by instalments."), None
345                if student.entry_session < 2015:
346                    return _("You are not allowed "
347                             "to pay by instalments."), None
348            if previous_session:
349                # Students can pay for previous sessions in all
350                # workflow states.  Fresh students are excluded by the
351                # update method of the PreviousPaymentAddFormPage.
352                if previous_level == 100:
353                    amount = getattr(certificate, 'school_fee_1', 0.0)
354                else:
355                    if student.entry_session >= 2015:
356                        amount = getattr(certificate, 'school_fee_2', 0.0)
357                    else:
358                        amount = getattr(certificate, 'school_fee_3', 0.0)
359            elif student.state == CLEARED and category != 'schoolfee_2':
360                amount = getattr(certificate, 'school_fee_1', 0.0)
361                # Cut school fee by 50%
362                if category == 'schoolfee_1' and amount:
363                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
364            elif student.is_fresh and category == 'schoolfee_2':
365                amount = getattr(certificate, 'school_fee_1', 0.0)
366                # Cut school fee by 50%
367                if amount:
368                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
369            elif student.state == RETURNING and category != 'schoolfee_2':
370                if not student.father_name:
371                    return _("Personal data form is not properly filled."), None
372                # In case of returning school fee payment the payment session
373                # and level contain the values of the session the student
374                # has paid for.
375                p_session, p_level = self.getReturningData(student)
376                try:
377                    academic_session = grok.getSite()[
378                        'configuration'][str(p_session)]
379                except KeyError:
380                    return _(u'Session configuration object is not available.'), None
381                if student.entry_session >= 2015:
382                    amount = getattr(certificate, 'school_fee_2', 0.0)
383                    # Cut school fee by 50%
384                    if category == 'schoolfee_1' and amount:
385                        amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
386                else:
387                    amount = getattr(certificate, 'school_fee_3', 0.0)
388            elif category == 'schoolfee_2':
389                amount = getattr(certificate, 'school_fee_2', 0.0)
390                # Cut school fee by 50%
391                if amount:
392                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
393            else:
394                return _('Wrong state.'), None
395            if amount in (0.0, None):
396                return _(u'Amount could not be determined.'), None
397            # Add Student Union Fee , Student Id Card Fee and Welfare Assurance
398            if category in ('schoolfee_incl', 'schoolfee_1'):
399                amount += gateway_net_amt(academic_session.welfare_fee) + \
400                    gateway_net_amt(academic_session.union_fee)
401                if student.entry_session == 2016 \
402                    and student.entry_mode == 'ug_ft' \
403                    and student.state == CLEARED:
404                    amount += gateway_net_amt(academic_session.id_card_fee)
405            # Add non-indigenous fee and session specific penalty fees
406            if student.is_postgrad:
407                amount += academic_session.penalty_pg
408                if student.lga and not student.lga.startswith('edo'):
409                    amount += 20000.0
410            else:
411                amount += academic_session.penalty_ug
412        elif not student.is_postgrad:
413            fee_name = category + '_fee'
414            amount = getattr(academic_session, fee_name, 0.0)
415        if amount in (0.0, None):
416            return _(u'Amount could not be determined.'), None
417        # Create ticket.
418        for key in student['payments'].keys():
419            ticket = student['payments'][key]
420            if ticket.p_state == 'paid' and\
421               ticket.p_category == category and \
422               ticket.p_item == p_item and \
423               ticket.p_session == p_session:
424                  return _('This type of payment has already been made.'), None
425            # Additional condition in AAUE
426            if category in ('schoolfee', 'schoolfee_incl', 'schoolfee_1'):
427                if ticket.p_state == 'paid' and \
428                   ticket.p_category in ('schoolfee',
429                                         'schoolfee_incl',
430                                         'schoolfee_1') and \
431                   ticket.p_item == p_item and \
432                   ticket.p_session == p_session:
433                      return _(
434                          'Another school fee payment for this '
435                          'session has already been made.'), None
436
437        if self._isPaymentDisabled(p_session, category, student):
438            return _('This category of payments has been disabled.'), None
439        payment = createObject(u'waeup.StudentOnlinePayment')
440        timestamp = ("%d" % int(time()*10000))[1:]
441        payment.p_id = "p%s" % timestamp
442        payment.p_category = category
443        payment.p_item = p_item
444        payment.p_session = p_session
445        payment.p_level = p_level
446        payment.p_current = p_current
447        payment.amount_auth = amount
448        return None, payment
449
450    def _admissionText(self, student, portal_language):
451        inst_name = grok.getSite()['configuration'].name
452        entry_session = student['studycourse'].entry_session
453        entry_session = academic_sessions_vocab.getTerm(entry_session).title
454        text = trans(_(
455            'This is to inform you that you have been offered provisional'
456            ' admission into ${a} for the ${b} academic session as follows:',
457            mapping = {'a': inst_name, 'b': entry_session}),
458            portal_language)
459        return text
460
461    def warnCreditsOOR(self, studylevel, course=None):
462        studycourse = studylevel.__parent__
463        certificate = getattr(studycourse,'certificate', None)
464        current_level = studycourse.current_level
465        if None in (current_level, certificate):
466            return
467        end_level = certificate.end_level
468        if current_level >= end_level:
469            limit = 52
470        else:
471            limit = 48
472        if course and studylevel.total_credits + course.credits > limit:
473            return  _('Maximum credits exceeded.')
474        elif studylevel.total_credits > limit:
475            return _('Maximum credits exceeded.')
476        return
477
478    def getBedCoordinates(self, bedticket):
479        """Return descriptive bed coordinates.
480        This method can be used to customize the `display_coordinates`
481        property method in order to  display a
482        customary description of the bed space.
483        """
484        bc = bedticket.bed_coordinates.split(',')
485        if len(bc) == 4:
486            return bc[0]
487        return bedticket.bed_coordinates
488
489    def getAccommodationDetails(self, student):
490        """Determine the accommodation data of a student.
491        """
492        d = {}
493        d['error'] = u''
494        hostels = grok.getSite()['hostels']
495        d['booking_session'] = hostels.accommodation_session
496        d['allowed_states'] = hostels.accommodation_states
497        d['startdate'] = hostels.startdate
498        d['enddate'] = hostels.enddate
499        d['expired'] = hostels.expired
500        # Determine bed type
501        bt = 'all'
502        if student.sex == 'f':
503            sex = 'female'
504        else:
505            sex = 'male'
506        special_handling = 'regular'
507        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
508        return d
509
510    def checkAccommodationRequirements(self, student, acc_details):
511        msg = super(CustomStudentsUtils, self).checkAccommodationRequirements(
512            student, acc_details)
513        if msg:
514            return msg
515        if student.current_mode not in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
516            return _('You are not eligible to book accommodation.')
517        return
518
519    # AAUE prefix
520    STUDENT_ID_PREFIX = u'E'
521
522    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
523            'studentstudylevels', 'coursetickets',
524            'studentpayments', 'studentunpaidpayments',
525            'bedtickets', 'paymentsoverview',
526            'studylevelsoverview', 'combocard', 'bursary',
527            'levelreportdata')
Note: See TracBrowser for help on using the repository browser.