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

Last change on this file since 14183 was 14154, checked in by Henrik Bettermann, 8 years ago

AAUE requested to use the CGPA on session results reports.

  • Property svn:keywords set to Id
File size: 12.9 KB
Line 
1## $Id: studylevel.py 14154 2016-09-05 04:45:14Z 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.total_score:
117                if ticket.total_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.total_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.total_score is not None:
159                if ticket.total_score < ticket.passmark:
160                    failed += 1
161                    credits_failed += ticket.credits
162                    if ticket.mandatory:
163                        courses_failed += 'm_%s_m ' % ticket.code
164                    else:
165                        courses_failed += '%s ' % ticket.code
166                else:
167                    passed += 1
168                    credits_passed += ticket.credits
169        return passed, failed, credits_passed, credits_failed, courses_failed
170
171    @property
172    def cumulative_params(self):
173        """Calculate the cumulative gpa and other cumulative parameters
174        for this level.
175        All levels below this level are taken under consideration
176        (including repeating levels).
177        This method is used for level reports and meanwhile also
178        for session results presentations.
179        """
180        credits_passed = 0
181        total_credits = 0
182        total_credits_counted = 0
183        total_credits_weighted = 0
184        cgpa = 0.0
185        if self.__parent__:
186            for level in self.__parent__.values():
187                if level.level > self.level:
188                    continue
189                credits_passed += level.passed_params[2]
190                total_credits += level.total_credits
191                gpa_params = level.gpa_params
192                total_credits_counted += gpa_params[1]
193                total_credits_weighted += gpa_params[2]
194            if total_credits_counted:
195                cgpa = round(total_credits_weighted / total_credits_counted, 3)
196        return (cgpa, total_credits_counted, total_credits_weighted,
197               total_credits, credits_passed)
198
199    @property
200    def is_current_level(self):
201        try:
202            return self.__parent__.current_level == self.level
203        except AttributeError:
204            return False
205
206    def writeLogMessage(self, view, message):
207        return self.__parent__.__parent__.writeLogMessage(view, message)
208
209    @property
210    def level_title(self):
211        studylevelsource = StudyLevelSource()
212        return studylevelsource.factory.getTitle(self.__parent__, self.level)
213
214    @property
215    def course_registration_allowed(self):
216        try:
217            deadline = grok.getSite()['configuration'][
218                str(self.level_session)].coursereg_deadline
219        except (TypeError, KeyError):
220            return True
221        if not deadline or deadline > datetime.now(pytz.utc):
222            return True
223        payment_made = False
224        if len(self.student['payments']):
225            for ticket in self.student['payments'].values():
226                if ticket.p_category == 'late_registration' and \
227                    ticket.p_session == self.level_session and \
228                    ticket.p_state == 'paid':
229                        payment_made = True
230        if payment_made:
231            return True
232        return False
233
234    def addCourseTicket(self, ticket, course):
235        """Add a course ticket object.
236        """
237        if not ICourseTicket.providedBy(ticket):
238            raise TypeError(
239                'StudentStudyLeves contain only ICourseTicket instances')
240        ticket.code = course.code
241        ticket.title = course.title
242        ticket.fcode = course.__parent__.__parent__.__parent__.code
243        ticket.dcode = course.__parent__.__parent__.code
244        ticket.credits = course.credits
245        ticket.passmark = course.passmark
246        ticket.semester = course.semester
247        self[ticket.code] = ticket
248        return
249
250    def addCertCourseTickets(self, cert):
251        """Collect all certificate courses and create course
252        tickets automatically.
253        """
254        if cert is not None:
255            for key, val in cert.items():
256                if val.level != self.level:
257                    continue
258                ticket = createObject(u'waeup.CourseTicket')
259                ticket.automatic = True
260                ticket.mandatory = val.mandatory
261                ticket.carry_over = False
262                self.addCourseTicket(ticket, val.course)
263        return
264
265StudentStudyLevel = attrs_to_fields(
266    StudentStudyLevel, omit=['total_credits', 'gpa'])
267
268class StudentStudyLevelFactory(grok.GlobalUtility):
269    """A factory for student study levels.
270    """
271    grok.implements(IFactory)
272    grok.name(u'waeup.StudentStudyLevel')
273    title = u"Create a new student study level.",
274    description = u"This factory instantiates new student study level instances."
275
276    def __call__(self, *args, **kw):
277        return StudentStudyLevel()
278
279    def getInterfaces(self):
280        return implementedBy(StudentStudyLevel)
281
282class CourseTicket(grok.Model):
283    """This is a course ticket which allows the
284    student to attend the course. Lecturers will enter scores and more at
285    the end of the term.
286
287    A course ticket contains a copy of the original course and
288    certificate course data. If the courses and/or the referring certificate
289    courses are removed, the corresponding tickets remain unchanged.
290    So we do not need any event triggered actions on course tickets.
291    """
292    grok.implements(ICourseTicket, IStudentNavigation)
293    grok.provides(ICourseTicket)
294
295    def __init__(self):
296        super(CourseTicket, self).__init__()
297        self.code = None
298        return
299
300    @property
301    def student(self):
302        """Get the associated student object.
303        """
304        try:
305            return self.__parent__.__parent__.__parent__
306        except AttributeError: # in unit tests
307            return None
308
309    @property
310    def certcode(self):
311        try:
312            return self.__parent__.__parent__.certificate.code
313        except AttributeError: # in unit tests
314            return None
315
316    @property
317    def removable_by_student(self):
318        """True if student is allowed to remove the ticket.
319        """
320        return not self.mandatory
321
322    @property
323    def editable_by_lecturer(self):
324        """True if lecturer is allowed to edit the ticket.
325        """
326        try:
327            cas = grok.getSite()[
328                'configuration'].current_academic_session
329            if self.student.state == VALIDATED and \
330                self.student.current_session == cas:
331                return True
332        except (AttributeError, TypeError): # in unit tests
333            pass
334        return False
335
336    def writeLogMessage(self, view, message):
337        return self.__parent__.__parent__.__parent__.writeLogMessage(
338            view, message)
339
340    @property
341    def level(self):
342        """Returns the id of the level the ticket has been added to.
343        """
344        try:
345            return self.__parent__.level
346        except AttributeError: # in unit tests
347            return None
348
349    @property
350    def level_session(self):
351        """Returns the session of the level the ticket has been added to.
352        """
353        try:
354            return self.__parent__.level_session
355        except AttributeError: # in unit tests
356            return None
357
358    @property
359    def total_score(self):
360        """Returns the total score of this ticket. In the base package
361        this is simply the score. In customized packages this could be
362        something else.
363        """
364        return self.score
365
366    @property
367    def grade(self):
368        """Returns the grade calculated from total score.
369        """
370        return getGradeWeightFromScore(self.total_score)[0]
371
372    @property
373    def weight(self):
374        """Returns the weight calculated from total score.
375        """
376        return getGradeWeightFromScore(self.total_score)[1]
377
378CourseTicket = attrs_to_fields(CourseTicket)
379
380class CourseTicketFactory(grok.GlobalUtility):
381    """A factory for student study levels.
382    """
383    grok.implements(IFactory)
384    grok.name(u'waeup.CourseTicket')
385    title = u"Create a new course ticket.",
386    description = u"This factory instantiates new course ticket instances."
387
388    def __call__(self, *args, **kw):
389        return CourseTicket()
390
391    def getInterfaces(self):
392        return implementedBy(CourseTicket)
Note: See TracBrowser for help on using the repository browser.