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

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

Re-enable ug_ft course registration.

  • Property svn:keywords set to Id
File size: 15.3 KB
Line 
1## $Id: studylevel.py 14613 2017-03-09 07:42:05Z 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 and not ticket.outstanding:
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 and not ticket.outstanding:
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        restitution_paid = True
140        if self.student.entry_session < 2016 \
141            and self.student.current_mode == 'ug_ft':
142            restitution_paid = False
143            for ticket in self.student['payments'].values():
144                if ticket.p_category == 'restitution' and \
145                    ticket.p_session == self.level_session and \
146                    ticket.p_state == 'paid':
147                        restitution_paid = True
148                        continue
149        if not restitution_paid:
150            return _("Please pay restitution fee first.")
151        if self.student.is_fresh:
152            return
153        try:
154            if self.student.is_postgrad:
155                deadline = grok.getSite()['configuration'][
156                        str(self.level_session)].coursereg_deadline_pg
157            elif self.student.current_mode.startswith('dp'):
158                deadline = grok.getSite()['configuration'][
159                        str(self.level_session)].coursereg_deadline_dp
160            elif self.student.current_mode.endswith('_pt'):
161                deadline = grok.getSite()['configuration'][
162                        str(self.level_session)].coursereg_deadline_pt
163            elif self.student.current_mode == 'found':
164                deadline = grok.getSite()['configuration'][
165                        str(self.level_session)].coursereg_deadline_found
166            else:
167                deadline = grok.getSite()['configuration'][
168                        str(self.level_session)].coursereg_deadline
169        except (TypeError, KeyError):
170            return
171        if not deadline or deadline > datetime.now(pytz.utc):
172            return
173        if len(self.student['payments']):
174            for ticket in self.student['payments'].values():
175                if ticket.p_category == 'late_registration' and \
176                    ticket.p_session == self.level_session and \
177                    ticket.p_state == 'paid':
178                        return
179        return _("Course registration has ended. "
180                 "Please pay the late registration fee.")
181
182    # only AAUE
183    @property
184    def remark(self):
185        certificate = getattr(self.__parent__,'certificate',None)
186        end_level = getattr(certificate, 'end_level', None)
187        failed_limit = 1.5
188        if self.student.entry_session < 2013:
189            failed_limit = 1.0
190        # final level student remark
191        if end_level and self.level >= end_level:
192            if self.level > end_level:
193                # spill-over level
194                if self.gpa_params[1] == 0:
195                    # no credits taken
196                    return 'NER'
197            elif self.gpa_params[1] < 30:
198                # credits taken below limit
199                return 'NER'
200            if self.level_verdict in ('FRNS', 'NER', 'NYV'):
201                return self.level_verdict
202            if '_m' in self.passed_params[4]:
203                return 'FRNS'
204            if not self.cumulative_params[0]:
205                return 'FRNS'
206            if len(self.passed_params[5]) \
207                and not self.passed_params[5] == 'Nil':
208                return 'FRNS'
209            if self.cumulative_params[0] < failed_limit:
210                return 'Fail'
211            if self.cumulative_params[0] < 1.5:
212                return 'Pass'
213            if self.cumulative_params[0] < 2.4:
214                return '3s_rd_s'
215            if self.cumulative_params[0] < 3.5:
216                return '2s_2_s'
217            if self.cumulative_params[0] < 4.5:
218                return '2s_1_s'
219            if self.cumulative_params[0] < 5.1:
220                return '1s_st_s'
221            return 'N/A'
222        # returning student remark
223        if self.level_verdict in ('FRNS', 'NER', 'NYV'):
224            return 'Probation'
225        if self.level_verdict == 'D':
226            return 'Withdrawn'
227        if self.gpa_params[1] < 30:
228            # credits taken below limit
229            return 'Probation'
230        if self.cumulative_params[0] < failed_limit:
231            return 'Probation'
232        if self.cumulative_params[0] < 5.1:
233            return 'Proceed'
234        return 'N/A'
235
236    def _schoolfeePaymentMade(self):
237        if len(self.student['payments']):
238            for ticket in self.student['payments'].values():
239                if ticket.p_state == 'paid' and \
240                    ticket.p_category in (
241                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
242                    ticket.p_session == self.student[
243                        'studycourse'].current_session:
244                    return True
245        return False
246
247    def _coursePaymentsMade(self, course):
248        if self.level_session < 2016:
249            return True
250        if not course.code[:3] in ('GST', 'ENT'):
251            return True
252        if len(self.student['payments']):
253            paid_cats = list()
254            for pticket in self.student['payments'].values():
255                if pticket.p_state == 'paid':
256                    paid_cats.append(pticket.p_category)
257            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
258                not 'gst_registration_1' in paid_cats:
259                return False
260            if course.code in ('GST222',) and \
261                not 'gst_registration_2' in paid_cats:
262                return False
263            if course.code in ('ENT201',) and \
264                not 'ent_registration_1' in paid_cats:
265                return False
266            if course.code in ('GST101', 'GST102') and \
267                not 'gst_text_book_1' in paid_cats and \
268                not 'gst_text_book_0' in paid_cats:
269                return False
270            if course.code in ('GST111', 'GST112') and \
271                not 'gst_text_book_2' in paid_cats and \
272                not 'gst_text_book_0' in paid_cats:
273                return False
274            if course.code in ('GST222',) and \
275                not 'gst_text_book_3' in paid_cats:
276                return False
277            if course.code in ('ENT201',) and \
278                not 'ent_text_book_1' in paid_cats:
279                return False
280            return True
281        return False
282
283    def addCourseTicket(self, ticket, course):
284        """Add a course ticket object.
285        """
286        if not ICourseTicket.providedBy(ticket):
287            raise TypeError(
288                'StudentStudyLeves contain only ICourseTicket instances')
289        # Raise TicketError if course is in 2nd semester but
290        # schoolfee has not yet been fully paid.
291        if course.semester == 2 and not self._schoolfeePaymentMade():
292            raise TicketError(
293                _('%s is a 2nd semester course which can only be added '
294                  'if school fees have been fully paid.' % course.code))
295        # Raise TicketError if registration fee or text
296        # book fee haven't been paid.
297        if not self._coursePaymentsMade(course):
298            raise TicketError(
299                _('%s can only be added if both registration fee and text '
300                  'book fee have been paid.'
301                  % course.code))
302        ticket.code = course.code
303        ticket.title = course.title
304        ticket.fcode = course.__parent__.__parent__.__parent__.code
305        ticket.dcode = course.__parent__.__parent__.code
306        ticket.credits = course.credits
307        if self.student.entry_session < 2013:
308            ticket.passmark = course.passmark - 5
309        else:
310            ticket.passmark = course.passmark
311        ticket.semester = course.semester
312        self[ticket.code] = ticket
313        return
314
315    def addCertCourseTickets(self, cert):
316        """Collect all certificate courses and create course
317        tickets automatically.
318        """
319        if cert is not None:
320            for key, val in cert.items():
321                if val.level != self.level:
322                    continue
323                ticket = createObject(u'waeup.CourseTicket')
324                ticket.automatic = True
325                ticket.mandatory = val.mandatory
326                ticket.carry_over = False
327                try:
328                    self.addCourseTicket(ticket, val.course)
329                except TicketError:
330                    pass
331        return
332
333CustomStudentStudyLevel = attrs_to_fields(
334    CustomStudentStudyLevel, omit=[
335    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
336
337class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
338    """A factory for student study levels.
339    """
340
341    def __call__(self, *args, **kw):
342        return CustomStudentStudyLevel()
343
344    def getInterfaces(self):
345        return implementedBy(CustomStudentStudyLevel)
346
347class CustomCourseTicket(CourseTicket):
348    """This is a course ticket which allows the
349    student to attend the course. Lecturers will enter scores and more at
350    the end of the term.
351
352    A course ticket contains a copy of the original course and
353    course referrer data. If the courses and/or their referrers are removed, the
354    corresponding tickets remain unchanged. So we do not need any event
355    triggered actions on course tickets.
356    """
357    grok.implements(ICustomCourseTicket, IStudentNavigation)
358    grok.provides(ICustomCourseTicket)
359
360    @property
361    def _getGradeWeightFromScore(self):
362        """Nigerian Course Grading System
363        """
364        if self.score == -1:
365            return ('-',0) # core course and result not yet available (used by AAUE)
366        if self.total_score is None:
367            return (None, None)
368        if self.total_score >= 70:
369            return ('A',5)
370        if self.total_score >= 60:
371            return ('B',4)
372        if self.total_score >= 50:
373            return ('C',3)
374        if self.total_score >= 45:
375            return ('D',2)
376        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
377            return ('E',1)
378        return ('F',0)
379
380    @property
381    def total_score(self):
382        """Returns ca + score.
383        """
384        if self.score == -1:
385            return 0
386        if not None in (self.score, self.ca):
387            return self.score + self.ca
388        return None
389
390    @property
391    def editable_by_lecturer(self):
392        """True if lecturer is allowed to edit the ticket.
393        """
394        return True
395
396CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
397
398class CustomCourseTicketFactory(CourseTicketFactory):
399    """A factory for student study levels.
400    """
401
402    def __call__(self, *args, **kw):
403        return CustomCourseTicket()
404
405    def getInterfaces(self):
406        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.