## $Id: studylevel.py 16022 2020-03-04 14:00:11Z henrik $ ## ## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """ Container which holds the data of a student study level and contains the course tickets. """ import grok import pytz from datetime import datetime from zope.component.interfaces import IFactory from zope.component import createObject from zope.interface import implementedBy from waeup.kofa.utils.helpers import attrs_to_fields from waeup.kofa.interfaces import RETURNING, VALIDATED, REGISTERED, PAID from waeup.kofa.students.browser import TicketError from waeup.kofa.students.studylevel import ( StudentStudyLevel, CourseTicket, CourseTicketFactory, StudentStudyLevelFactory) from waeup.kofa.students.interfaces import IStudentNavigation, ICourseTicket from waeup.aaue.students.interfaces import ( ICustomStudentStudyLevel, ICustomCourseTicket) from waeup.aaue.students.utils import MINIMUM_UNITS_THRESHOLD from waeup.aaue.interfaces import MessageFactory as _ class CustomStudentStudyLevel(StudentStudyLevel): """This is a container for course tickets. """ grok.implements(ICustomStudentStudyLevel, IStudentNavigation) grok.provides(ICustomStudentStudyLevel) @property def total_credits_s1(self): total = 0 for ticket in self.values(): if ticket.semester == 1 and not ticket.outstanding: total += ticket.credits return total @property def total_credits_s2(self): total = 0 for ticket in self.values(): if ticket.semester == 2 and not ticket.outstanding: total += ticket.credits return total @property def gpa_params(self): """Calculate gpa parameters for this level. """ credits_weighted = 0.0 credits_counted = 0 level_gpa = 0.0 for ticket in self.values(): if ticket.total_score is not None: credits_counted += ticket.credits credits_weighted += ticket.credits * ticket.weight if credits_counted: level_gpa = credits_weighted/credits_counted # Override level_gpa if value has been imported imported_gpa = getattr(self, 'imported_gpa', None) if imported_gpa: level_gpa = imported_gpa return level_gpa, credits_counted, credits_weighted @property def gpa_params_rectified(self): return self.gpa_params @property def passed_params(self): """Determine the number and credits of passed and failed courses. This method is used for level reports. """ passed = failed = 0 courses_failed = '' credits_failed = 0 credits_passed = 0 courses_not_taken = '' for ticket in self.values(): if ticket.total_score is not None: if ticket.total_score < ticket.passmark: failed += 1 credits_failed += ticket.credits if ticket.mandatory: courses_failed += 'm_%s_m ' % ticket.code else: courses_failed += '%s ' % ticket.code else: passed += 1 credits_passed += ticket.credits else: courses_not_taken += '%s ' % ticket.code if not len(courses_failed): courses_failed = 'Nil' if not len(courses_not_taken): courses_not_taken = 'Nil' return (passed, failed, credits_passed, credits_failed, courses_failed, courses_not_taken) @property def course_registration_forbidden(self): #fac_dep_paid = True #if self.student.entry_session >= 2016: # fac_dep_paid = False # for ticket in self.student['payments'].values(): # if ticket.p_category == 'fac_dep' and \ # ticket.p_session == self.level_session and \ # ticket.p_state == 'paid': # fac_dep_paid = True # continue #if not fac_dep_paid: # return _("Please pay faculty and departmental dues first.") ###################################################### # Temporarily disable ug_ft course registration #if self.student.current_mode == 'ug_ft': # return _("Course registration has been disabled.") ###################################################### restitution_paid = True if self.student.current_session == 2016 \ and self.student.current_mode in ('ug_ft', 'dp_ft') \ and not self.student.is_fresh: restitution_paid = False for ticket in self.student['payments'].values(): if ticket.p_category == 'restitution' and \ ticket.p_session == self.level_session and \ ticket.p_state == 'paid': restitution_paid = True continue if not restitution_paid: return _("Please pay restitution fee first.") #if self.student.is_fresh: # return try: academic_session = grok.getSite()['configuration'][ str(self.level_session)] if self.student.is_postgrad: deadline = academic_session.coursereg_deadline_pg elif self.student.current_mode.startswith('dp'): deadline = academic_session.coursereg_deadline_dp elif self.student.current_mode in ( 'ug_pt', 'de_pt', 'de_dsh', 'ug_dsh'): deadline = academic_session.coursereg_deadline_pt elif self.student.current_mode == 'found': deadline = academic_session.coursereg_deadline_found elif self.student.current_mode == 'bridge': deadline = academic_session.coursereg_deadline_bridge else: deadline = academic_session.coursereg_deadline except (TypeError, KeyError): return if not deadline or deadline > datetime.now(pytz.utc): return if self.student.is_postgrad: lcrfee = academic_session.late_pg_registration_fee else: lcrfee = academic_session.late_registration_fee if not lcrfee: return _("Course registration has been disabled.") if len(self.student['payments']): for ticket in self.student['payments'].values(): if ticket.p_category == 'late_registration' and \ ticket.p_session == self.level_session and \ ticket.p_state == 'paid': return return _("Course registration has ended. " "Please pay the late registration fee.") # only AAUE @property def remark(self): certificate = getattr(self.__parent__,'certificate',None) end_level = getattr(certificate, 'end_level', None) study_mode = getattr(certificate, 'study_mode', None) is_dp = False if study_mode and study_mode.startswith('dp'): is_dp = True failed_limit = 1.5 if self.student.entry_session < 2013: failed_limit = 1.0 # final level student remark if end_level and self.level >= end_level: if self.level > end_level: # spill-over level if self.gpa_params[1] == 0: # no credits taken return 'NER' elif self.gpa_params[1] < MINIMUM_UNITS_THRESHOLD: # credits taken below limit return 'NER' if self.level_verdict in ('FRNS', 'NER', 'NYV'): return self.level_verdict if '_m' in self.passed_params[4]: return 'FRNS' if not self.cumulative_params[0]: return 'FRNS' if len(self.passed_params[5]) \ and not self.passed_params[5] == 'Nil': return 'FRNS' if self.cumulative_params[1] < 60: return 'FRNS' if self.cumulative_params[0] < failed_limit: return 'Fail' dummy, repeat = divmod(self.level, 100) if self.cumulative_params[0] < 5.1 and repeat == 20: # Irrespective of the CGPA of a student, if the He/She has # 3rd Extension, such student will be graduated with a "Pass". return 'Pass' if self.cumulative_params[0] < 1.5: if is_dp: return 'Fail' return 'Pass' if self.cumulative_params[0] < 2.4: if is_dp: return 'Pass' return '3s_rd_s' if self.cumulative_params[0] < 3.5: if is_dp: return 'Merit' return '2s_2_s' if self.cumulative_params[0] < 4.5: if is_dp: return 'Credit' return '2s_1_s' if self.cumulative_params[0] < 5.1: if is_dp: return 'Distinction' return '1s_st_s' return 'N/A' # returning student remark if self.level_verdict in ('FRNS', 'NER', 'NYV'): return 'Probation' if self.level_verdict == 'D': return 'Withdrawn' if self.gpa_params[1] == 0: # no credits taken return 'NER' if self.gpa_params[1] < MINIMUM_UNITS_THRESHOLD: # credits taken below limit return 'Probation' if self.cumulative_params[0] < failed_limit: return 'Probation' if self.cumulative_params[0] < 5.1: return 'Proceed' return 'N/A' def _schoolfeePaymentMade(self): if len(self.student['payments']): for ticket in self.student['payments'].values(): if ticket.p_state == 'paid' and \ ticket.p_category in ( 'schoolfee', 'schoolfee_incl', 'schoolfee_2',) and \ ticket.p_session == self.student[ 'studycourse'].current_session: return True return False def _coursePaymentsMade(self, course): if self.level_session < 2016: return True if not course.code[:3] in ('GST', 'ENT'): return True if len(self.student['payments']): paid_cats = list() for pticket in self.student['payments'].values(): if pticket.p_state == 'paid': paid_cats.append(pticket.p_category) if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \ not 'gst_registration_1' in paid_cats: return False if course.code in ('GST222',) and \ not 'gst_registration_2' in paid_cats: return False #if course.code in ('ENT201',) and \ # not 'ent_registration_1' in paid_cats: # return False if course.code in ('GST101', 'GST102') and \ not 'gst_text_book_1' in paid_cats and \ not 'gst_text_book_0' in paid_cats: return False if course.code in ('GST111', 'GST112') and \ not 'gst_text_book_2' in paid_cats and \ not 'gst_text_book_0' in paid_cats: return False if course.code in ('GST222',) and \ not 'gst_text_book_3' in paid_cats: return False #if course.code in ('ENT201',) and \ # not 'ent_text_book_1' in paid_cats: # return False return True return False def addCourseTicket(self, ticket, course): """Add a course ticket object. """ if not ICourseTicket.providedBy(ticket): raise TypeError( 'StudentStudyLeves contain only ICourseTicket instances') # Raise TicketError if course is in 2nd semester but # schoolfee has not yet been fully paid. if course.semester == 2 and not self._schoolfeePaymentMade(): raise TicketError( _('%s is a 2nd semester course which can only be added ' 'if school fees have been fully paid.' % course.code)) # Raise TicketError if registration fee or text # book fee haven't been paid. if not self._coursePaymentsMade(course): raise TicketError( _('%s can only be added if both registration fee and text ' 'book fee have been paid.' % course.code)) # We check if there exists a certificate course in the certificate # container which refers to the course. If such an object does # not exist, students and managers will be prevented from registering # the corresponding course. cert = self.__parent__.certificate ticket_allowed = False for val in cert.values(): if val.course == course: ticket_allowed = True break if not ticket_allowed: raise TicketError( _('%s is not part of the %s curriculum.' % (course.code, cert.code))) ticket.code = course.code ticket.title = course.title ticket.fcode = course.__parent__.__parent__.__parent__.code ticket.dcode = course.__parent__.__parent__.code ticket.credits = course.credits if self.student.entry_session < 2013: ticket.passmark = course.passmark - 5 else: ticket.passmark = course.passmark ticket.semester = course.semester self[ticket.code] = ticket return def addCertCourseTickets(self, cert): """Collect all certificate courses and create course tickets automatically. """ if cert is not None: for key, val in cert.items(): if val.level != self.level: continue ticket = createObject(u'waeup.CourseTicket') ticket.automatic = True ticket.mandatory = val.mandatory ticket.carry_over = False ticket.course_category = val.course_category try: self.addCourseTicket(ticket, val.course) except TicketError: pass return CustomStudentStudyLevel = attrs_to_fields( CustomStudentStudyLevel, omit=[ 'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa']) class CustomStudentStudyLevelFactory(StudentStudyLevelFactory): """A factory for student study levels. """ def __call__(self, *args, **kw): return CustomStudentStudyLevel() def getInterfaces(self): return implementedBy(CustomStudentStudyLevel) class CustomCourseTicket(CourseTicket): """This is a course ticket which allows the student to attend the course. Lecturers will enter scores and more at the end of the term. A course ticket contains a copy of the original course and course referrer data. If the courses and/or their referrers are removed, the corresponding tickets remain unchanged. So we do not need any event triggered actions on course tickets. """ grok.implements(ICustomCourseTicket, IStudentNavigation) grok.provides(ICustomCourseTicket) @property def _getGradeWeightFromScore(self): """Nigerian Course Grading System """ if self.score == -1: return ('-',0) # core course and result not yet available (used by AAUE) if self.total_score is None: return (None, None) if self.total_score >= 70: return ('A',5) if self.total_score >= 60: return ('B',4) if self.total_score >= 50: return ('C',3) if self.total_score >= 45: return ('D',2) if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45 return ('E',1) return ('F',0) @property def total_score(self): """Returns ca + score or imported total score. """ # Override total_score if value has been imported if getattr(self, 'imported_ts', None): return self.imported_ts if self.score == -1: return 0 if not None in (self.score, self.ca): return self.score + self.ca return None @property def removable_by_student(self): """True if student is allowed to remove the ticket. """ return not self.mandatory and not self.score @property def editable_by_lecturer(self): """True if lecturer is allowed to edit the ticket. """ try: cas = grok.getSite()[ 'configuration'].current_academic_session # Temporarily we allow students to pay for next session, so their # current_session might have increased if self.student.state in ( VALIDATED, REGISTERED, PAID, RETURNING) and \ self.student.current_session in (cas, cas+1): return True except (AttributeError, TypeError): # in unit tests pass return False CustomCourseTicket = attrs_to_fields(CustomCourseTicket) class CustomCourseTicketFactory(CourseTicketFactory): """A factory for student study levels. """ def __call__(self, *args, **kw): return CustomCourseTicket() def getInterfaces(self): return implementedBy(CustomCourseTicket)