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

Last change on this file since 15144 was 15090, checked in by Henrik Bettermann, 6 years ago

Bugfix

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