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

Last change on this file since 15257 was 15194, checked in by Henrik Bettermann, 6 years ago

See AAUE #513.

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