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

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

Disable ug_ft returning course registration.

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