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

Last change on this file since 14840 was 14675, checked in by Henrik Bettermann, 8 years ago

Disable "ent_registration_1" and "ent_text_book_1" as criteria for submission of course lists.

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