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

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

Fix level report (remove bug). Lists cannot be rendered. It seems that nobody has created level reports with failed courses so far.

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