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

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

List codes of course w/o result.

  • Property svn:keywords set to Id
File size: 13.7 KB
RevLine 
[7191]1## $Id: studylevel.py 14392 2017-01-13 11:34:53Z 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
27from zope.component import createObject, queryUtility
[8330]28from zope.interface import implementedBy
[10631]29from waeup.kofa.interfaces import academic_sessions_vocab, VALIDATED
[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
[14351]49def getGradeWeightFromScore(score, student):
[14350]50    """Nigerian Course Grading System
51    """
[14351]52    if score is None:
[9684]53        return (None, None)
[14351]54    if score >= 70:
[9684]55        return ('A',5)
[14351]56    if score >= 60:
[9684]57        return ('B',4)
[14351]58    if score >= 50:
[9684]59        return ('C',3)
[14351]60    if score >= 45:
[9684]61        return ('D',2)
[14351]62    if score >= 40 and student.entry_session < 2013:
[9684]63        return ('E',1)
64    return ('F',0)
65
[6775]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
[8736]76    @property
77    def student(self):
78        try:
79            return self.__parent__.__parent__
80        except AttributeError:
81            return None
[6775]82
[9235]83    @property
[9253]84    def certcode(self):
85        try:
86            return self.__parent__.certificate.code
87        except AttributeError:
88            return None
89
90    @property
[9235]91    def number_of_tickets(self):
92        return len(self)
93
[9257]94    @property
[9532]95    def total_credits(self):
96        total = 0
97        for ticket in self.values():
98            total += ticket.credits
99        return total
100
101    @property
[9912]102    def getSessionString(self):
[13006]103        try:
104            session_string = academic_sessions_vocab.getTerm(
105                self.level_session).title
106        except LookupError:
107            return None
108        return session_string
[9912]109
110    @property
[10479]111    def gpa_params_rectified(self):
[13002]112        """Calculate corrected level (sessional) gpa parameters.
[10598]113        The corrected gpa is displayed on transcripts only.
114        """
115        credits_weighted = 0.0
[9687]116        credits_counted = 0
[10276]117        level_gpa = 0.0
[9687]118        for ticket in self.values():
[14133]119            if ticket.carry_over is False and ticket.total_score:
120                if ticket.total_score < ticket.passmark:
[10276]121                    co_ticket = find_carry_over(ticket)
[10313]122                    if co_ticket is not None and co_ticket.weight is not None:
[10276]123                        credits_counted += co_ticket.credits
[10598]124                        credits_weighted += co_ticket.credits * co_ticket.weight
[10276]125                    continue
[9687]126                credits_counted += ticket.credits
[10598]127                credits_weighted += ticket.credits * ticket.weight
[9687]128        if credits_counted:
[14382]129            level_gpa = credits_weighted/credits_counted
[10598]130        return level_gpa, credits_counted, credits_weighted
[9687]131
132    @property
[10479]133    def gpa_params(self):
[10598]134        """Calculate gpa parameters for this level.
135        """
136        credits_weighted = 0.0
[10479]137        credits_counted = 0
138        level_gpa = 0.0
139        for ticket in self.values():
[14133]140            if ticket.total_score is not None:
[10479]141                credits_counted += ticket.credits
[10598]142                credits_weighted += ticket.credits * ticket.weight
[10479]143        if credits_counted:
[14382]144            level_gpa = credits_weighted/credits_counted
[14200]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
[10598]150        return level_gpa, credits_counted, credits_weighted
[10479]151
152    @property
153    def gpa(self):
154        return self.gpa_params[0]
155
156    @property
[10553]157    def passed_params(self):
[10598]158        """Determine the number and credits of passed and failed courses.
[14368]159        Count the number of courses registered but not taken.
[10598]160        This method is used for level reports.
161        """
[10553]162        passed = failed = 0
[13868]163        courses_failed = ''
[10616]164        credits_failed = 0
165        credits_passed = 0
[14392]166        courses_not_taken = ''
[10553]167        for ticket in self.values():
[14133]168            if ticket.total_score is not None:
169                if ticket.total_score < ticket.passmark:
[10553]170                    failed += 1
[10616]171                    credits_failed += ticket.credits
[14123]172                    if ticket.mandatory:
173                        courses_failed += 'm_%s_m ' % ticket.code
174                    else:
175                        courses_failed += '%s ' % ticket.code
[10553]176                else:
177                    passed += 1
[10616]178                    credits_passed += ticket.credits
[14368]179            else:
[14392]180                courses_not_taken += '%s ' % ticket.code
[14368]181        return (passed, failed, credits_passed,
182                credits_failed, courses_failed,
[14392]183                courses_not_taken)
[10553]184
185    @property
[10598]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
[14154]190        (including repeating levels).
191        This method is used for level reports and meanwhile also
192        for session results presentations.
[10598]193        """
[10616]194        credits_passed = 0
[10598]195        total_credits = 0
196        total_credits_counted = 0
197        total_credits_weighted = 0
[10618]198        cgpa = 0.0
[13002]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:
[14382]209                cgpa = total_credits_weighted/total_credits_counted
[14200]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
[10598]215        return (cgpa, total_credits_counted, total_credits_weighted,
[10616]216               total_credits, credits_passed)
[10598]217
218    @property
[9257]219    def is_current_level(self):
220        try:
221            return self.__parent__.current_level == self.level
222        except AttributeError:
223            return False
224
[8735]225    def writeLogMessage(self, view, message):
226        return self.__parent__.__parent__.writeLogMessage(view, message)
227
[6775]228    @property
229    def level_title(self):
230        studylevelsource = StudyLevelSource()
231        return studylevelsource.factory.getTitle(self.__parent__, self.level)
232
[13031]233    @property
[14247]234    def course_registration_forbidden(self):
[13031]235        try:
236            deadline = grok.getSite()['configuration'][
[13057]237                str(self.level_session)].coursereg_deadline
238        except (TypeError, KeyError):
[14247]239            return
[13069]240        if not deadline or deadline > datetime.now(pytz.utc):
[14247]241            return
[13031]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':
[14247]247                        return
248        return _("Course registration has ended. "
249                 "Please pay the late registration fee.")
[13031]250
[8920]251    def addCourseTicket(self, ticket, course):
[6781]252        """Add a course ticket object.
253        """
[8920]254        if not ICourseTicket.providedBy(ticket):
[6781]255            raise TypeError(
256                'StudentStudyLeves contain only ICourseTicket instances')
[8920]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
[6781]265        return
266
[9501]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
[9690]282StudentStudyLevel = attrs_to_fields(
[10479]283    StudentStudyLevel, omit=['total_credits', 'gpa'])
[6781]284
[7536]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
[6781]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.
[6783]303
304    A course ticket contains a copy of the original course and
[12882]305    certificate course data. If the courses and/or the referring certificate
[8920]306    courses are removed, the corresponding tickets remain unchanged.
[12882]307    So we do not need any event triggered actions on course tickets.
[6781]308    """
309    grok.implements(ICourseTicket, IStudentNavigation)
310    grok.provides(ICourseTicket)
311
[6795]312    def __init__(self):
[6781]313        super(CourseTicket, self).__init__()
[6795]314        self.code = None
[6781]315        return
316
[8736]317    @property
318    def student(self):
[8338]319        """Get the associated student object.
320        """
321        try:
322            return self.__parent__.__parent__.__parent__
[13003]323        except AttributeError: # in unit tests
[8338]324            return None
[6781]325
[9253]326    @property
327    def certcode(self):
328        try:
329            return self.__parent__.__parent__.certificate.code
[13003]330        except AttributeError: # in unit tests
[9253]331            return None
332
[9698]333    @property
334    def removable_by_student(self):
[13046]335        """True if student is allowed to remove the ticket.
336        """
[9698]337        return not self.mandatory
338
[10631]339    @property
340    def editable_by_lecturer(self):
[13046]341        """True if lecturer is allowed to edit the ticket.
342        """
[13003]343        try:
[13031]344            cas = grok.getSite()[
345                'configuration'].current_academic_session
346            if self.student.state == VALIDATED and \
347                self.student.current_session == cas:
[13003]348                return True
[13008]349        except (AttributeError, TypeError): # in unit tests
[13003]350            pass
[10631]351        return False
352
[8735]353    def writeLogMessage(self, view, message):
[13031]354        return self.__parent__.__parent__.__parent__.writeLogMessage(
355            view, message)
[8735]356
[9925]357    @property
358    def level(self):
[7633]359        """Returns the id of the level the ticket has been added to.
360        """
[8338]361        try:
362            return self.__parent__.level
[13003]363        except AttributeError: # in unit tests
[8338]364            return None
[7633]365
[9925]366    @property
367    def level_session(self):
[7633]368        """Returns the session of the level the ticket has been added to.
369        """
[8338]370        try:
371            return self.__parent__.level_session
[13003]372        except AttributeError: # in unit tests
[8338]373            return None
[7633]374
[9684]375    @property
[14133]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
[9684]384    def grade(self):
[14133]385        """Returns the grade calculated from total score.
[9684]386        """
[14350]387        return getGradeWeightFromScore(self.total_score, self.student)[0]
[7633]388
[9684]389    @property
390    def weight(self):
[14133]391        """Returns the weight calculated from total score.
[9684]392        """
[14350]393        return getGradeWeightFromScore(self.total_score, self.student)[1]
[9684]394
[6782]395CourseTicket = attrs_to_fields(CourseTicket)
[7548]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.