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

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

Replace course_registration_allowed by course_registration_forbidden method.

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