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

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

More docs.

  • Property svn:keywords set to Id
File size: 11.5 KB
Line 
1## $Id: studylevel.py 12882 2015-04-24 10:07:26Z henrik $
2##
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
23from zope.component.interfaces import IFactory
24from zope.catalog.interfaces import ICatalog
25from zope.component import createObject, queryUtility
26from zope.interface import implementedBy
27from waeup.kofa.interfaces import academic_sessions_vocab, VALIDATED
28from waeup.kofa.students.interfaces import (
29    IStudentStudyLevel, IStudentNavigation, ICourseTicket)
30from waeup.kofa.utils.helpers import attrs_to_fields
31from waeup.kofa.students.vocabularies import StudyLevelSource
32
33def find_carry_over(ticket):
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
45
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
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
71    @property
72    def student(self):
73        try:
74            return self.__parent__.__parent__
75        except AttributeError:
76            return None
77
78    @property
79    def certcode(self):
80        try:
81            return self.__parent__.certificate.code
82        except AttributeError:
83            return None
84
85    @property
86    def number_of_tickets(self):
87        return len(self)
88
89    @property
90    def total_credits(self):
91        total = 0
92        for ticket in self.values():
93            total += ticket.credits
94        return total
95
96    @property
97    def getSessionString(self):
98        return academic_sessions_vocab.getTerm(
99            self.level_session).title
100
101    @property
102    def gpa_params_rectified(self):
103        """Calculate corrected corrected level (sesional) gpa parameters.
104
105        The corrected gpa is displayed on transcripts only.
106        """
107        credits_weighted = 0.0
108        credits_counted = 0
109        level_gpa = 0.0
110        for ticket in self.values():
111            if ticket.carry_over is False and ticket.score:
112                if ticket.score < ticket.passmark:
113                    co_ticket = find_carry_over(ticket)
114                    if co_ticket is not None and co_ticket.weight is not None:
115                        credits_counted += co_ticket.credits
116                        credits_weighted += co_ticket.credits * co_ticket.weight
117                    continue
118                credits_counted += ticket.credits
119                credits_weighted += ticket.credits * ticket.weight
120        if credits_counted:
121            level_gpa = round(credits_weighted/credits_counted, 3)
122        return level_gpa, credits_counted, credits_weighted
123
124    @property
125    def gpa_params(self):
126        """Calculate gpa parameters for this level.
127        """
128        credits_weighted = 0.0
129        credits_counted = 0
130        level_gpa = 0.0
131        for ticket in self.values():
132            if ticket.score is not None:
133                credits_counted += ticket.credits
134                credits_weighted += ticket.credits * ticket.weight
135        if credits_counted:
136            level_gpa = round(credits_weighted/credits_counted, 3)
137        return level_gpa, credits_counted, credits_weighted
138
139    @property
140    def gpa(self):
141        return self.gpa_params[0]
142
143    @property
144    def passed_params(self):
145        """Determine the number and credits of passed and failed courses.
146
147        This method is used for level reports.
148        """
149        passed = failed = 0
150        courses_failed = []
151        credits_failed = 0
152        credits_passed = 0
153        for ticket in self.values():
154            if ticket.score is not None:
155                if ticket.score < ticket.passmark:
156                    failed += 1
157                    credits_failed += ticket.credits
158                    courses_failed.append(ticket.code)
159                else:
160                    passed += 1
161                    credits_passed += ticket.credits
162        return passed, failed, credits_passed, credits_failed, courses_failed
163
164    @property
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        """
172        credits_passed = 0
173        total_credits = 0
174        total_credits_counted = 0
175        total_credits_weighted = 0
176        cgpa = 0.0
177        for level in self.__parent__.values():
178            if level.level > self.level:
179                continue
180            credits_passed += level.passed_params[2]
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,
188               total_credits, credits_passed)
189
190    @property
191    def is_current_level(self):
192        try:
193            return self.__parent__.current_level == self.level
194        except AttributeError:
195            return False
196
197    def writeLogMessage(self, view, message):
198        return self.__parent__.__parent__.writeLogMessage(view, message)
199
200    @property
201    def level_title(self):
202        studylevelsource = StudyLevelSource()
203        return studylevelsource.factory.getTitle(self.__parent__, self.level)
204
205    def addCourseTicket(self, ticket, course):
206        """Add a course ticket object.
207        """
208        if not ICourseTicket.providedBy(ticket):
209            raise TypeError(
210                'StudentStudyLeves contain only ICourseTicket instances')
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
219        return
220
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
236StudentStudyLevel = attrs_to_fields(
237    StudentStudyLevel, omit=['total_credits', 'gpa'])
238
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
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.
257
258    A course ticket contains a copy of the original course and
259    certificate course data. If the courses and/or the referring certificate
260    courses are removed, the corresponding tickets remain unchanged.
261    So we do not need any event triggered actions on course tickets.
262    """
263    grok.implements(ICourseTicket, IStudentNavigation)
264    grok.provides(ICourseTicket)
265
266    def __init__(self):
267        super(CourseTicket, self).__init__()
268        self.code = None
269        return
270
271    @property
272    def student(self):
273        """Get the associated student object.
274        """
275        try:
276            return self.__parent__.__parent__.__parent__
277        except AttributeError:
278            return None
279
280    @property
281    def certcode(self):
282        try:
283            return self.__parent__.__parent__.certificate.code
284        except AttributeError:
285            return None
286
287    @property
288    def removable_by_student(self):
289        return not self.mandatory
290
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
298    def writeLogMessage(self, view, message):
299        return self.__parent__.__parent__.__parent__.writeLogMessage(view, message)
300
301    @property
302    def level(self):
303        """Returns the id of the level the ticket has been added to.
304        """
305        try:
306            return self.__parent__.level
307        except AttributeError:
308            return None
309
310    @property
311    def level_session(self):
312        """Returns the session of the level the ticket has been added to.
313        """
314        try:
315            return self.__parent__.level_session
316        except AttributeError:
317            return None
318
319    @property
320    def grade(self):
321        """Returns the grade calculated from score.
322        """
323        return getGradeWeightFromScore(self.score)[0]
324
325    @property
326    def weight(self):
327        """Returns the weight calculated from score.
328        """
329        return getGradeWeightFromScore(self.score)[1]
330
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
344CourseTicket = attrs_to_fields(CourseTicket)
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.