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

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

Move getGradeWeightFromScore into StudentStudyLevel? class in order to ease customization.

  • Property svn:keywords set to Id
File size: 13.8 KB
Line 
1## $Id: studylevel.py 14531 2017-02-10 13:53:40Z 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
34from waeup.kofa.interfaces import MessageFactory as _
35
36def find_carry_over(ticket):
37    studylevel = ticket.__parent__
38    studycourse = ticket.__parent__.__parent__
39    levels = sorted(studycourse.keys())
40    index = levels.index(str(studylevel.level))
41    try:
42        next_level = levels[index+1]
43    except IndexError:
44        return None
45    next_studylevel = studycourse[next_level]
46    co_ticket = next_studylevel.get(ticket.code, None)
47    return co_ticket
48
49class StudentStudyLevel(grok.Container):
50    """This is a container for course tickets.
51    """
52    grok.implements(IStudentStudyLevel, IStudentNavigation)
53    grok.provides(IStudentStudyLevel)
54
55    def __init__(self):
56        super(StudentStudyLevel, self).__init__()
57        return
58
59    @property
60    def student(self):
61        try:
62            return self.__parent__.__parent__
63        except AttributeError:
64            return None
65
66    @property
67    def certcode(self):
68        try:
69            return self.__parent__.certificate.code
70        except AttributeError:
71            return None
72
73    @property
74    def number_of_tickets(self):
75        return len(self)
76
77    @property
78    def total_credits(self):
79        total = 0
80        for ticket in self.values():
81            total += ticket.credits
82        return total
83
84    @property
85    def getSessionString(self):
86        try:
87            session_string = academic_sessions_vocab.getTerm(
88                self.level_session).title
89        except LookupError:
90            return None
91        return session_string
92
93    @property
94    def gpa_params_rectified(self):
95        """Calculate corrected level (sessional) gpa parameters.
96        The corrected gpa is displayed on transcripts only.
97        """
98        credits_weighted = 0.0
99        credits_counted = 0
100        level_gpa = 0.0
101        for ticket in self.values():
102            if ticket.carry_over is False and ticket.total_score:
103                if ticket.total_score < ticket.passmark:
104                    co_ticket = find_carry_over(ticket)
105                    if co_ticket is not None and co_ticket.weight is not None:
106                        credits_counted += co_ticket.credits
107                        credits_weighted += co_ticket.credits * co_ticket.weight
108                    continue
109                credits_counted += ticket.credits
110                credits_weighted += ticket.credits * ticket.weight
111        if credits_counted:
112            level_gpa = credits_weighted/credits_counted
113        return level_gpa, credits_counted, credits_weighted
114
115    @property
116    def gpa_params(self):
117        """Calculate gpa parameters for this level.
118        """
119        credits_weighted = 0.0
120        credits_counted = 0
121        level_gpa = 0.0
122        for ticket in self.values():
123            if ticket.total_score is not None:
124                credits_counted += ticket.credits
125                credits_weighted += ticket.credits * ticket.weight
126        if credits_counted:
127            level_gpa = credits_weighted/credits_counted
128        # Override level_gpa if value has been imported
129        # (not implemented in base package)
130        imported_gpa = getattr(self, 'imported_gpa', None)
131        if imported_gpa:
132            level_gpa = imported_gpa
133        return level_gpa, credits_counted, credits_weighted
134
135    @property
136    def gpa(self):
137        return self.gpa_params[0]
138
139    @property
140    def passed_params(self):
141        """Determine the number and credits of passed and failed courses.
142        Count the number of courses registered but not taken.
143        This method is used for level reports.
144        """
145        passed = failed = 0
146        courses_failed = ''
147        credits_failed = 0
148        credits_passed = 0
149        courses_not_taken = ''
150        for ticket in self.values():
151            if ticket.total_score is not None:
152                if ticket.total_score < ticket.passmark:
153                    failed += 1
154                    credits_failed += ticket.credits
155                    if ticket.mandatory:
156                        courses_failed += 'm_%s_m ' % ticket.code
157                    else:
158                        courses_failed += '%s ' % ticket.code
159                else:
160                    passed += 1
161                    credits_passed += ticket.credits
162            else:
163                courses_not_taken += '%s ' % ticket.code
164        return (passed, failed, credits_passed,
165                credits_failed, courses_failed,
166                courses_not_taken)
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).
174        This method is used for level reports and meanwhile also
175        for session results presentations.
176        """
177        credits_passed = 0
178        total_credits = 0
179        total_credits_counted = 0
180        total_credits_weighted = 0
181        cgpa = 0.0
182        if self.__parent__:
183            for level in self.__parent__.values():
184                if level.level > self.level:
185                    continue
186                credits_passed += level.passed_params[2]
187                total_credits += level.total_credits
188                gpa_params = level.gpa_params
189                total_credits_counted += gpa_params[1]
190                total_credits_weighted += gpa_params[2]
191            if total_credits_counted:
192                cgpa = total_credits_weighted/total_credits_counted
193            # Override cgpa if value has been imported
194            # (not implemented in base package)
195            imported_cgpa = getattr(self, 'imported_cgpa', None)
196            if imported_cgpa:
197                cgpa = imported_cgpa
198        return (cgpa, total_credits_counted, total_credits_weighted,
199               total_credits, credits_passed)
200
201    @property
202    def is_current_level(self):
203        try:
204            return self.__parent__.current_level == self.level
205        except AttributeError:
206            return False
207
208    def writeLogMessage(self, view, message):
209        return self.__parent__.__parent__.writeLogMessage(view, message)
210
211    @property
212    def level_title(self):
213        studylevelsource = StudyLevelSource()
214        return studylevelsource.factory.getTitle(self.__parent__, self.level)
215
216    @property
217    def course_registration_forbidden(self):
218        try:
219            deadline = grok.getSite()['configuration'][
220                str(self.level_session)].coursereg_deadline
221        except (TypeError, KeyError):
222            return
223        if not deadline or deadline > datetime.now(pytz.utc):
224            return
225        if len(self.student['payments']):
226            for ticket in self.student['payments'].values():
227                if ticket.p_category == 'late_registration' and \
228                    ticket.p_session == self.level_session and \
229                    ticket.p_state == 'paid':
230                        return
231        return _("Course registration has ended. "
232                 "Please pay the late registration fee.")
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 _getGradeWeightFromScore(self):
368        """Nigerian Course Grading System
369        """
370        if self.total_score is None:
371            return (None, None)
372        if self.total_score >= 70:
373            return ('A',5)
374        if self.total_score >= 60:
375            return ('B',4)
376        if self.total_score >= 50:
377            return ('C',3)
378        if self.total_score >= 45:
379            return ('D',2)
380        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
381            return ('E',1)
382        return ('F',0)
383
384    @property
385    def grade(self):
386        """Returns the grade calculated from total score.
387        """
388        return self._getGradeWeightFromScore[0]
389
390    @property
391    def weight(self):
392        """Returns the weight calculated from total score.
393        """
394        return self._getGradeWeightFromScore[1]
395
396CourseTicket = attrs_to_fields(CourseTicket)
397
398class CourseTicketFactory(grok.GlobalUtility):
399    """A factory for student study levels.
400    """
401    grok.implements(IFactory)
402    grok.name(u'waeup.CourseTicket')
403    title = u"Create a new course ticket.",
404    description = u"This factory instantiates new course ticket instances."
405
406    def __call__(self, *args, **kw):
407        return CourseTicket()
408
409    def getInterfaces(self):
410        return implementedBy(CourseTicket)
Note: See TracBrowser for help on using the repository browser.