source: main/waeup.kofa/trunk/src/waeup/kofa/students/studylevel.py @ 14352

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

Use old name 'score' instead of 'total'.

  • Property svn:keywords set to Id
File size: 13.5 KB
RevLine 
[7191]1## $Id: studylevel.py 14351 2016-12-16 11:49:01Z henrik $
2##
[6775]3## Copyright (C) 2011 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
[13031]23import pytz
24from datetime import datetime
[7536]25from zope.component.interfaces import IFactory
[10539]26from zope.catalog.interfaces import ICatalog
27from zope.component import createObject, queryUtility
[8330]28from zope.interface import implementedBy
[10631]29from waeup.kofa.interfaces import academic_sessions_vocab, VALIDATED
[7811]30from waeup.kofa.students.interfaces import (
[6781]31    IStudentStudyLevel, IStudentNavigation, ICourseTicket)
[7811]32from waeup.kofa.utils.helpers import attrs_to_fields
33from waeup.kofa.students.vocabularies import StudyLevelSource
[14247]34from waeup.kofa.interfaces import MessageFactory as _
[6775]35
[10276]36def find_carry_over(ticket):
[10277]37    studylevel = ticket.__parent__
38    studycourse = ticket.__parent__.__parent__
39    levels = sorted(studycourse.keys())
40    index = levels.index(str(studylevel.level))
41    try:
42        next_level = levels[index+1]
43    except IndexError:
44        return None
45    next_studylevel = studycourse[next_level]
46    co_ticket = next_studylevel.get(ticket.code, None)
47    return co_ticket
[10276]48
[14351]49def getGradeWeightFromScore(score, student):
[14350]50    """Nigerian Course Grading System
51    """
[14351]52    if score is None:
[9684]53        return (None, None)
[14351]54    if score >= 70:
[9684]55        return ('A',5)
[14351]56    if score >= 60:
[9684]57        return ('B',4)
[14351]58    if score >= 50:
[9684]59        return ('C',3)
[14351]60    if score >= 45:
[9684]61        return ('D',2)
[14351]62    if score >= 40 and student.entry_session < 2013:
[9684]63        return ('E',1)
64    return ('F',0)
65
[6775]66class StudentStudyLevel(grok.Container):
67    """This is a container for course tickets.
68    """
69    grok.implements(IStudentStudyLevel, IStudentNavigation)
70    grok.provides(IStudentStudyLevel)
71
72    def __init__(self):
73        super(StudentStudyLevel, self).__init__()
74        return
75
[8736]76    @property
77    def student(self):
78        try:
79            return self.__parent__.__parent__
80        except AttributeError:
81            return None
[6775]82
[9235]83    @property
[9253]84    def certcode(self):
85        try:
86            return self.__parent__.certificate.code
87        except AttributeError:
88            return None
89
90    @property
[9235]91    def number_of_tickets(self):
92        return len(self)
93
[9257]94    @property
[9532]95    def total_credits(self):
96        total = 0
97        for ticket in self.values():
98            total += ticket.credits
99        return total
100
101    @property
[9912]102    def getSessionString(self):
[13006]103        try:
104            session_string = academic_sessions_vocab.getTerm(
105                self.level_session).title
106        except LookupError:
107            return None
108        return session_string
[9912]109
110    @property
[10479]111    def gpa_params_rectified(self):
[13002]112        """Calculate corrected level (sessional) gpa parameters.
[10598]113        The corrected gpa is displayed on transcripts only.
114        """
115        credits_weighted = 0.0
[9687]116        credits_counted = 0
[10276]117        level_gpa = 0.0
[9687]118        for ticket in self.values():
[14133]119            if ticket.carry_over is False and ticket.total_score:
120                if ticket.total_score < ticket.passmark:
[10276]121                    co_ticket = find_carry_over(ticket)
[10313]122                    if co_ticket is not None and co_ticket.weight is not None:
[10276]123                        credits_counted += co_ticket.credits
[10598]124                        credits_weighted += co_ticket.credits * co_ticket.weight
[10276]125                    continue
[9687]126                credits_counted += ticket.credits
[10598]127                credits_weighted += ticket.credits * ticket.weight
[9687]128        if credits_counted:
[10598]129            level_gpa = round(credits_weighted/credits_counted, 3)
130        return level_gpa, credits_counted, credits_weighted
[9687]131
132    @property
[10479]133    def gpa_params(self):
[10598]134        """Calculate gpa parameters for this level.
135        """
136        credits_weighted = 0.0
[10479]137        credits_counted = 0
138        level_gpa = 0.0
139        for ticket in self.values():
[14133]140            if ticket.total_score is not None:
[10479]141                credits_counted += ticket.credits
[10598]142                credits_weighted += ticket.credits * ticket.weight
[10479]143        if credits_counted:
[10598]144            level_gpa = round(credits_weighted/credits_counted, 3)
[14200]145        # Override level_gpa if value has been imported
146        # (not implemented in base package)
147        imported_gpa = getattr(self, 'imported_gpa', None)
148        if imported_gpa:
149            level_gpa = imported_gpa
[10598]150        return level_gpa, credits_counted, credits_weighted
[10479]151
152    @property
153    def gpa(self):
154        return self.gpa_params[0]
155
156    @property
[10553]157    def passed_params(self):
[10598]158        """Determine the number and credits of passed and failed courses.
159        This method is used for level reports.
160        """
[10553]161        passed = failed = 0
[13868]162        courses_failed = ''
[10616]163        credits_failed = 0
164        credits_passed = 0
[10553]165        for ticket in self.values():
[14133]166            if ticket.total_score is not None:
167                if ticket.total_score < ticket.passmark:
[10553]168                    failed += 1
[10616]169                    credits_failed += ticket.credits
[14123]170                    if ticket.mandatory:
171                        courses_failed += 'm_%s_m ' % ticket.code
172                    else:
173                        courses_failed += '%s ' % ticket.code
[10553]174                else:
175                    passed += 1
[10616]176                    credits_passed += ticket.credits
177        return passed, failed, credits_passed, credits_failed, courses_failed
[10553]178
179    @property
[10598]180    def cumulative_params(self):
181        """Calculate the cumulative gpa and other cumulative parameters
182        for this level.
183        All levels below this level are taken under consideration
[14154]184        (including repeating levels).
185        This method is used for level reports and meanwhile also
186        for session results presentations.
[10598]187        """
[10616]188        credits_passed = 0
[10598]189        total_credits = 0
190        total_credits_counted = 0
191        total_credits_weighted = 0
[10618]192        cgpa = 0.0
[13002]193        if self.__parent__:
194            for level in self.__parent__.values():
195                if level.level > self.level:
196                    continue
197                credits_passed += level.passed_params[2]
198                total_credits += level.total_credits
199                gpa_params = level.gpa_params
200                total_credits_counted += gpa_params[1]
201                total_credits_weighted += gpa_params[2]
202            if total_credits_counted:
203                cgpa = round(total_credits_weighted / total_credits_counted, 3)
[14200]204            # Override cgpa if value has been imported
205            # (not implemented in base package)
206            imported_cgpa = getattr(self, 'imported_cgpa', None)
207            if imported_cgpa:
208                cgpa = imported_cgpa
[10598]209        return (cgpa, total_credits_counted, total_credits_weighted,
[10616]210               total_credits, credits_passed)
[10598]211
212    @property
[9257]213    def is_current_level(self):
214        try:
215            return self.__parent__.current_level == self.level
216        except AttributeError:
217            return False
218
[8735]219    def writeLogMessage(self, view, message):
220        return self.__parent__.__parent__.writeLogMessage(view, message)
221
[6775]222    @property
223    def level_title(self):
224        studylevelsource = StudyLevelSource()
225        return studylevelsource.factory.getTitle(self.__parent__, self.level)
226
[13031]227    @property
[14247]228    def course_registration_forbidden(self):
[13031]229        try:
230            deadline = grok.getSite()['configuration'][
[13057]231                str(self.level_session)].coursereg_deadline
232        except (TypeError, KeyError):
[14247]233            return
[13069]234        if not deadline or deadline > datetime.now(pytz.utc):
[14247]235            return
[13031]236        if len(self.student['payments']):
237            for ticket in self.student['payments'].values():
238                if ticket.p_category == 'late_registration' and \
239                    ticket.p_session == self.level_session and \
240                    ticket.p_state == 'paid':
[14247]241                        return
242        return _("Course registration has ended. "
243                 "Please pay the late registration fee.")
[13031]244
[8920]245    def addCourseTicket(self, ticket, course):
[6781]246        """Add a course ticket object.
247        """
[8920]248        if not ICourseTicket.providedBy(ticket):
[6781]249            raise TypeError(
250                'StudentStudyLeves contain only ICourseTicket instances')
[8920]251        ticket.code = course.code
252        ticket.title = course.title
253        ticket.fcode = course.__parent__.__parent__.__parent__.code
254        ticket.dcode = course.__parent__.__parent__.code
255        ticket.credits = course.credits
256        ticket.passmark = course.passmark
257        ticket.semester = course.semester
258        self[ticket.code] = ticket
[6781]259        return
260
[9501]261    def addCertCourseTickets(self, cert):
262        """Collect all certificate courses and create course
263        tickets automatically.
264        """
265        if cert is not None:
266            for key, val in cert.items():
267                if val.level != self.level:
268                    continue
269                ticket = createObject(u'waeup.CourseTicket')
270                ticket.automatic = True
271                ticket.mandatory = val.mandatory
272                ticket.carry_over = False
273                self.addCourseTicket(ticket, val.course)
274        return
275
[9690]276StudentStudyLevel = attrs_to_fields(
[10479]277    StudentStudyLevel, omit=['total_credits', 'gpa'])
[6781]278
[7536]279class StudentStudyLevelFactory(grok.GlobalUtility):
280    """A factory for student study levels.
281    """
282    grok.implements(IFactory)
283    grok.name(u'waeup.StudentStudyLevel')
284    title = u"Create a new student study level.",
285    description = u"This factory instantiates new student study level instances."
286
287    def __call__(self, *args, **kw):
288        return StudentStudyLevel()
289
290    def getInterfaces(self):
291        return implementedBy(StudentStudyLevel)
292
[6781]293class CourseTicket(grok.Model):
294    """This is a course ticket which allows the
295    student to attend the course. Lecturers will enter scores and more at
296    the end of the term.
[6783]297
298    A course ticket contains a copy of the original course and
[12882]299    certificate course data. If the courses and/or the referring certificate
[8920]300    courses are removed, the corresponding tickets remain unchanged.
[12882]301    So we do not need any event triggered actions on course tickets.
[6781]302    """
303    grok.implements(ICourseTicket, IStudentNavigation)
304    grok.provides(ICourseTicket)
305
[6795]306    def __init__(self):
[6781]307        super(CourseTicket, self).__init__()
[6795]308        self.code = None
[6781]309        return
310
[8736]311    @property
312    def student(self):
[8338]313        """Get the associated student object.
314        """
315        try:
316            return self.__parent__.__parent__.__parent__
[13003]317        except AttributeError: # in unit tests
[8338]318            return None
[6781]319
[9253]320    @property
321    def certcode(self):
322        try:
323            return self.__parent__.__parent__.certificate.code
[13003]324        except AttributeError: # in unit tests
[9253]325            return None
326
[9698]327    @property
328    def removable_by_student(self):
[13046]329        """True if student is allowed to remove the ticket.
330        """
[9698]331        return not self.mandatory
332
[10631]333    @property
334    def editable_by_lecturer(self):
[13046]335        """True if lecturer is allowed to edit the ticket.
336        """
[13003]337        try:
[13031]338            cas = grok.getSite()[
339                'configuration'].current_academic_session
340            if self.student.state == VALIDATED and \
341                self.student.current_session == cas:
[13003]342                return True
[13008]343        except (AttributeError, TypeError): # in unit tests
[13003]344            pass
[10631]345        return False
346
[8735]347    def writeLogMessage(self, view, message):
[13031]348        return self.__parent__.__parent__.__parent__.writeLogMessage(
349            view, message)
[8735]350
[9925]351    @property
352    def level(self):
[7633]353        """Returns the id of the level the ticket has been added to.
354        """
[8338]355        try:
356            return self.__parent__.level
[13003]357        except AttributeError: # in unit tests
[8338]358            return None
[7633]359
[9925]360    @property
361    def level_session(self):
[7633]362        """Returns the session of the level the ticket has been added to.
363        """
[8338]364        try:
365            return self.__parent__.level_session
[13003]366        except AttributeError: # in unit tests
[8338]367            return None
[7633]368
[9684]369    @property
[14133]370    def total_score(self):
371        """Returns the total score of this ticket. In the base package
372        this is simply the score. In customized packages this could be
373        something else.
374        """
375        return self.score
376
377    @property
[9684]378    def grade(self):
[14133]379        """Returns the grade calculated from total score.
[9684]380        """
[14350]381        return getGradeWeightFromScore(self.total_score, self.student)[0]
[7633]382
[9684]383    @property
384    def weight(self):
[14133]385        """Returns the weight calculated from total score.
[9684]386        """
[14350]387        return getGradeWeightFromScore(self.total_score, self.student)[1]
[9684]388
[6782]389CourseTicket = attrs_to_fields(CourseTicket)
[7548]390
391class CourseTicketFactory(grok.GlobalUtility):
392    """A factory for student study levels.
393    """
394    grok.implements(IFactory)
395    grok.name(u'waeup.CourseTicket')
396    title = u"Create a new course ticket.",
397    description = u"This factory instantiates new course ticket instances."
398
399    def __call__(self, *args, **kw):
400        return CourseTicket()
401
402    def getInterfaces(self):
403        return implementedBy(CourseTicket)
Note: See TracBrowser for help on using the repository browser.