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

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

Use old name 'score' instead of 'total'.

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