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

Last change on this file since 14557 was 14537, checked in by Henrik Bettermann, 8 years ago

Students at final year level are marked NER if their credits taken fall below 30.

  • Property svn:keywords set to Id
File size: 15.3 KB
RevLine 
[8326]1## $Id: studylevel.py 14537 2017-02-13 06:14:08Z 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)
[14248]37from waeup.aaue.interfaces import MessageFactory as _
[8326]38
39
40class CustomStudentStudyLevel(StudentStudyLevel):
41    """This is a container for course tickets.
42    """
43    grok.implements(ICustomStudentStudyLevel, IStudentNavigation)
44    grok.provides(ICustomStudentStudyLevel)
45
[9914]46    @property
47    def total_credits_s1(self):
48        total = 0
49        for ticket in self.values():
50            if ticket.semester == 1:
51                total += ticket.credits
52        return total
53
54    @property
55    def total_credits_s2(self):
56        total = 0
57        for ticket in self.values():
58            if ticket.semester == 2:
59                total += ticket.credits
60        return total
61
[10443]62    @property
[13834]63    def gpa_params(self):
64        """Calculate gpa parameters for this level.
65        """
66        credits_weighted = 0.0
67        credits_counted = 0
68        level_gpa = 0.0
69        for ticket in self.values():
[14532]70            if ticket.total_score is not None:
[13834]71                credits_counted += ticket.credits
72                credits_weighted += ticket.credits * ticket.weight
73        if credits_counted:
[14384]74            level_gpa = credits_weighted/credits_counted
[14206]75        # Override level_gpa if value has been imported
76        imported_gpa = getattr(self, 'imported_gpa', None)
77        if imported_gpa:
78            level_gpa = imported_gpa
[13834]79        return level_gpa, credits_counted, credits_weighted
80
81    @property
[10480]82    def gpa_params_rectified(self):
83        return self.gpa_params
[10443]84
[13036]85    @property
[14357]86    def passed_params(self):
87        """Determine the number and credits of passed and failed courses.
88        This method is used for level reports.
89        """
90        passed = failed = 0
91        courses_failed = ''
92        credits_failed = 0
93        credits_passed = 0
[14393]94        courses_not_taken = ''
[14357]95        for ticket in self.values():
[14532]96            if ticket.total_score is not None:
[14357]97                if ticket.total_score < ticket.passmark:
98                    failed += 1
99                    credits_failed += ticket.credits
100                    if ticket.mandatory:
101                        courses_failed += 'm_%s_m ' % ticket.code
102                    else:
103                        courses_failed += '%s ' % ticket.code
104                else:
105                    passed += 1
106                    credits_passed += ticket.credits
[14370]107            else:
[14393]108                courses_not_taken += '%s ' % ticket.code
[14388]109        if not len(courses_failed):
[14506]110            courses_failed = 'Nil'
[14428]111        if not len(courses_not_taken):
[14506]112            courses_not_taken = 'Nil'
[14370]113        return (passed, failed, credits_passed,
114                credits_failed, courses_failed,
[14393]115                courses_not_taken)
[14357]116
117    @property
[14248]118    def course_registration_forbidden(self):
[14501]119        #fac_dep_paid = True
120        #if self.student.entry_session >= 2016:
121        #    fac_dep_paid = False
122        #    for ticket in self.student['payments'].values():
123        #        if ticket.p_category == 'fac_dep' and \
124        #            ticket.p_session == self.level_session and \
125        #            ticket.p_state == 'paid':
126        #                fac_dep_paid = True
127        #                continue
128        #if not fac_dep_paid:
129        #    return _("Please pay faculty and departmental dues first.")
[14509]130
131
132        ######################################################
133        # Temporarily disable ug_ft course registration
134        if self.student.current_mode == 'ug_ft':
135            return _("Please check back later, course registration is not yet available.")
136        ######################################################
137
138
139
[14500]140        restitution_paid = True
141        if self.student.entry_session < 2016 \
142            and self.student.current_mode == 'ug_ft':
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)
[14380]188        failed_limit = 1.5
189        if self.student.entry_session < 2013:
190            failed_limit = 1.0
[14377]191        # final level student remark
192        if end_level and self.level >= end_level:
[14464]193            if self.level > end_level:
194                # spill-over level
195                if self.gpa_params[1] == 0:
196                    # no credits taken
[14487]197                    return 'NER'
[14537]198            elif self.gpa_params[1] < 30:
199                # credits taken below limit
200                return 'NER'
[14487]201            if self.level_verdict in ('FRNS', 'NER', 'NYV'):
[14415]202                return self.level_verdict
[14412]203            if '_m' in self.passed_params[4]:
[14161]204                return 'FRNS'
[14412]205            if not self.cumulative_params[0]:
206                return 'FRNS'
[14435]207            if len(self.passed_params[5]) \
[14506]208                and not self.passed_params[5] == 'Nil':
[14412]209                return 'FRNS'
[14380]210            if self.cumulative_params[0] < failed_limit:
[14161]211                return 'Fail'
[14463]212            if self.cumulative_params[0] < 1.5:
213                return 'Pass'
[14161]214            if self.cumulative_params[0] < 2.4:
[14327]215                return '3s_rd_s'
[14161]216            if self.cumulative_params[0] < 3.5:
[14327]217                return '2s_2_s'
[14161]218            if self.cumulative_params[0] < 4.5:
[14327]219                return '2s_1_s'
[14161]220            if self.cumulative_params[0] < 5.1:
[14327]221                return '1s_st_s'
[14161]222            return 'N/A'
223        # returning student remark
[14487]224        if self.level_verdict in ('FRNS', 'NER', 'NYV'):
[14475]225            return 'Probation'
226        if self.level_verdict == 'D':
227            return 'Withdrawn'
[14444]228        if self.gpa_params[1] < 30:
[14464]229            # credits taken below limit
[14415]230            return 'Probation'
[14380]231        if self.cumulative_params[0] < failed_limit:
[14082]232            return 'Probation'
233        if self.cumulative_params[0] < 5.1:
234            return 'Proceed'
235        return 'N/A'
236
[14227]237    def _schoolfeePaymentMade(self):
238        if len(self.student['payments']):
239            for ticket in self.student['payments'].values():
240                if ticket.p_state == 'paid' and \
241                    ticket.p_category in (
242                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
243                    ticket.p_session == self.student[
244                        'studycourse'].current_session:
245                    return True
246        return False
247
[14259]248    def _coursePaymentsMade(self, course):
[14266]249        if self.level_session < 2016:
250            return True
[14259]251        if not course.code[:3] in ('GST', 'ENT'):
252            return True
253        if len(self.student['payments']):
254            paid_cats = list()
255            for pticket in self.student['payments'].values():
256                if pticket.p_state == 'paid':
257                    paid_cats.append(pticket.p_category)
258            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
259                not 'gst_registration_1' in paid_cats:
260                return False
261            if course.code in ('GST222',) and \
262                not 'gst_registration_2' in paid_cats:
263                return False
264            if course.code in ('ENT201',) and \
[14355]265                not 'ent_registration_1' in paid_cats:
[14259]266                return False
[14355]267            if course.code in ('GST101', 'GST102') and \
[14348]268                not 'gst_text_book_1' in paid_cats and \
[14355]269                not 'gst_text_book_0' in paid_cats:
[14259]270                return False
[14355]271            if course.code in ('GST111', 'GST112') and \
[14348]272                not 'gst_text_book_2' in paid_cats and \
[14355]273                not 'gst_text_book_0' in paid_cats:
[14348]274                return False
[14259]275            if course.code in ('GST222',) and \
276                not 'gst_text_book_3' in paid_cats:
277                return False
278            if course.code in ('ENT201',) and \
[14355]279                not 'ent_text_book_1' in paid_cats:
[14259]280                return False
281            return True
282        return False
283
[14075]284    def addCourseTicket(self, ticket, course):
285        """Add a course ticket object.
286        """
287        if not ICourseTicket.providedBy(ticket):
288            raise TypeError(
289                'StudentStudyLeves contain only ICourseTicket instances')
[14227]290        # Raise TicketError if course is in 2nd semester but
291        # schoolfee has not yet been fully paid.
292        if course.semester == 2 and not self._schoolfeePaymentMade():
[14252]293            raise TicketError(
294                _('%s is a 2nd semester course which can only be added '
295                  'if school fees have been fully paid.' % course.code))
[14259]296        # Raise TicketError if registration fee or text
297        # book fee haven't been paid.
298        if not self._coursePaymentsMade(course):
299            raise TicketError(
300                _('%s can only be added if both registration fee and text '
301                  'book fee have been paid.'
302                  % course.code))
[14075]303        ticket.code = course.code
304        ticket.title = course.title
305        ticket.fcode = course.__parent__.__parent__.__parent__.code
306        ticket.dcode = course.__parent__.__parent__.code
307        ticket.credits = course.credits
308        if self.student.entry_session < 2013:
309            ticket.passmark = course.passmark - 5
310        else:
311            ticket.passmark = course.passmark
312        ticket.semester = course.semester
313        self[ticket.code] = ticket
314        return
315
[14227]316    def addCertCourseTickets(self, cert):
317        """Collect all certificate courses and create course
318        tickets automatically.
319        """
320        if cert is not None:
321            for key, val in cert.items():
322                if val.level != self.level:
323                    continue
324                ticket = createObject(u'waeup.CourseTicket')
325                ticket.automatic = True
326                ticket.mandatory = val.mandatory
327                ticket.carry_over = False
328                try:
329                    self.addCourseTicket(ticket, val.course)
330                except TicketError:
331                    pass
332        return
333
[9692]334CustomStudentStudyLevel = attrs_to_fields(
[9914]335    CustomStudentStudyLevel, omit=[
[10480]336    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
[8326]337
338class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
339    """A factory for student study levels.
340    """
341
342    def __call__(self, *args, **kw):
343        return CustomStudentStudyLevel()
344
345    def getInterfaces(self):
346        return implementedBy(CustomStudentStudyLevel)
347
348class CustomCourseTicket(CourseTicket):
349    """This is a course ticket which allows the
350    student to attend the course. Lecturers will enter scores and more at
351    the end of the term.
352
353    A course ticket contains a copy of the original course and
354    course referrer data. If the courses and/or their referrers are removed, the
355    corresponding tickets remain unchanged. So we do not need any event
356    triggered actions on course tickets.
357    """
358    grok.implements(ICustomCourseTicket, IStudentNavigation)
359    grok.provides(ICustomCourseTicket)
360
[13834]361    @property
[14532]362    def _getGradeWeightFromScore(self):
363        """Nigerian Course Grading System
364        """
365        if self.score == -1:
366            return ('-',0) # core course and result not yet available (used by AAUE)
367        if self.total_score is None:
368            return (None, None)
369        if self.total_score >= 70:
370            return ('A',5)
371        if self.total_score >= 60:
372            return ('B',4)
373        if self.total_score >= 50:
374            return ('C',3)
375        if self.total_score >= 45:
376            return ('D',2)
377        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
378            return ('E',1)
379        return ('F',0)
380
381    @property
[14136]382    def total_score(self):
383        """Returns ca + score.
384        """
[14532]385        if self.score == -1:
386            return 0
[14136]387        if not None in (self.score, self.ca):
388            return self.score + self.ca
[14532]389        return None
[14136]390
[14288]391    @property
392    def editable_by_lecturer(self):
393        """True if lecturer is allowed to edit the ticket.
394        """
395        return True
396
[8326]397CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
398
399class CustomCourseTicketFactory(CourseTicketFactory):
400    """A factory for student study levels.
401    """
402
403    def __call__(self, *args, **kw):
404        return CustomCourseTicket()
405
406    def getInterfaces(self):
407        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.