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

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

Pass degree has NOT been phased out for diploma students.

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