source: main/waeup.aaue/trunk/src/waeup/aaue/students/studylevel.py @ 15430

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

We check if there exists a certificate course in the certificate
container which refers to course ticket to be added. If such an object does
not exist, students and managers will be prevented from registering
the corresponding course.

  • Property svn:keywords set to Id
File size: 17.6 KB
RevLine 
[8326]1## $Id: studylevel.py 15429 2019-05-27 10:51:04Z henrik $
2##
3## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""
19Container which holds the data of a student study level
20and contains the course tickets.
21"""
22import grok
[13036]23import pytz
24from datetime import datetime
[8326]25from zope.component.interfaces import IFactory
[9502]26from zope.component import createObject
[8326]27from zope.interface import implementedBy
28from waeup.kofa.utils.helpers import attrs_to_fields
[15412]29from waeup.kofa.interfaces import RETURNING, VALIDATED, REGISTERED
[14227]30from waeup.kofa.students.browser import TicketError
[8326]31from waeup.kofa.students.studylevel import (
32    StudentStudyLevel, CourseTicket,
33    CourseTicketFactory, StudentStudyLevelFactory)
[14075]34from waeup.kofa.students.interfaces import IStudentNavigation, ICourseTicket
[8444]35from waeup.aaue.students.interfaces import (
[8867]36    ICustomStudentStudyLevel, ICustomCourseTicket)
[14663]37from waeup.aaue.students.utils import MINIMUM_UNITS_THRESHOLD
[14248]38from waeup.aaue.interfaces import MessageFactory as _
[8326]39
40
41class CustomStudentStudyLevel(StudentStudyLevel):
42    """This is a container for course tickets.
43    """
44    grok.implements(ICustomStudentStudyLevel, IStudentNavigation)
45    grok.provides(ICustomStudentStudyLevel)
46
[9914]47    @property
48    def total_credits_s1(self):
49        total = 0
50        for ticket in self.values():
[14576]51            if ticket.semester == 1 and not ticket.outstanding:
[9914]52                total += ticket.credits
53        return total
54
55    @property
56    def total_credits_s2(self):
57        total = 0
58        for ticket in self.values():
[14576]59            if ticket.semester == 2 and not ticket.outstanding:
[9914]60                total += ticket.credits
61        return total
62
[10443]63    @property
[13834]64    def gpa_params(self):
65        """Calculate gpa parameters for this level.
66        """
67        credits_weighted = 0.0
68        credits_counted = 0
69        level_gpa = 0.0
70        for ticket in self.values():
[14532]71            if ticket.total_score is not None:
[13834]72                credits_counted += ticket.credits
73                credits_weighted += ticket.credits * ticket.weight
74        if credits_counted:
[14384]75            level_gpa = credits_weighted/credits_counted
[14206]76        # Override level_gpa if value has been imported
77        imported_gpa = getattr(self, 'imported_gpa', None)
78        if imported_gpa:
79            level_gpa = imported_gpa
[13834]80        return level_gpa, credits_counted, credits_weighted
81
82    @property
[10480]83    def gpa_params_rectified(self):
84        return self.gpa_params
[10443]85
[13036]86    @property
[14357]87    def passed_params(self):
88        """Determine the number and credits of passed and failed courses.
89        This method is used for level reports.
90        """
91        passed = failed = 0
92        courses_failed = ''
93        credits_failed = 0
94        credits_passed = 0
[14393]95        courses_not_taken = ''
[14357]96        for ticket in self.values():
[14532]97            if ticket.total_score is not None:
[14357]98                if ticket.total_score < ticket.passmark:
99                    failed += 1
100                    credits_failed += ticket.credits
101                    if ticket.mandatory:
102                        courses_failed += 'm_%s_m ' % ticket.code
103                    else:
104                        courses_failed += '%s ' % ticket.code
105                else:
106                    passed += 1
107                    credits_passed += ticket.credits
[14370]108            else:
[14393]109                courses_not_taken += '%s ' % ticket.code
[14388]110        if not len(courses_failed):
[14506]111            courses_failed = 'Nil'
[14428]112        if not len(courses_not_taken):
[14506]113            courses_not_taken = 'Nil'
[14370]114        return (passed, failed, credits_passed,
115                credits_failed, courses_failed,
[14393]116                courses_not_taken)
[14357]117
118    @property
[14248]119    def course_registration_forbidden(self):
[14501]120        #fac_dep_paid = True
121        #if self.student.entry_session >= 2016:
122        #    fac_dep_paid = False
123        #    for ticket in self.student['payments'].values():
124        #        if ticket.p_category == 'fac_dep' and \
125        #            ticket.p_session == self.level_session and \
126        #            ticket.p_state == 'paid':
127        #                fac_dep_paid = True
128        #                continue
129        #if not fac_dep_paid:
130        #    return _("Please pay faculty and departmental dues first.")
[14509]131
132
133        ######################################################
[15370]134        # Temporarily disable ug_ft course registration
[15378]135        #if self.student.current_mode == 'ug_ft':
136        #    return _("Course registration has been disabled.")
[14509]137        ######################################################
138
139
[14500]140        restitution_paid = True
[15178]141        if self.student.current_session == 2016 \
142            and self.student.current_mode in ('ug_ft', 'dp_ft') \
143            and not self.student.is_fresh:
[14500]144            restitution_paid = False
145            for ticket in self.student['payments'].values():
146                if ticket.p_category == 'restitution' and \
147                    ticket.p_session == self.level_session and \
148                    ticket.p_state == 'paid':
149                        restitution_paid = True
150                        continue
151        if not restitution_paid:
152            return _("Please pay restitution fee first.")
[15428]153        #if self.student.is_fresh:
154        #    return
[13036]155        try:
[15398]156            academic_session = grok.getSite()['configuration'][
157                str(self.level_session)]
[14117]158            if self.student.is_postgrad:
[15398]159                deadline = academic_session.coursereg_deadline_pg
[14117]160            elif self.student.current_mode.startswith('dp'):
[15398]161                deadline = academic_session.coursereg_deadline_dp
[14117]162            elif self.student.current_mode.endswith('_pt'):
[15398]163                deadline = academic_session.coursereg_deadline_pt
[14117]164            elif self.student.current_mode == 'found':
[15398]165                deadline = academic_session.coursereg_deadline_found
[14117]166            else:
[15398]167                deadline = academic_session.coursereg_deadline
[13071]168        except (TypeError, KeyError):
[14248]169            return
[13070]170        if not deadline or deadline > datetime.now(pytz.utc):
[14248]171            return
[15398]172        if self.student.is_postgrad:
173            lcrfee = academic_session.late_pg_registration_fee
174        else:
175            lcrfee = academic_session.late_registration_fee
176        if not lcrfee:
177            return _("Course registration has been disabled.")
[13036]178        if len(self.student['payments']):
179            for ticket in self.student['payments'].values():
180                if ticket.p_category == 'late_registration' and \
181                    ticket.p_session == self.level_session and \
182                    ticket.p_state == 'paid':
[14248]183                        return
184        return _("Course registration has ended. "
185                 "Please pay the late registration fee.")
[13036]186
[14082]187    # only AAUE
188    @property
189    def remark(self):
[14161]190        certificate = getattr(self.__parent__,'certificate',None)
191        end_level = getattr(certificate, 'end_level', None)
[14918]192        study_mode = getattr(certificate, 'study_mode', None)
193        is_dp = False
194        if study_mode and study_mode.startswith('dp'):
195            is_dp = True
[14380]196        failed_limit = 1.5
197        if self.student.entry_session < 2013:
198            failed_limit = 1.0
[14377]199        # final level student remark
200        if end_level and self.level >= end_level:
[14464]201            if self.level > end_level:
202                # spill-over level
203                if self.gpa_params[1] == 0:
204                    # no credits taken
[14487]205                    return 'NER'
[14663]206            elif self.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
[14537]207                # credits taken below limit
208                return 'NER'
[14487]209            if self.level_verdict in ('FRNS', 'NER', 'NYV'):
[14415]210                return self.level_verdict
[14412]211            if '_m' in self.passed_params[4]:
[14161]212                return 'FRNS'
[14412]213            if not self.cumulative_params[0]:
214                return 'FRNS'
[14435]215            if len(self.passed_params[5]) \
[14506]216                and not self.passed_params[5] == 'Nil':
[14412]217                return 'FRNS'
[14955]218            if self.cumulative_params[1] < 60:
[14623]219                return 'FRNS'
[14380]220            if self.cumulative_params[0] < failed_limit:
[14161]221                return 'Fail'
[15054]222            dummy, repeat = divmod(self.level, 100)
[15090]223            if self.cumulative_params[0] < 5.1 and repeat == 20:
[15054]224                # Irrespective of the CGPA of a student, if the He/She has
225                # 3rd Extension, such student will be graduated with a "Pass".
226                return 'Pass'
[14463]227            if self.cumulative_params[0] < 1.5:
[14918]228                if is_dp:
229                    return 'Fail'
[14463]230                return 'Pass'
[14161]231            if self.cumulative_params[0] < 2.4:
[14918]232                if is_dp:
233                    return 'Pass'
[14327]234                return '3s_rd_s'
[14161]235            if self.cumulative_params[0] < 3.5:
[14918]236                if is_dp:
237                    return 'Merit'
[14327]238                return '2s_2_s'
[14161]239            if self.cumulative_params[0] < 4.5:
[14918]240                if is_dp:
241                    return 'Credit'
[14327]242                return '2s_1_s'
[14161]243            if self.cumulative_params[0] < 5.1:
[14918]244                if is_dp:
245                    return 'Distinction'
[14327]246                return '1s_st_s'
[14161]247            return 'N/A'
248        # returning student remark
[14487]249        if self.level_verdict in ('FRNS', 'NER', 'NYV'):
[14475]250            return 'Probation'
251        if self.level_verdict == 'D':
252            return 'Withdrawn'
[14662]253        if self.gpa_params[1] == 0:
254            # no credits taken
255            return 'NER'
[14663]256        if self.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
[14464]257            # credits taken below limit
[14415]258            return 'Probation'
[14380]259        if self.cumulative_params[0] < failed_limit:
[14082]260            return 'Probation'
261        if self.cumulative_params[0] < 5.1:
262            return 'Proceed'
263        return 'N/A'
264
[14227]265    def _schoolfeePaymentMade(self):
266        if len(self.student['payments']):
267            for ticket in self.student['payments'].values():
268                if ticket.p_state == 'paid' and \
269                    ticket.p_category in (
270                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
271                    ticket.p_session == self.student[
272                        'studycourse'].current_session:
273                    return True
274        return False
275
[14259]276    def _coursePaymentsMade(self, course):
[14266]277        if self.level_session < 2016:
278            return True
[14259]279        if not course.code[:3] in ('GST', 'ENT'):
280            return True
281        if len(self.student['payments']):
282            paid_cats = list()
283            for pticket in self.student['payments'].values():
284                if pticket.p_state == 'paid':
285                    paid_cats.append(pticket.p_category)
286            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
287                not 'gst_registration_1' in paid_cats:
288                return False
289            if course.code in ('GST222',) and \
290                not 'gst_registration_2' in paid_cats:
291                return False
[14675]292            #if course.code in ('ENT201',) and \
293            #    not 'ent_registration_1' in paid_cats:
294            #    return False
[14355]295            if course.code in ('GST101', 'GST102') and \
[14348]296                not 'gst_text_book_1' in paid_cats and \
[14355]297                not 'gst_text_book_0' in paid_cats:
[14259]298                return False
[14355]299            if course.code in ('GST111', 'GST112') and \
[14348]300                not 'gst_text_book_2' in paid_cats and \
[14355]301                not 'gst_text_book_0' in paid_cats:
[14348]302                return False
[14259]303            if course.code in ('GST222',) and \
304                not 'gst_text_book_3' in paid_cats:
305                return False
[14675]306            #if course.code in ('ENT201',) and \
307            #    not 'ent_text_book_1' in paid_cats:
308            #    return False
[14259]309            return True
310        return False
311
[14075]312    def addCourseTicket(self, ticket, course):
313        """Add a course ticket object.
314        """
315        if not ICourseTicket.providedBy(ticket):
316            raise TypeError(
317                'StudentStudyLeves contain only ICourseTicket instances')
[14227]318        # Raise TicketError if course is in 2nd semester but
319        # schoolfee has not yet been fully paid.
320        if course.semester == 2 and not self._schoolfeePaymentMade():
[14252]321            raise TicketError(
322                _('%s is a 2nd semester course which can only be added '
323                  'if school fees have been fully paid.' % course.code))
[14259]324        # Raise TicketError if registration fee or text
325        # book fee haven't been paid.
326        if not self._coursePaymentsMade(course):
327            raise TicketError(
328                _('%s can only be added if both registration fee and text '
329                  'book fee have been paid.'
330                  % course.code))
[15429]331        # We check if there exists a certificate course in the certificate
332        # container which refers to the course. If such an object does
333        # not exist, students and managers will be prevented from registering
334        # the corresponding course.
335        cert = self.__parent__.certificate
336        ticket_allowed = False
337        for val in cert.values():
338            if val.course == course:
339                ticket_allowed = True
340                break
341        if not ticket_allowed:
342            raise TicketError(
343                _('%s is not part of the %s curriculum.'
344                  % (course.code, cert.code)))
[14075]345        ticket.code = course.code
346        ticket.title = course.title
347        ticket.fcode = course.__parent__.__parent__.__parent__.code
348        ticket.dcode = course.__parent__.__parent__.code
349        ticket.credits = course.credits
350        if self.student.entry_session < 2013:
351            ticket.passmark = course.passmark - 5
352        else:
353            ticket.passmark = course.passmark
354        ticket.semester = course.semester
355        self[ticket.code] = ticket
356        return
357
[14227]358    def addCertCourseTickets(self, cert):
359        """Collect all certificate courses and create course
360        tickets automatically.
361        """
362        if cert is not None:
363            for key, val in cert.items():
364                if val.level != self.level:
365                    continue
366                ticket = createObject(u'waeup.CourseTicket')
367                ticket.automatic = True
368                ticket.mandatory = val.mandatory
369                ticket.carry_over = False
370                try:
371                    self.addCourseTicket(ticket, val.course)
372                except TicketError:
373                    pass
374        return
375
[9692]376CustomStudentStudyLevel = attrs_to_fields(
[9914]377    CustomStudentStudyLevel, omit=[
[10480]378    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
[8326]379
380class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
381    """A factory for student study levels.
382    """
383
384    def __call__(self, *args, **kw):
385        return CustomStudentStudyLevel()
386
387    def getInterfaces(self):
388        return implementedBy(CustomStudentStudyLevel)
389
390class CustomCourseTicket(CourseTicket):
391    """This is a course ticket which allows the
392    student to attend the course. Lecturers will enter scores and more at
393    the end of the term.
394
395    A course ticket contains a copy of the original course and
396    course referrer data. If the courses and/or their referrers are removed, the
397    corresponding tickets remain unchanged. So we do not need any event
398    triggered actions on course tickets.
399    """
400    grok.implements(ICustomCourseTicket, IStudentNavigation)
401    grok.provides(ICustomCourseTicket)
402
[13834]403    @property
[14532]404    def _getGradeWeightFromScore(self):
405        """Nigerian Course Grading System
406        """
407        if self.score == -1:
408            return ('-',0) # core course and result not yet available (used by AAUE)
409        if self.total_score is None:
410            return (None, None)
411        if self.total_score >= 70:
412            return ('A',5)
413        if self.total_score >= 60:
414            return ('B',4)
415        if self.total_score >= 50:
416            return ('C',3)
417        if self.total_score >= 45:
418            return ('D',2)
419        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
420            return ('E',1)
421        return ('F',0)
422
423    @property
[14136]424    def total_score(self):
[15228]425        """Returns ca + score or imported total score.
[14136]426        """
[15226]427        # Override total_score if value has been imported
428        if getattr(self, 'imported_ts', None):
429            return self.imported_ts
[14532]430        if self.score == -1:
431            return 0
[14136]432        if not None in (self.score, self.ca):
433            return self.score + self.ca
[14532]434        return None
[14136]435
[14288]436    @property
437    def editable_by_lecturer(self):
438        """True if lecturer is allowed to edit the ticket.
439        """
[15412]440        try:
441            cas = grok.getSite()[
442                'configuration'].current_academic_session
443            if self.student.state in (VALIDATED, REGISTERED) and \
444                self.student.current_session == cas:
445                return True
446        except (AttributeError, TypeError): # in unit tests
447            pass
448        return False
[14288]449
[8326]450CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
451
452class CustomCourseTicketFactory(CourseTicketFactory):
453    """A factory for student study levels.
454    """
455
456    def __call__(self, *args, **kw):
457        return CustomCourseTicket()
458
459    def getInterfaces(self):
460        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.