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

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

Add grade '-' as requested by AAUE.

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