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

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

Mark mandatory (core) courses.

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