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

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

Allow score editing only in states courses registered and validated.

  • Property svn:keywords set to Id
File size: 16.9 KB
Line 
1## $Id: studylevel.py 15412 2019-05-16 13:59:24Z 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        ticket.code = course.code
332        ticket.title = course.title
333        ticket.fcode = course.__parent__.__parent__.__parent__.code
334        ticket.dcode = course.__parent__.__parent__.code
335        ticket.credits = course.credits
336        if self.student.entry_session < 2013:
337            ticket.passmark = course.passmark - 5
338        else:
339            ticket.passmark = course.passmark
340        ticket.semester = course.semester
341        self[ticket.code] = ticket
342        return
343
344    def addCertCourseTickets(self, cert):
345        """Collect all certificate courses and create course
346        tickets automatically.
347        """
348        if cert is not None:
349            for key, val in cert.items():
350                if val.level != self.level:
351                    continue
352                ticket = createObject(u'waeup.CourseTicket')
353                ticket.automatic = True
354                ticket.mandatory = val.mandatory
355                ticket.carry_over = False
356                try:
357                    self.addCourseTicket(ticket, val.course)
358                except TicketError:
359                    pass
360        return
361
362CustomStudentStudyLevel = attrs_to_fields(
363    CustomStudentStudyLevel, omit=[
364    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
365
366class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
367    """A factory for student study levels.
368    """
369
370    def __call__(self, *args, **kw):
371        return CustomStudentStudyLevel()
372
373    def getInterfaces(self):
374        return implementedBy(CustomStudentStudyLevel)
375
376class CustomCourseTicket(CourseTicket):
377    """This is a course ticket which allows the
378    student to attend the course. Lecturers will enter scores and more at
379    the end of the term.
380
381    A course ticket contains a copy of the original course and
382    course referrer data. If the courses and/or their referrers are removed, the
383    corresponding tickets remain unchanged. So we do not need any event
384    triggered actions on course tickets.
385    """
386    grok.implements(ICustomCourseTicket, IStudentNavigation)
387    grok.provides(ICustomCourseTicket)
388
389    @property
390    def _getGradeWeightFromScore(self):
391        """Nigerian Course Grading System
392        """
393        if self.score == -1:
394            return ('-',0) # core course and result not yet available (used by AAUE)
395        if self.total_score is None:
396            return (None, None)
397        if self.total_score >= 70:
398            return ('A',5)
399        if self.total_score >= 60:
400            return ('B',4)
401        if self.total_score >= 50:
402            return ('C',3)
403        if self.total_score >= 45:
404            return ('D',2)
405        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
406            return ('E',1)
407        return ('F',0)
408
409    @property
410    def total_score(self):
411        """Returns ca + score or imported total score.
412        """
413        # Override total_score if value has been imported
414        if getattr(self, 'imported_ts', None):
415            return self.imported_ts
416        if self.score == -1:
417            return 0
418        if not None in (self.score, self.ca):
419            return self.score + self.ca
420        return None
421
422    @property
423    def editable_by_lecturer(self):
424        """True if lecturer is allowed to edit the ticket.
425        """
426        try:
427            cas = grok.getSite()[
428                'configuration'].current_academic_session
429            if self.student.state in (VALIDATED, REGISTERED) and \
430                self.student.current_session == cas:
431                return True
432        except (AttributeError, TypeError): # in unit tests
433            pass
434        return False
435
436CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
437
438class CustomCourseTicketFactory(CourseTicketFactory):
439    """A factory for student study levels.
440    """
441
442    def __call__(self, *args, **kw):
443        return CustomCourseTicket()
444
445    def getInterfaces(self):
446        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.