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

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

Use ticket.passmark for determining the grade.

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