source: main/waeup.aaue/trunk/src/waeup/aaue/students/studylevel.py @ 14534

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

If ticket.score is set to '-1', total_score will be 0 and the grade is '-'.

  • Property svn:keywords set to Id
File size: 15.3 KB
Line 
1## $Id: studylevel.py 14532 2017-02-10 17:03:20Z henrik $
2##
3## Copyright (C) 2012 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.component import createObject
27from zope.interface import implementedBy
28from waeup.kofa.utils.helpers import attrs_to_fields
29from waeup.kofa.interfaces import CREATED
30from waeup.kofa.students.browser import TicketError
31from waeup.kofa.students.studylevel import (
32    StudentStudyLevel, CourseTicket,
33    CourseTicketFactory, StudentStudyLevelFactory)
34from waeup.kofa.students.interfaces import IStudentNavigation, ICourseTicket
35from waeup.aaue.students.interfaces import (
36    ICustomStudentStudyLevel, ICustomCourseTicket)
37from waeup.aaue.interfaces import MessageFactory as _
38
39
40class CustomStudentStudyLevel(StudentStudyLevel):
41    """This is a container for course tickets.
42    """
43    grok.implements(ICustomStudentStudyLevel, IStudentNavigation)
44    grok.provides(ICustomStudentStudyLevel)
45
46    @property
47    def total_credits_s1(self):
48        total = 0
49        for ticket in self.values():
50            if ticket.semester == 1:
51                total += ticket.credits
52        return total
53
54    @property
55    def total_credits_s2(self):
56        total = 0
57        for ticket in self.values():
58            if ticket.semester == 2:
59                total += ticket.credits
60        return total
61
62    @property
63    def gpa_params(self):
64        """Calculate gpa parameters for this level.
65        """
66        credits_weighted = 0.0
67        credits_counted = 0
68        level_gpa = 0.0
69        for ticket in self.values():
70            if ticket.total_score is not None:
71                credits_counted += ticket.credits
72                credits_weighted += ticket.credits * ticket.weight
73        if credits_counted:
74            level_gpa = credits_weighted/credits_counted
75        # Override level_gpa if value has been imported
76        imported_gpa = getattr(self, 'imported_gpa', None)
77        if imported_gpa:
78            level_gpa = imported_gpa
79        return level_gpa, credits_counted, credits_weighted
80
81    @property
82    def gpa_params_rectified(self):
83        return self.gpa_params
84
85    @property
86    def passed_params(self):
87        """Determine the number and credits of passed and failed courses.
88        This method is used for level reports.
89        """
90        passed = failed = 0
91        courses_failed = ''
92        credits_failed = 0
93        credits_passed = 0
94        courses_not_taken = ''
95        for ticket in self.values():
96            if ticket.total_score is not None:
97                if ticket.total_score < ticket.passmark:
98                    failed += 1
99                    credits_failed += ticket.credits
100                    if ticket.mandatory:
101                        courses_failed += 'm_%s_m ' % ticket.code
102                    else:
103                        courses_failed += '%s ' % ticket.code
104                else:
105                    passed += 1
106                    credits_passed += ticket.credits
107            else:
108                courses_not_taken += '%s ' % ticket.code
109        if not len(courses_failed):
110            courses_failed = 'Nil'
111        if not len(courses_not_taken):
112            courses_not_taken = 'Nil'
113        return (passed, failed, credits_passed,
114                credits_failed, courses_failed,
115                courses_not_taken)
116
117    @property
118    def course_registration_forbidden(self):
119        #fac_dep_paid = True
120        #if self.student.entry_session >= 2016:
121        #    fac_dep_paid = False
122        #    for ticket in self.student['payments'].values():
123        #        if ticket.p_category == 'fac_dep' and \
124        #            ticket.p_session == self.level_session and \
125        #            ticket.p_state == 'paid':
126        #                fac_dep_paid = True
127        #                continue
128        #if not fac_dep_paid:
129        #    return _("Please pay faculty and departmental dues first.")
130
131
132        ######################################################
133        # Temporarily disable ug_ft course registration
134        if self.student.current_mode == 'ug_ft':
135            return _("Please check back later, course registration is not yet available.")
136        ######################################################
137
138
139
140        restitution_paid = True
141        if self.student.entry_session < 2016 \
142            and self.student.current_mode == 'ug_ft':
143            restitution_paid = False
144            for ticket in self.student['payments'].values():
145                if ticket.p_category == 'restitution' and \
146                    ticket.p_session == self.level_session and \
147                    ticket.p_state == 'paid':
148                        restitution_paid = True
149                        continue
150        if not restitution_paid:
151            return _("Please pay restitution fee first.")
152        if self.student.is_fresh:
153            return
154        try:
155            if self.student.is_postgrad:
156                deadline = grok.getSite()['configuration'][
157                        str(self.level_session)].coursereg_deadline_pg
158            elif self.student.current_mode.startswith('dp'):
159                deadline = grok.getSite()['configuration'][
160                        str(self.level_session)].coursereg_deadline_dp
161            elif self.student.current_mode.endswith('_pt'):
162                deadline = grok.getSite()['configuration'][
163                        str(self.level_session)].coursereg_deadline_pt
164            elif self.student.current_mode == 'found':
165                deadline = grok.getSite()['configuration'][
166                        str(self.level_session)].coursereg_deadline_found
167            else:
168                deadline = grok.getSite()['configuration'][
169                        str(self.level_session)].coursereg_deadline
170        except (TypeError, KeyError):
171            return
172        if not deadline or deadline > datetime.now(pytz.utc):
173            return
174        if len(self.student['payments']):
175            for ticket in self.student['payments'].values():
176                if ticket.p_category == 'late_registration' and \
177                    ticket.p_session == self.level_session and \
178                    ticket.p_state == 'paid':
179                        return
180        return _("Course registration has ended. "
181                 "Please pay the late registration fee.")
182
183    # only AAUE
184    @property
185    def remark(self):
186        certificate = getattr(self.__parent__,'certificate',None)
187        end_level = getattr(certificate, 'end_level', None)
188        failed_limit = 1.5
189        if self.student.entry_session < 2013:
190            failed_limit = 1.0
191        # final level student remark
192        if end_level and self.level >= end_level:
193            if self.level > end_level:
194                # spill-over level
195                if self.gpa_params[1] == 0:
196                    # no credits taken
197                    return 'NER'
198            else:
199                if self.gpa_params[1] < 30:
200                    # credits taken below limit
201                    return 'NER'
202            if self.level_verdict in ('FRNS', 'NER', 'NYV'):
203                return self.level_verdict
204            if '_m' in self.passed_params[4]:
205                return 'FRNS'
206            if not self.cumulative_params[0]:
207                return 'FRNS'
208            if len(self.passed_params[5]) \
209                and not self.passed_params[5] == 'Nil':
210                return 'FRNS'
211            if self.cumulative_params[0] < failed_limit:
212                return 'Fail'
213            if self.cumulative_params[0] < 1.5:
214                return 'Pass'
215            if self.cumulative_params[0] < 2.4:
216                return '3s_rd_s'
217            if self.cumulative_params[0] < 3.5:
218                return '2s_2_s'
219            if self.cumulative_params[0] < 4.5:
220                return '2s_1_s'
221            if self.cumulative_params[0] < 5.1:
222                return '1s_st_s'
223            return 'N/A'
224        # returning student remark
225        if self.level_verdict in ('FRNS', 'NER', 'NYV'):
226            return 'Probation'
227        if self.level_verdict == 'D':
228            return 'Withdrawn'
229        if self.gpa_params[1] < 30:
230            # credits taken below limit
231            return 'Probation'
232        if self.cumulative_params[0] < failed_limit:
233            return 'Probation'
234        if self.cumulative_params[0] < 5.1:
235            return 'Proceed'
236        return 'N/A'
237
238    def _schoolfeePaymentMade(self):
239        if len(self.student['payments']):
240            for ticket in self.student['payments'].values():
241                if ticket.p_state == 'paid' and \
242                    ticket.p_category in (
243                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
244                    ticket.p_session == self.student[
245                        'studycourse'].current_session:
246                    return True
247        return False
248
249    def _coursePaymentsMade(self, course):
250        if self.level_session < 2016:
251            return True
252        if not course.code[:3] in ('GST', 'ENT'):
253            return True
254        if len(self.student['payments']):
255            paid_cats = list()
256            for pticket in self.student['payments'].values():
257                if pticket.p_state == 'paid':
258                    paid_cats.append(pticket.p_category)
259            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
260                not 'gst_registration_1' in paid_cats:
261                return False
262            if course.code in ('GST222',) and \
263                not 'gst_registration_2' in paid_cats:
264                return False
265            if course.code in ('ENT201',) and \
266                not 'ent_registration_1' in paid_cats:
267                return False
268            if course.code in ('GST101', 'GST102') and \
269                not 'gst_text_book_1' in paid_cats and \
270                not 'gst_text_book_0' in paid_cats:
271                return False
272            if course.code in ('GST111', 'GST112') and \
273                not 'gst_text_book_2' in paid_cats and \
274                not 'gst_text_book_0' in paid_cats:
275                return False
276            if course.code in ('GST222',) and \
277                not 'gst_text_book_3' in paid_cats:
278                return False
279            if course.code in ('ENT201',) and \
280                not 'ent_text_book_1' in paid_cats:
281                return False
282            return True
283        return False
284
285    def addCourseTicket(self, ticket, course):
286        """Add a course ticket object.
287        """
288        if not ICourseTicket.providedBy(ticket):
289            raise TypeError(
290                'StudentStudyLeves contain only ICourseTicket instances')
291        # Raise TicketError if course is in 2nd semester but
292        # schoolfee has not yet been fully paid.
293        if course.semester == 2 and not self._schoolfeePaymentMade():
294            raise TicketError(
295                _('%s is a 2nd semester course which can only be added '
296                  'if school fees have been fully paid.' % course.code))
297        # Raise TicketError if registration fee or text
298        # book fee haven't been paid.
299        if not self._coursePaymentsMade(course):
300            raise TicketError(
301                _('%s can only be added if both registration fee and text '
302                  'book fee have been paid.'
303                  % course.code))
304        ticket.code = course.code
305        ticket.title = course.title
306        ticket.fcode = course.__parent__.__parent__.__parent__.code
307        ticket.dcode = course.__parent__.__parent__.code
308        ticket.credits = course.credits
309        if self.student.entry_session < 2013:
310            ticket.passmark = course.passmark - 5
311        else:
312            ticket.passmark = course.passmark
313        ticket.semester = course.semester
314        self[ticket.code] = ticket
315        return
316
317    def addCertCourseTickets(self, cert):
318        """Collect all certificate courses and create course
319        tickets automatically.
320        """
321        if cert is not None:
322            for key, val in cert.items():
323                if val.level != self.level:
324                    continue
325                ticket = createObject(u'waeup.CourseTicket')
326                ticket.automatic = True
327                ticket.mandatory = val.mandatory
328                ticket.carry_over = False
329                try:
330                    self.addCourseTicket(ticket, val.course)
331                except TicketError:
332                    pass
333        return
334
335CustomStudentStudyLevel = attrs_to_fields(
336    CustomStudentStudyLevel, omit=[
337    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
338
339class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
340    """A factory for student study levels.
341    """
342
343    def __call__(self, *args, **kw):
344        return CustomStudentStudyLevel()
345
346    def getInterfaces(self):
347        return implementedBy(CustomStudentStudyLevel)
348
349class CustomCourseTicket(CourseTicket):
350    """This is a course ticket which allows the
351    student to attend the course. Lecturers will enter scores and more at
352    the end of the term.
353
354    A course ticket contains a copy of the original course and
355    course referrer data. If the courses and/or their referrers are removed, the
356    corresponding tickets remain unchanged. So we do not need any event
357    triggered actions on course tickets.
358    """
359    grok.implements(ICustomCourseTicket, IStudentNavigation)
360    grok.provides(ICustomCourseTicket)
361
362    @property
363    def _getGradeWeightFromScore(self):
364        """Nigerian Course Grading System
365        """
366        if self.score == -1:
367            return ('-',0) # core course and result not yet available (used by AAUE)
368        if self.total_score is None:
369            return (None, None)
370        if self.total_score >= 70:
371            return ('A',5)
372        if self.total_score >= 60:
373            return ('B',4)
374        if self.total_score >= 50:
375            return ('C',3)
376        if self.total_score >= 45:
377            return ('D',2)
378        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
379            return ('E',1)
380        return ('F',0)
381
382    @property
383    def total_score(self):
384        """Returns ca + score.
385        """
386        if self.score == -1:
387            return 0
388        if not None in (self.score, self.ca):
389            return self.score + self.ca
390        return None
391
392    @property
393    def editable_by_lecturer(self):
394        """True if lecturer is allowed to edit the ticket.
395        """
396        return True
397
398CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
399
400class CustomCourseTicketFactory(CourseTicketFactory):
401    """A factory for student study levels.
402    """
403
404    def __call__(self, *args, **kw):
405        return CustomCourseTicket()
406
407    def getInterfaces(self):
408        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.