## $Id: studylevel.py 14288 2016-11-18 07:19:25Z 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 CREATED 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.interfaces import MessageFactory as _ def getGradeWeightFromScore(total, student): if total is None: return (None, None) if total >= 70: return ('A',5) if total >= 60: return ('B',4) if total >= 50: return ('C',3) if total >= 45: return ('D',2) if total >= 40 and student.entry_session < 2013: return ('E',1) return ('F',0) 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: total += ticket.credits return total @property def total_credits_s2(self): total = 0 for ticket in self.values(): if ticket.semester == 2: 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 None not in (ticket.score, ticket.ca): credits_counted += ticket.credits credits_weighted += ticket.credits * ticket.weight if credits_counted: level_gpa = round(credits_weighted/credits_counted, 3) # 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 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.") if self.student.is_fresh: return try: if self.student.is_postgrad: deadline = grok.getSite()['configuration'][ str(self.level_session)].coursereg_deadline_pg elif self.student.current_mode.startswith('dp'): deadline = grok.getSite()['configuration'][ str(self.level_session)].coursereg_deadline_dp elif self.student.current_mode.endswith('_pt'): deadline = grok.getSite()['configuration'][ str(self.level_session)].coursereg_deadline_pt elif self.student.current_mode == 'found': deadline = grok.getSite()['configuration'][ str(self.level_session)].coursereg_deadline_found else: deadline = grok.getSite()['configuration'][ str(self.level_session)].coursereg_deadline except (TypeError, KeyError): return if not deadline or deadline > datetime.now(pytz.utc): return 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) # final student remark if end_level and self.student.current_level >= end_level: failed_courses = self.passed_params[4] if '_m' in failed_courses: return 'FRNS' if self.cumulative_params[0] < 1.5: return 'Fail' if self.cumulative_params[0] < 2.4: return '3rd' if self.cumulative_params[0] < 3.5: return '2nd Lower' if self.cumulative_params[0] < 4.5: return '2nd Upper' if self.cumulative_params[0] < 5.1: return '1st' return 'N/A' # returning student remark if self.cumulative_params[0] < 1.5: 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 'gst_registration_3' in paid_cats: return False if course.code in ('GST101', 'GST102') and \ not 'gst_text_book_1' in paid_cats: return False if course.code in ('GST111', 'GST112') and \ not 'gst_text_book_2' 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 'gst_text_book_4' 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)) 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 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 grade(self): """Returns the grade calculated from score. """ return getGradeWeightFromScore(self.total_score, self.student)[0] @property def weight(self): """Returns the weight calculated from score. """ return getGradeWeightFromScore(self.total_score, self.student)[1] @property def total_score(self): """Returns ca + score. """ if not None in (self.score, self.ca): return self.score + self.ca @property def editable_by_lecturer(self): """True if lecturer is allowed to edit the ticket. """ return True 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)