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

Last change on this file since 15999 was 15963, checked in by Henrik Bettermann, 5 years ago

Export matric_number.

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