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

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

ivama: Any student with less than 30 units in summary of result should not proceed no matter the GPA/CGPA.

  • Property svn:keywords set to Id
File size: 14.2 KB
RevLine 
[8326]1## $Id: studylevel.py 14464 2017-01-26 11:03:09Z 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
[13036]23import pytz
24from datetime import datetime
[8326]25from zope.component.interfaces import IFactory
[9502]26from zope.component import createObject
[8326]27from zope.interface import implementedBy
28from waeup.kofa.utils.helpers import attrs_to_fields
[13036]29from waeup.kofa.interfaces import CREATED
[14227]30from waeup.kofa.students.browser import TicketError
[8326]31from waeup.kofa.students.studylevel import (
32    StudentStudyLevel, CourseTicket,
33    CourseTicketFactory, StudentStudyLevelFactory)
[14075]34from waeup.kofa.students.interfaces import IStudentNavigation, ICourseTicket
[8444]35from waeup.aaue.students.interfaces import (
[8867]36    ICustomStudentStudyLevel, ICustomCourseTicket)
[14248]37from waeup.aaue.interfaces import MessageFactory as _
[8326]38
39
40class CustomStudentStudyLevel(StudentStudyLevel):
41    """This is a container for course tickets.
42    """
43    grok.implements(ICustomStudentStudyLevel, IStudentNavigation)
44    grok.provides(ICustomStudentStudyLevel)
45
[9914]46    @property
47    def total_credits_s1(self):
48        total = 0
49        for ticket in self.values():
50            if ticket.semester == 1:
51                total += ticket.credits
52        return total
53
54    @property
55    def total_credits_s2(self):
56        total = 0
57        for ticket in self.values():
58            if ticket.semester == 2:
59                total += ticket.credits
60        return total
61
[10443]62    @property
[13834]63    def gpa_params(self):
64        """Calculate gpa parameters for this level.
65        """
66        credits_weighted = 0.0
67        credits_counted = 0
68        level_gpa = 0.0
69        for ticket in self.values():
70            if None not in (ticket.score, ticket.ca):
71                credits_counted += ticket.credits
72                credits_weighted += ticket.credits * ticket.weight
73        if credits_counted:
[14384]74            level_gpa = credits_weighted/credits_counted
[14206]75        # Override level_gpa if value has been imported
76        imported_gpa = getattr(self, 'imported_gpa', None)
77        if imported_gpa:
78            level_gpa = imported_gpa
[13834]79        return level_gpa, credits_counted, credits_weighted
80
81    @property
[10480]82    def gpa_params_rectified(self):
83        return self.gpa_params
[10443]84
[13036]85    @property
[14357]86    def passed_params(self):
87        """Determine the number and credits of passed and failed courses.
88        This method is used for level reports.
89        """
90        passed = failed = 0
91        courses_failed = ''
92        credits_failed = 0
93        credits_passed = 0
[14393]94        courses_not_taken = ''
[14357]95        for ticket in self.values():
96            if None not in (ticket.score, ticket.ca):
97                if ticket.total_score < ticket.passmark:
98                    failed += 1
99                    credits_failed += ticket.credits
100                    if ticket.mandatory:
101                        courses_failed += 'm_%s_m ' % ticket.code
102                    else:
103                        courses_failed += '%s ' % ticket.code
104                else:
105                    passed += 1
106                    credits_passed += ticket.credits
[14370]107            else:
[14393]108                courses_not_taken += '%s ' % ticket.code
[14388]109        if not len(courses_failed):
[14417]110            courses_failed = 'NIL'
[14428]111        if not len(courses_not_taken):
112            courses_not_taken = 'NIL'
[14370]113        return (passed, failed, credits_passed,
114                credits_failed, courses_failed,
[14393]115                courses_not_taken)
[14357]116
117    @property
[14248]118    def course_registration_forbidden(self):
[14249]119        fac_dep_paid = True
[14248]120        if self.student.entry_session >= 2016:
[14249]121            fac_dep_paid = False
[14248]122            for ticket in self.student['payments'].values():
123                if ticket.p_category == 'fac_dep' and \
124                    ticket.p_session == self.level_session and \
125                    ticket.p_state == 'paid':
126                        fac_dep_paid = True
127                        continue
128        if not fac_dep_paid:
129            return _("Please pay faculty and departmental dues first.")
[14375]130        restitution_paid = True
131        if self.student.entry_session < 2016 \
132            and self.student.current_mode == 'ug_ft':
133            restitution_paid = False
134            for ticket in self.student['payments'].values():
135                if ticket.p_category == 'restitution' and \
136                    ticket.p_session == self.level_session and \
137                    ticket.p_state == 'paid':
138                        restitution_paid = True
139                        continue
140        if not restitution_paid:
141            return _("Please pay restitution fee first.")
[13036]142        if self.student.is_fresh:
[14248]143            return
[13036]144        try:
[14117]145            if self.student.is_postgrad:
146                deadline = grok.getSite()['configuration'][
147                        str(self.level_session)].coursereg_deadline_pg
148            elif self.student.current_mode.startswith('dp'):
149                deadline = grok.getSite()['configuration'][
150                        str(self.level_session)].coursereg_deadline_dp
151            elif self.student.current_mode.endswith('_pt'):
152                deadline = grok.getSite()['configuration'][
153                        str(self.level_session)].coursereg_deadline_pt
154            elif self.student.current_mode == 'found':
155                deadline = grok.getSite()['configuration'][
156                        str(self.level_session)].coursereg_deadline_found
157            else:
158                deadline = grok.getSite()['configuration'][
159                        str(self.level_session)].coursereg_deadline
[13071]160        except (TypeError, KeyError):
[14248]161            return
[13070]162        if not deadline or deadline > datetime.now(pytz.utc):
[14248]163            return
[13036]164        if len(self.student['payments']):
165            for ticket in self.student['payments'].values():
166                if ticket.p_category == 'late_registration' and \
167                    ticket.p_session == self.level_session and \
168                    ticket.p_state == 'paid':
[14248]169                        return
170        return _("Course registration has ended. "
171                 "Please pay the late registration fee.")
[13036]172
[14082]173    # only AAUE
174    @property
175    def remark(self):
[14161]176        certificate = getattr(self.__parent__,'certificate',None)
177        end_level = getattr(certificate, 'end_level', None)
[14380]178        failed_limit = 1.5
179        if self.student.entry_session < 2013:
180            failed_limit = 1.0
[14377]181        # final level student remark
182        if end_level and self.level >= end_level:
[14464]183            if self.level > end_level:
184                # spill-over level
185                if self.gpa_params[1] == 0:
186                    # no credits taken
187                    return 'NEOR'
188            else:
189                if self.gpa_params[1] < 30:
190                    # credits taken below limit
191                    return 'NEOR'
[14415]192            if self.level_verdict in ('FRNS', 'NEOR', 'NEOV'):
193                return self.level_verdict
[14412]194            if '_m' in self.passed_params[4]:
[14161]195                return 'FRNS'
[14412]196            if not self.cumulative_params[0]:
197                return 'FRNS'
[14435]198            if len(self.passed_params[5]) \
199                and not self.passed_params[5] == 'NIL':
[14412]200                return 'FRNS'
[14380]201            if self.cumulative_params[0] < failed_limit:
[14161]202                return 'Fail'
[14463]203            if self.cumulative_params[0] < 1.5:
204                return 'Pass'
[14161]205            if self.cumulative_params[0] < 2.4:
[14327]206                return '3s_rd_s'
[14161]207            if self.cumulative_params[0] < 3.5:
[14327]208                return '2s_2_s'
[14161]209            if self.cumulative_params[0] < 4.5:
[14327]210                return '2s_1_s'
[14161]211            if self.cumulative_params[0] < 5.1:
[14327]212                return '1s_st_s'
[14161]213            return 'N/A'
214        # returning student remark
[14444]215        if self.gpa_params[1] < 30:
[14464]216            # credits taken below limit
[14415]217            return 'Probation'
218        if self.level_verdict in ('FRNS', 'NEOR', 'NEOV'):
219            return 'Probation'
[14380]220        if self.cumulative_params[0] < failed_limit:
[14082]221            return 'Probation'
222        if self.cumulative_params[0] < 5.1:
223            return 'Proceed'
224        return 'N/A'
225
[14227]226    def _schoolfeePaymentMade(self):
227        if len(self.student['payments']):
228            for ticket in self.student['payments'].values():
229                if ticket.p_state == 'paid' and \
230                    ticket.p_category in (
231                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
232                    ticket.p_session == self.student[
233                        'studycourse'].current_session:
234                    return True
235        return False
236
[14259]237    def _coursePaymentsMade(self, course):
[14266]238        if self.level_session < 2016:
239            return True
[14259]240        if not course.code[:3] in ('GST', 'ENT'):
241            return True
242        if len(self.student['payments']):
243            paid_cats = list()
244            for pticket in self.student['payments'].values():
245                if pticket.p_state == 'paid':
246                    paid_cats.append(pticket.p_category)
247            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
248                not 'gst_registration_1' in paid_cats:
249                return False
250            if course.code in ('GST222',) and \
251                not 'gst_registration_2' in paid_cats:
252                return False
253            if course.code in ('ENT201',) and \
[14355]254                not 'ent_registration_1' in paid_cats:
[14259]255                return False
[14355]256            if course.code in ('GST101', 'GST102') and \
[14348]257                not 'gst_text_book_1' in paid_cats and \
[14355]258                not 'gst_text_book_0' in paid_cats:
[14259]259                return False
[14355]260            if course.code in ('GST111', 'GST112') and \
[14348]261                not 'gst_text_book_2' in paid_cats and \
[14355]262                not 'gst_text_book_0' in paid_cats:
[14348]263                return False
[14259]264            if course.code in ('GST222',) and \
265                not 'gst_text_book_3' in paid_cats:
266                return False
267            if course.code in ('ENT201',) and \
[14355]268                not 'ent_text_book_1' in paid_cats:
[14259]269                return False
270            return True
271        return False
272
[14075]273    def addCourseTicket(self, ticket, course):
274        """Add a course ticket object.
275        """
276        if not ICourseTicket.providedBy(ticket):
277            raise TypeError(
278                'StudentStudyLeves contain only ICourseTicket instances')
[14227]279        # Raise TicketError if course is in 2nd semester but
280        # schoolfee has not yet been fully paid.
281        if course.semester == 2 and not self._schoolfeePaymentMade():
[14252]282            raise TicketError(
283                _('%s is a 2nd semester course which can only be added '
284                  'if school fees have been fully paid.' % course.code))
[14259]285        # Raise TicketError if registration fee or text
286        # book fee haven't been paid.
287        if not self._coursePaymentsMade(course):
288            raise TicketError(
289                _('%s can only be added if both registration fee and text '
290                  'book fee have been paid.'
291                  % course.code))
[14075]292        ticket.code = course.code
293        ticket.title = course.title
294        ticket.fcode = course.__parent__.__parent__.__parent__.code
295        ticket.dcode = course.__parent__.__parent__.code
296        ticket.credits = course.credits
297        if self.student.entry_session < 2013:
298            ticket.passmark = course.passmark - 5
299        else:
300            ticket.passmark = course.passmark
301        ticket.semester = course.semester
302        self[ticket.code] = ticket
303        return
304
[14227]305    def addCertCourseTickets(self, cert):
306        """Collect all certificate courses and create course
307        tickets automatically.
308        """
309        if cert is not None:
310            for key, val in cert.items():
311                if val.level != self.level:
312                    continue
313                ticket = createObject(u'waeup.CourseTicket')
314                ticket.automatic = True
315                ticket.mandatory = val.mandatory
316                ticket.carry_over = False
317                try:
318                    self.addCourseTicket(ticket, val.course)
319                except TicketError:
320                    pass
321        return
322
[9692]323CustomStudentStudyLevel = attrs_to_fields(
[9914]324    CustomStudentStudyLevel, omit=[
[10480]325    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
[8326]326
327class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
328    """A factory for student study levels.
329    """
330
331    def __call__(self, *args, **kw):
332        return CustomStudentStudyLevel()
333
334    def getInterfaces(self):
335        return implementedBy(CustomStudentStudyLevel)
336
337class CustomCourseTicket(CourseTicket):
338    """This is a course ticket which allows the
339    student to attend the course. Lecturers will enter scores and more at
340    the end of the term.
341
342    A course ticket contains a copy of the original course and
343    course referrer data. If the courses and/or their referrers are removed, the
344    corresponding tickets remain unchanged. So we do not need any event
345    triggered actions on course tickets.
346    """
347    grok.implements(ICustomCourseTicket, IStudentNavigation)
348    grok.provides(ICustomCourseTicket)
349
[13834]350    @property
[14136]351    def total_score(self):
352        """Returns ca + score.
353        """
354        if not None in (self.score, self.ca):
355            return self.score + self.ca
356
[14288]357    @property
358    def editable_by_lecturer(self):
359        """True if lecturer is allowed to edit the ticket.
360        """
361        return True
362
[8326]363CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
364
365class CustomCourseTicketFactory(CourseTicketFactory):
366    """A factory for student study levels.
367    """
368
369    def __call__(self, *args, **kw):
370        return CustomCourseTicket()
371
372    def getInterfaces(self):
373        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.