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

Last change on this file since 12991 was 12882, checked in by Henrik Bettermann, 9 years ago

More docs.

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