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

Last change on this file since 10816 was 10631, checked in by Henrik Bettermann, 11 years ago

Define conditions for score editing. Tests will follow.

  • Property svn:keywords set to Id
File size: 11.6 KB
Line 
1## $Id: studylevel.py 10631 2013-09-21 05:12:49Z 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        self.level = None
70        return
71
72    @property
73    def student(self):
74        try:
75            return self.__parent__.__parent__
76        except AttributeError:
77            return None
78
79    @property
80    def certcode(self):
81        try:
82            return self.__parent__.certificate.code
83        except AttributeError:
84            return None
85
86    @property
87    def number_of_tickets(self):
88        return len(self)
89
90    @property
91    def total_credits(self):
92        total = 0
93        for ticket in self.values():
94            total += ticket.credits
95        return total
96
97    @property
98    def getSessionString(self):
99        return academic_sessions_vocab.getTerm(
100            self.level_session).title
101
102    @property
103    def gpa_params_rectified(self):
104        """Calculate corrected corrected level (sesional) gpa parameters.
105
106        The corrected gpa is displayed on transcripts only.
107        """
108        credits_weighted = 0.0
109        credits_counted = 0
110        level_gpa = 0.0
111        for ticket in self.values():
112            if ticket.carry_over is False and ticket.score:
113                if ticket.score < ticket.passmark:
114                    co_ticket = find_carry_over(ticket)
115                    if co_ticket is not None and co_ticket.weight is not None:
116                        credits_counted += co_ticket.credits
117                        credits_weighted += co_ticket.credits * co_ticket.weight
118                    continue
119                credits_counted += ticket.credits
120                credits_weighted += ticket.credits * ticket.weight
121        if credits_counted:
122            level_gpa = round(credits_weighted/credits_counted, 3)
123        return level_gpa, credits_counted, credits_weighted
124
125    @property
126    def gpa_params(self):
127        """Calculate gpa parameters for this level.
128        """
129        credits_weighted = 0.0
130        credits_counted = 0
131        level_gpa = 0.0
132        for ticket in self.values():
133            if ticket.score is not None:
134                credits_counted += ticket.credits
135                credits_weighted += ticket.credits * ticket.weight
136        if credits_counted:
137            level_gpa = round(credits_weighted/credits_counted, 3)
138        return level_gpa, credits_counted, credits_weighted
139
140    @property
141    def gpa(self):
142        return self.gpa_params[0]
143
144    @property
145    def passed_params(self):
146        """Determine the number and credits of passed and failed courses.
147
148        This method is used for level reports.
149        """
150        passed = failed = 0
151        courses_failed = []
152        credits_failed = 0
153        credits_passed = 0
154        for ticket in self.values():
155            if ticket.score is not None:
156                if ticket.score < ticket.passmark:
157                    failed += 1
158                    credits_failed += ticket.credits
159                    courses_failed.append(ticket.code)
160                else:
161                    passed += 1
162                    credits_passed += ticket.credits
163        return passed, failed, credits_passed, credits_failed, courses_failed
164
165    @property
166    def cumulative_params(self):
167        """Calculate the cumulative gpa and other cumulative parameters
168        for this level.
169
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        for level in self.__parent__.values():
179            if level.level > self.level:
180                continue
181            credits_passed += level.passed_params[2]
182            total_credits += level.total_credits
183            gpa_params = level.gpa_params
184            total_credits_counted += gpa_params[1]
185            total_credits_weighted += gpa_params[2]
186        if total_credits_counted:
187            cgpa = round(total_credits_weighted / total_credits_counted, 3)
188        return (cgpa, total_credits_counted, total_credits_weighted,
189               total_credits, credits_passed)
190
191    @property
192    def is_current_level(self):
193        try:
194            return self.__parent__.current_level == self.level
195        except AttributeError:
196            return False
197
198    def writeLogMessage(self, view, message):
199        return self.__parent__.__parent__.writeLogMessage(view, message)
200
201    @property
202    def level_title(self):
203        studylevelsource = StudyLevelSource()
204        return studylevelsource.factory.getTitle(self.__parent__, self.level)
205
206    def addCourseTicket(self, ticket, course):
207        """Add a course ticket object.
208        """
209        if not ICourseTicket.providedBy(ticket):
210            raise TypeError(
211                'StudentStudyLeves contain only ICourseTicket instances')
212        ticket.code = course.code
213        ticket.title = course.title
214        ticket.fcode = course.__parent__.__parent__.__parent__.code
215        ticket.dcode = course.__parent__.__parent__.code
216        ticket.credits = course.credits
217        ticket.passmark = course.passmark
218        ticket.semester = course.semester
219        self[ticket.code] = ticket
220        return
221
222    def addCertCourseTickets(self, cert):
223        """Collect all certificate courses and create course
224        tickets automatically.
225        """
226        if cert is not None:
227            for key, val in cert.items():
228                if val.level != self.level:
229                    continue
230                ticket = createObject(u'waeup.CourseTicket')
231                ticket.automatic = True
232                ticket.mandatory = val.mandatory
233                ticket.carry_over = False
234                self.addCourseTicket(ticket, val.course)
235        return
236
237StudentStudyLevel = attrs_to_fields(
238    StudentStudyLevel, omit=['total_credits', 'gpa'])
239
240class StudentStudyLevelFactory(grok.GlobalUtility):
241    """A factory for student study levels.
242    """
243    grok.implements(IFactory)
244    grok.name(u'waeup.StudentStudyLevel')
245    title = u"Create a new student study level.",
246    description = u"This factory instantiates new student study level instances."
247
248    def __call__(self, *args, **kw):
249        return StudentStudyLevel()
250
251    def getInterfaces(self):
252        return implementedBy(StudentStudyLevel)
253
254class CourseTicket(grok.Model):
255    """This is a course ticket which allows the
256    student to attend the course. Lecturers will enter scores and more at
257    the end of the term.
258
259    A course ticket contains a copy of the original course and
260    certificate course data. If the courses and/or the referrin certificate
261    courses are removed, the corresponding tickets remain unchanged.
262    So we do not need any event
263    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:
280            return None
281
282    @property
283    def certcode(self):
284        try:
285            return self.__parent__.__parent__.certificate.code
286        except AttributeError:
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        cas = grok.getSite()['configuration'].current_academic_session
296        if self.student.state == VALIDATED and self.student.current_session == cas:
297            return True
298        return False
299
300    def writeLogMessage(self, view, message):
301        return self.__parent__.__parent__.__parent__.writeLogMessage(view, message)
302
303    @property
304    def level(self):
305        """Returns the id of the level the ticket has been added to.
306        """
307        try:
308            return self.__parent__.level
309        except AttributeError:
310            return None
311
312    @property
313    def level_session(self):
314        """Returns the session of the level the ticket has been added to.
315        """
316        try:
317            return self.__parent__.level_session
318        except AttributeError:
319            return None
320
321    @property
322    def grade(self):
323        """Returns the grade calculated from score.
324        """
325        return getGradeWeightFromScore(self.score)[0]
326
327    @property
328    def weight(self):
329        """Returns the weight calculated from score.
330        """
331        return getGradeWeightFromScore(self.score)[1]
332
333    @property
334    def course(self):
335        """Returns the course the ticket is referring to. Returns
336        None if the course has been removed.
337
338        This method is not used in Kofa anymore.
339        """
340        cat = queryUtility(ICatalog, name='courses_catalog')
341        result = cat.searchResults(code=(self.code, self.code))
342        if len(result) != 1:
343            return None
344        return list(result)[0]
345
346CourseTicket = attrs_to_fields(CourseTicket)
347
348class CourseTicketFactory(grok.GlobalUtility):
349    """A factory for student study levels.
350    """
351    grok.implements(IFactory)
352    grok.name(u'waeup.CourseTicket')
353    title = u"Create a new course ticket.",
354    description = u"This factory instantiates new course ticket instances."
355
356    def __call__(self, *args, **kw):
357        return CourseTicket()
358
359    def getInterfaces(self):
360        return implementedBy(CourseTicket)
Note: See TracBrowser for help on using the repository browser.