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

Last change on this file since 14999 was 14955, checked in by Henrik Bettermann, 7 years ago

Decrease limit for total_credits_counted.

  • Property svn:keywords set to Id
File size: 16.1 KB
Line 
1## $Id: studylevel.py 14955 2018-02-21 08:13:07Z 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            if self.cumulative_params[0] < 1.5:
219                if is_dp:
220                    return 'Fail'
221                return 'Pass'
222            if self.cumulative_params[0] < 2.4:
223                if is_dp:
224                    return 'Pass'
225                return '3s_rd_s'
226            if self.cumulative_params[0] < 3.5:
227                if is_dp:
228                    return 'Merit'
229                return '2s_2_s'
230            if self.cumulative_params[0] < 4.5:
231                if is_dp:
232                    return 'Credit'
233                return '2s_1_s'
234            if self.cumulative_params[0] < 5.1:
235                if is_dp:
236                    return 'Distinction'
237                return '1s_st_s'
238            return 'N/A'
239        # returning student remark
240        if self.level_verdict in ('FRNS', 'NER', 'NYV'):
241            return 'Probation'
242        if self.level_verdict == 'D':
243            return 'Withdrawn'
244        if self.gpa_params[1] == 0:
245            # no credits taken
246            return 'NER'
247        if self.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
248            # credits taken below limit
249            return 'Probation'
250        if self.cumulative_params[0] < failed_limit:
251            return 'Probation'
252        if self.cumulative_params[0] < 5.1:
253            return 'Proceed'
254        return 'N/A'
255
256    def _schoolfeePaymentMade(self):
257        if len(self.student['payments']):
258            for ticket in self.student['payments'].values():
259                if ticket.p_state == 'paid' and \
260                    ticket.p_category in (
261                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
262                    ticket.p_session == self.student[
263                        'studycourse'].current_session:
264                    return True
265        return False
266
267    def _coursePaymentsMade(self, course):
268        if self.level_session < 2016:
269            return True
270        if not course.code[:3] in ('GST', 'ENT'):
271            return True
272        if len(self.student['payments']):
273            paid_cats = list()
274            for pticket in self.student['payments'].values():
275                if pticket.p_state == 'paid':
276                    paid_cats.append(pticket.p_category)
277            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
278                not 'gst_registration_1' in paid_cats:
279                return False
280            if course.code in ('GST222',) and \
281                not 'gst_registration_2' in paid_cats:
282                return False
283            #if course.code in ('ENT201',) and \
284            #    not 'ent_registration_1' in paid_cats:
285            #    return False
286            if course.code in ('GST101', 'GST102') and \
287                not 'gst_text_book_1' in paid_cats and \
288                not 'gst_text_book_0' in paid_cats:
289                return False
290            if course.code in ('GST111', 'GST112') and \
291                not 'gst_text_book_2' in paid_cats and \
292                not 'gst_text_book_0' in paid_cats:
293                return False
294            if course.code in ('GST222',) and \
295                not 'gst_text_book_3' in paid_cats:
296                return False
297            #if course.code in ('ENT201',) and \
298            #    not 'ent_text_book_1' in paid_cats:
299            #    return False
300            return True
301        return False
302
303    def addCourseTicket(self, ticket, course):
304        """Add a course ticket object.
305        """
306        if not ICourseTicket.providedBy(ticket):
307            raise TypeError(
308                'StudentStudyLeves contain only ICourseTicket instances')
309        # Raise TicketError if course is in 2nd semester but
310        # schoolfee has not yet been fully paid.
311        if course.semester == 2 and not self._schoolfeePaymentMade():
312            raise TicketError(
313                _('%s is a 2nd semester course which can only be added '
314                  'if school fees have been fully paid.' % course.code))
315        # Raise TicketError if registration fee or text
316        # book fee haven't been paid.
317        if not self._coursePaymentsMade(course):
318            raise TicketError(
319                _('%s can only be added if both registration fee and text '
320                  'book fee have been paid.'
321                  % course.code))
322        ticket.code = course.code
323        ticket.title = course.title
324        ticket.fcode = course.__parent__.__parent__.__parent__.code
325        ticket.dcode = course.__parent__.__parent__.code
326        ticket.credits = course.credits
327        if self.student.entry_session < 2013:
328            ticket.passmark = course.passmark - 5
329        else:
330            ticket.passmark = course.passmark
331        ticket.semester = course.semester
332        self[ticket.code] = ticket
333        return
334
335    def addCertCourseTickets(self, cert):
336        """Collect all certificate courses and create course
337        tickets automatically.
338        """
339        if cert is not None:
340            for key, val in cert.items():
341                if val.level != self.level:
342                    continue
343                ticket = createObject(u'waeup.CourseTicket')
344                ticket.automatic = True
345                ticket.mandatory = val.mandatory
346                ticket.carry_over = False
347                try:
348                    self.addCourseTicket(ticket, val.course)
349                except TicketError:
350                    pass
351        return
352
353CustomStudentStudyLevel = attrs_to_fields(
354    CustomStudentStudyLevel, omit=[
355    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
356
357class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
358    """A factory for student study levels.
359    """
360
361    def __call__(self, *args, **kw):
362        return CustomStudentStudyLevel()
363
364    def getInterfaces(self):
365        return implementedBy(CustomStudentStudyLevel)
366
367class CustomCourseTicket(CourseTicket):
368    """This is a course ticket which allows the
369    student to attend the course. Lecturers will enter scores and more at
370    the end of the term.
371
372    A course ticket contains a copy of the original course and
373    course referrer data. If the courses and/or their referrers are removed, the
374    corresponding tickets remain unchanged. So we do not need any event
375    triggered actions on course tickets.
376    """
377    grok.implements(ICustomCourseTicket, IStudentNavigation)
378    grok.provides(ICustomCourseTicket)
379
380    @property
381    def _getGradeWeightFromScore(self):
382        """Nigerian Course Grading System
383        """
384        if self.score == -1:
385            return ('-',0) # core course and result not yet available (used by AAUE)
386        if self.total_score is None:
387            return (None, None)
388        if self.total_score >= 70:
389            return ('A',5)
390        if self.total_score >= 60:
391            return ('B',4)
392        if self.total_score >= 50:
393            return ('C',3)
394        if self.total_score >= 45:
395            return ('D',2)
396        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
397            return ('E',1)
398        return ('F',0)
399
400    @property
401    def total_score(self):
402        """Returns ca + score.
403        """
404        if self.score == -1:
405            return 0
406        if not None in (self.score, self.ca):
407            return self.score + self.ca
408        return None
409
410    @property
411    def editable_by_lecturer(self):
412        """True if lecturer is allowed to edit the ticket.
413        """
414        return True
415
416CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
417
418class CustomCourseTicketFactory(CourseTicketFactory):
419    """A factory for student study levels.
420    """
421
422    def __call__(self, *args, **kw):
423        return CustomCourseTicket()
424
425    def getInterfaces(self):
426        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.