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

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

Count and display the number of courses registered but not taken.

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