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

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

Bugfix

  • Property svn:keywords set to Id
File size: 16.4 KB
RevLine 
[8326]1## $Id: studylevel.py 15090 2018-07-28 05:03:54Z 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
[13036]29from waeup.kofa.interfaces import CREATED
[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        ######################################################
134        # Temporarily disable ug_ft course registration
[14613]135        #if self.student.current_mode == 'ug_ft':
136        #    return _("Please check back later, course registration is not yet available.")
[14509]137        ######################################################
138
139
[14500]140        restitution_paid = True
141        if self.student.entry_session < 2016 \
[14660]142            and self.student.current_mode in ('ug_ft', 'dp_ft'):
[14500]143            restitution_paid = False
144            for ticket in self.student['payments'].values():
145                if ticket.p_category == 'restitution' and \
146                    ticket.p_session == self.level_session and \
147                    ticket.p_state == 'paid':
148                        restitution_paid = True
149                        continue
150        if not restitution_paid:
151            return _("Please pay restitution fee first.")
[13036]152        if self.student.is_fresh:
[14248]153            return
[13036]154        try:
[14117]155            if self.student.is_postgrad:
156                deadline = grok.getSite()['configuration'][
157                        str(self.level_session)].coursereg_deadline_pg
158            elif self.student.current_mode.startswith('dp'):
159                deadline = grok.getSite()['configuration'][
160                        str(self.level_session)].coursereg_deadline_dp
161            elif self.student.current_mode.endswith('_pt'):
162                deadline = grok.getSite()['configuration'][
163                        str(self.level_session)].coursereg_deadline_pt
164            elif self.student.current_mode == 'found':
165                deadline = grok.getSite()['configuration'][
166                        str(self.level_session)].coursereg_deadline_found
167            else:
168                deadline = grok.getSite()['configuration'][
169                        str(self.level_session)].coursereg_deadline
[13071]170        except (TypeError, KeyError):
[14248]171            return
[13070]172        if not deadline or deadline > datetime.now(pytz.utc):
[14248]173            return
[13036]174        if len(self.student['payments']):
175            for ticket in self.student['payments'].values():
176                if ticket.p_category == 'late_registration' and \
177                    ticket.p_session == self.level_session and \
178                    ticket.p_state == 'paid':
[14248]179                        return
180        return _("Course registration has ended. "
181                 "Please pay the late registration fee.")
[13036]182
[14082]183    # only AAUE
184    @property
185    def remark(self):
[14161]186        certificate = getattr(self.__parent__,'certificate',None)
187        end_level = getattr(certificate, 'end_level', None)
[14918]188        study_mode = getattr(certificate, 'study_mode', None)
189        is_dp = False
190        if study_mode and study_mode.startswith('dp'):
191            is_dp = True
[14380]192        failed_limit = 1.5
193        if self.student.entry_session < 2013:
194            failed_limit = 1.0
[14377]195        # final level student remark
196        if end_level and self.level >= end_level:
[14464]197            if self.level > end_level:
198                # spill-over level
199                if self.gpa_params[1] == 0:
200                    # no credits taken
[14487]201                    return 'NER'
[14663]202            elif self.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
[14537]203                # credits taken below limit
204                return 'NER'
[14487]205            if self.level_verdict in ('FRNS', 'NER', 'NYV'):
[14415]206                return self.level_verdict
[14412]207            if '_m' in self.passed_params[4]:
[14161]208                return 'FRNS'
[14412]209            if not self.cumulative_params[0]:
210                return 'FRNS'
[14435]211            if len(self.passed_params[5]) \
[14506]212                and not self.passed_params[5] == 'Nil':
[14412]213                return 'FRNS'
[14955]214            if self.cumulative_params[1] < 60:
[14623]215                return 'FRNS'
[14380]216            if self.cumulative_params[0] < failed_limit:
[14161]217                return 'Fail'
[15054]218            dummy, repeat = divmod(self.level, 100)
[15090]219            if self.cumulative_params[0] < 5.1 and repeat == 20:
[15054]220                # Irrespective of the CGPA of a student, if the He/She has
221                # 3rd Extension, such student will be graduated with a "Pass".
222                return 'Pass'
[14463]223            if self.cumulative_params[0] < 1.5:
[14918]224                if is_dp:
225                    return 'Fail'
[14463]226                return 'Pass'
[14161]227            if self.cumulative_params[0] < 2.4:
[14918]228                if is_dp:
229                    return 'Pass'
[14327]230                return '3s_rd_s'
[14161]231            if self.cumulative_params[0] < 3.5:
[14918]232                if is_dp:
233                    return 'Merit'
[14327]234                return '2s_2_s'
[14161]235            if self.cumulative_params[0] < 4.5:
[14918]236                if is_dp:
237                    return 'Credit'
[14327]238                return '2s_1_s'
[14161]239            if self.cumulative_params[0] < 5.1:
[14918]240                if is_dp:
241                    return 'Distinction'
[14327]242                return '1s_st_s'
[14161]243            return 'N/A'
244        # returning student remark
[14487]245        if self.level_verdict in ('FRNS', 'NER', 'NYV'):
[14475]246            return 'Probation'
247        if self.level_verdict == 'D':
248            return 'Withdrawn'
[14662]249        if self.gpa_params[1] == 0:
250            # no credits taken
251            return 'NER'
[14663]252        if self.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
[14464]253            # credits taken below limit
[14415]254            return 'Probation'
[14380]255        if self.cumulative_params[0] < failed_limit:
[14082]256            return 'Probation'
257        if self.cumulative_params[0] < 5.1:
258            return 'Proceed'
259        return 'N/A'
260
[14227]261    def _schoolfeePaymentMade(self):
262        if len(self.student['payments']):
263            for ticket in self.student['payments'].values():
264                if ticket.p_state == 'paid' and \
265                    ticket.p_category in (
266                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
267                    ticket.p_session == self.student[
268                        'studycourse'].current_session:
269                    return True
270        return False
271
[14259]272    def _coursePaymentsMade(self, course):
[14266]273        if self.level_session < 2016:
274            return True
[14259]275        if not course.code[:3] in ('GST', 'ENT'):
276            return True
277        if len(self.student['payments']):
278            paid_cats = list()
279            for pticket in self.student['payments'].values():
280                if pticket.p_state == 'paid':
281                    paid_cats.append(pticket.p_category)
282            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
283                not 'gst_registration_1' in paid_cats:
284                return False
285            if course.code in ('GST222',) and \
286                not 'gst_registration_2' in paid_cats:
287                return False
[14675]288            #if course.code in ('ENT201',) and \
289            #    not 'ent_registration_1' in paid_cats:
290            #    return False
[14355]291            if course.code in ('GST101', 'GST102') and \
[14348]292                not 'gst_text_book_1' in paid_cats and \
[14355]293                not 'gst_text_book_0' in paid_cats:
[14259]294                return False
[14355]295            if course.code in ('GST111', 'GST112') and \
[14348]296                not 'gst_text_book_2' in paid_cats and \
[14355]297                not 'gst_text_book_0' in paid_cats:
[14348]298                return False
[14259]299            if course.code in ('GST222',) and \
300                not 'gst_text_book_3' in paid_cats:
301                return False
[14675]302            #if course.code in ('ENT201',) and \
303            #    not 'ent_text_book_1' in paid_cats:
304            #    return False
[14259]305            return True
306        return False
307
[14075]308    def addCourseTicket(self, ticket, course):
309        """Add a course ticket object.
310        """
311        if not ICourseTicket.providedBy(ticket):
312            raise TypeError(
313                'StudentStudyLeves contain only ICourseTicket instances')
[14227]314        # Raise TicketError if course is in 2nd semester but
315        # schoolfee has not yet been fully paid.
316        if course.semester == 2 and not self._schoolfeePaymentMade():
[14252]317            raise TicketError(
318                _('%s is a 2nd semester course which can only be added '
319                  'if school fees have been fully paid.' % course.code))
[14259]320        # Raise TicketError if registration fee or text
321        # book fee haven't been paid.
322        if not self._coursePaymentsMade(course):
323            raise TicketError(
324                _('%s can only be added if both registration fee and text '
325                  'book fee have been paid.'
326                  % course.code))
[14075]327        ticket.code = course.code
328        ticket.title = course.title
329        ticket.fcode = course.__parent__.__parent__.__parent__.code
330        ticket.dcode = course.__parent__.__parent__.code
331        ticket.credits = course.credits
332        if self.student.entry_session < 2013:
333            ticket.passmark = course.passmark - 5
334        else:
335            ticket.passmark = course.passmark
336        ticket.semester = course.semester
337        self[ticket.code] = ticket
338        return
339
[14227]340    def addCertCourseTickets(self, cert):
341        """Collect all certificate courses and create course
342        tickets automatically.
343        """
344        if cert is not None:
345            for key, val in cert.items():
346                if val.level != self.level:
347                    continue
348                ticket = createObject(u'waeup.CourseTicket')
349                ticket.automatic = True
350                ticket.mandatory = val.mandatory
351                ticket.carry_over = False
352                try:
353                    self.addCourseTicket(ticket, val.course)
354                except TicketError:
355                    pass
356        return
357
[9692]358CustomStudentStudyLevel = attrs_to_fields(
[9914]359    CustomStudentStudyLevel, omit=[
[10480]360    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
[8326]361
362class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
363    """A factory for student study levels.
364    """
365
366    def __call__(self, *args, **kw):
367        return CustomStudentStudyLevel()
368
369    def getInterfaces(self):
370        return implementedBy(CustomStudentStudyLevel)
371
372class CustomCourseTicket(CourseTicket):
373    """This is a course ticket which allows the
374    student to attend the course. Lecturers will enter scores and more at
375    the end of the term.
376
377    A course ticket contains a copy of the original course and
378    course referrer data. If the courses and/or their referrers are removed, the
379    corresponding tickets remain unchanged. So we do not need any event
380    triggered actions on course tickets.
381    """
382    grok.implements(ICustomCourseTicket, IStudentNavigation)
383    grok.provides(ICustomCourseTicket)
384
[13834]385    @property
[14532]386    def _getGradeWeightFromScore(self):
387        """Nigerian Course Grading System
388        """
389        if self.score == -1:
390            return ('-',0) # core course and result not yet available (used by AAUE)
391        if self.total_score is None:
392            return (None, None)
393        if self.total_score >= 70:
394            return ('A',5)
395        if self.total_score >= 60:
396            return ('B',4)
397        if self.total_score >= 50:
398            return ('C',3)
399        if self.total_score >= 45:
400            return ('D',2)
401        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
402            return ('E',1)
403        return ('F',0)
404
405    @property
[14136]406    def total_score(self):
407        """Returns ca + score.
408        """
[14532]409        if self.score == -1:
410            return 0
[14136]411        if not None in (self.score, self.ca):
412            return self.score + self.ca
[14532]413        return None
[14136]414
[14288]415    @property
416    def editable_by_lecturer(self):
417        """True if lecturer is allowed to edit the ticket.
418        """
419        return True
420
[8326]421CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
422
423class CustomCourseTicketFactory(CourseTicketFactory):
424    """A factory for student study levels.
425    """
426
427    def __call__(self, *args, **kw):
428        return CustomCourseTicket()
429
430    def getInterfaces(self):
431        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.