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

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

We check if there exists a certificate course in the certificate
container which refers to course ticket to be added. If such an object does
not exist, students and managers will be prevented from registering
the corresponding course.

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