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

Last change on this file since 13022 was 13008, checked in by Henrik Bettermann, 10 years ago

Catch all errors.

  • Property svn:keywords set to Id
File size: 11.5 KB
Line 
1## $Id: studylevel.py 13008 2015-05-27 15:56:15Z 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        try:
99            session_string = academic_sessions_vocab.getTerm(
100                self.level_session).title
101        except LookupError:
102            return None
103        return session_string
104
105    @property
106    def gpa_params_rectified(self):
107        """Calculate corrected level (sessional) gpa parameters.
108        The corrected gpa is displayed on transcripts only.
109        """
110        credits_weighted = 0.0
111        credits_counted = 0
112        level_gpa = 0.0
113        for ticket in self.values():
114            if ticket.carry_over is False and ticket.score:
115                if ticket.score < ticket.passmark:
116                    co_ticket = find_carry_over(ticket)
117                    if co_ticket is not None and co_ticket.weight is not None:
118                        credits_counted += co_ticket.credits
119                        credits_weighted += co_ticket.credits * co_ticket.weight
120                    continue
121                credits_counted += ticket.credits
122                credits_weighted += ticket.credits * ticket.weight
123        if credits_counted:
124            level_gpa = round(credits_weighted/credits_counted, 3)
125        return level_gpa, credits_counted, credits_weighted
126
127    @property
128    def gpa_params(self):
129        """Calculate gpa parameters for this level.
130        """
131        credits_weighted = 0.0
132        credits_counted = 0
133        level_gpa = 0.0
134        for ticket in self.values():
135            if ticket.score is not None:
136                credits_counted += ticket.credits
137                credits_weighted += ticket.credits * ticket.weight
138        if credits_counted:
139            level_gpa = round(credits_weighted/credits_counted, 3)
140        return level_gpa, credits_counted, credits_weighted
141
142    @property
143    def gpa(self):
144        return self.gpa_params[0]
145
146    @property
147    def passed_params(self):
148        """Determine the number and credits of passed and failed courses.
149        This method is used for level reports.
150        """
151        passed = failed = 0
152        courses_failed = []
153        credits_failed = 0
154        credits_passed = 0
155        for ticket in self.values():
156            if ticket.score is not None:
157                if ticket.score < ticket.passmark:
158                    failed += 1
159                    credits_failed += ticket.credits
160                    courses_failed.append(ticket.code)
161                else:
162                    passed += 1
163                    credits_passed += ticket.credits
164        return passed, failed, credits_passed, credits_failed, courses_failed
165
166    @property
167    def cumulative_params(self):
168        """Calculate the cumulative gpa and other cumulative parameters
169        for this level.
170        All levels below this level are taken under consideration
171        (including repeating levels). This method is used for level reports.
172        """
173        credits_passed = 0
174        total_credits = 0
175        total_credits_counted = 0
176        total_credits_weighted = 0
177        cgpa = 0.0
178        if self.__parent__:
179            for level in self.__parent__.values():
180                if level.level > self.level:
181                    continue
182                credits_passed += level.passed_params[2]
183                total_credits += level.total_credits
184                gpa_params = level.gpa_params
185                total_credits_counted += gpa_params[1]
186                total_credits_weighted += gpa_params[2]
187            if total_credits_counted:
188                cgpa = round(total_credits_weighted / total_credits_counted, 3)
189        return (cgpa, total_credits_counted, total_credits_weighted,
190               total_credits, credits_passed)
191
192    @property
193    def is_current_level(self):
194        try:
195            return self.__parent__.current_level == self.level
196        except AttributeError:
197            return False
198
199    def writeLogMessage(self, view, message):
200        return self.__parent__.__parent__.writeLogMessage(view, message)
201
202    @property
203    def level_title(self):
204        studylevelsource = StudyLevelSource()
205        return studylevelsource.factory.getTitle(self.__parent__, self.level)
206
207    def addCourseTicket(self, ticket, course):
208        """Add a course ticket object.
209        """
210        if not ICourseTicket.providedBy(ticket):
211            raise TypeError(
212                'StudentStudyLeves contain only ICourseTicket instances')
213        ticket.code = course.code
214        ticket.title = course.title
215        ticket.fcode = course.__parent__.__parent__.__parent__.code
216        ticket.dcode = course.__parent__.__parent__.code
217        ticket.credits = course.credits
218        ticket.passmark = course.passmark
219        ticket.semester = course.semester
220        self[ticket.code] = ticket
221        return
222
223    def addCertCourseTickets(self, cert):
224        """Collect all certificate courses and create course
225        tickets automatically.
226        """
227        if cert is not None:
228            for key, val in cert.items():
229                if val.level != self.level:
230                    continue
231                ticket = createObject(u'waeup.CourseTicket')
232                ticket.automatic = True
233                ticket.mandatory = val.mandatory
234                ticket.carry_over = False
235                self.addCourseTicket(ticket, val.course)
236        return
237
238StudentStudyLevel = attrs_to_fields(
239    StudentStudyLevel, omit=['total_credits', 'gpa'])
240
241class StudentStudyLevelFactory(grok.GlobalUtility):
242    """A factory for student study levels.
243    """
244    grok.implements(IFactory)
245    grok.name(u'waeup.StudentStudyLevel')
246    title = u"Create a new student study level.",
247    description = u"This factory instantiates new student study level instances."
248
249    def __call__(self, *args, **kw):
250        return StudentStudyLevel()
251
252    def getInterfaces(self):
253        return implementedBy(StudentStudyLevel)
254
255class CourseTicket(grok.Model):
256    """This is a course ticket which allows the
257    student to attend the course. Lecturers will enter scores and more at
258    the end of the term.
259
260    A course ticket contains a copy of the original course and
261    certificate course data. If the courses and/or the referring certificate
262    courses are removed, the corresponding tickets remain unchanged.
263    So we do not need any event triggered actions on course tickets.
264    """
265    grok.implements(ICourseTicket, IStudentNavigation)
266    grok.provides(ICourseTicket)
267
268    def __init__(self):
269        super(CourseTicket, self).__init__()
270        self.code = None
271        return
272
273    @property
274    def student(self):
275        """Get the associated student object.
276        """
277        try:
278            return self.__parent__.__parent__.__parent__
279        except AttributeError: # in unit tests
280            return None
281
282    @property
283    def certcode(self):
284        try:
285            return self.__parent__.__parent__.certificate.code
286        except AttributeError: # in unit tests
287            return None
288
289    @property
290    def removable_by_student(self):
291        return not self.mandatory
292
293    @property
294    def editable_by_lecturer(self):
295        try:
296            cas = grok.getSite()['configuration'].current_academic_session
297            if self.student.state == VALIDATED and self.student.current_session == cas:
298                return True
299        except (AttributeError, TypeError): # in unit tests
300            pass
301        return False
302
303    def writeLogMessage(self, view, message):
304        return self.__parent__.__parent__.__parent__.writeLogMessage(view, message)
305
306    @property
307    def level(self):
308        """Returns the id of the level the ticket has been added to.
309        """
310        try:
311            return self.__parent__.level
312        except AttributeError: # in unit tests
313            return None
314
315    @property
316    def level_session(self):
317        """Returns the session of the level the ticket has been added to.
318        """
319        try:
320            return self.__parent__.level_session
321        except AttributeError: # in unit tests
322            return None
323
324    @property
325    def grade(self):
326        """Returns the grade calculated from score.
327        """
328        return getGradeWeightFromScore(self.score)[0]
329
330    @property
331    def weight(self):
332        """Returns the weight calculated from score.
333        """
334        return getGradeWeightFromScore(self.score)[1]
335
336CourseTicket = attrs_to_fields(CourseTicket)
337
338class CourseTicketFactory(grok.GlobalUtility):
339    """A factory for student study levels.
340    """
341    grok.implements(IFactory)
342    grok.name(u'waeup.CourseTicket')
343    title = u"Create a new course ticket.",
344    description = u"This factory instantiates new course ticket instances."
345
346    def __call__(self, *args, **kw):
347        return CourseTicket()
348
349    def getInterfaces(self):
350        return implementedBy(CourseTicket)
Note: See TracBrowser for help on using the repository browser.