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

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

Enable the import of GPA parameters and show imported instead of calculated values.

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