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

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

Disable ug_ft returning course registration.

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