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

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

More docs.

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