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

Last change on this file since 15462 was 15458, checked in by Henrik Bettermann, 5 years ago

Add bridge course registration deadline.

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