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

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

Show imported total scores in bold.

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