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

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

Replace 'Nil' by 'NIL'.

  • Property svn:keywords set to Id
File size: 13.8 KB
Line 
1## $Id: studylevel.py 14417 2017-01-18 10:26:07Z 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.interfaces import MessageFactory as _
38
39
40class CustomStudentStudyLevel(StudentStudyLevel):
41    """This is a container for course tickets.
42    """
43    grok.implements(ICustomStudentStudyLevel, IStudentNavigation)
44    grok.provides(ICustomStudentStudyLevel)
45
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
62    @property
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:
74            level_gpa = credits_weighted/credits_counted
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
79        return level_gpa, credits_counted, credits_weighted
80
81    @property
82    def gpa_params_rectified(self):
83        return self.gpa_params
84
85    @property
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
94        courses_not_taken = ''
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
107            else:
108                courses_not_taken += '%s ' % ticket.code
109        if not len(courses_failed):
110            courses_failed = 'NIL'
111        return (passed, failed, credits_passed,
112                credits_failed, courses_failed,
113                courses_not_taken)
114
115    @property
116    def course_registration_forbidden(self):
117        fac_dep_paid = True
118        if self.student.entry_session >= 2016:
119            fac_dep_paid = False
120            for ticket in self.student['payments'].values():
121                if ticket.p_category == 'fac_dep' and \
122                    ticket.p_session == self.level_session and \
123                    ticket.p_state == 'paid':
124                        fac_dep_paid = True
125                        continue
126        if not fac_dep_paid:
127            return _("Please pay faculty and departmental dues first.")
128        restitution_paid = True
129        if self.student.entry_session < 2016 \
130            and self.student.current_mode == 'ug_ft':
131            restitution_paid = False
132            for ticket in self.student['payments'].values():
133                if ticket.p_category == 'restitution' and \
134                    ticket.p_session == self.level_session and \
135                    ticket.p_state == 'paid':
136                        restitution_paid = True
137                        continue
138        if not restitution_paid:
139            return _("Please pay restitution fee first.")
140        if self.student.is_fresh:
141            return
142        try:
143            if self.student.is_postgrad:
144                deadline = grok.getSite()['configuration'][
145                        str(self.level_session)].coursereg_deadline_pg
146            elif self.student.current_mode.startswith('dp'):
147                deadline = grok.getSite()['configuration'][
148                        str(self.level_session)].coursereg_deadline_dp
149            elif self.student.current_mode.endswith('_pt'):
150                deadline = grok.getSite()['configuration'][
151                        str(self.level_session)].coursereg_deadline_pt
152            elif self.student.current_mode == 'found':
153                deadline = grok.getSite()['configuration'][
154                        str(self.level_session)].coursereg_deadline_found
155            else:
156                deadline = grok.getSite()['configuration'][
157                        str(self.level_session)].coursereg_deadline
158        except (TypeError, KeyError):
159            return
160        if not deadline or deadline > datetime.now(pytz.utc):
161            return
162        if len(self.student['payments']):
163            for ticket in self.student['payments'].values():
164                if ticket.p_category == 'late_registration' and \
165                    ticket.p_session == self.level_session and \
166                    ticket.p_state == 'paid':
167                        return
168        return _("Course registration has ended. "
169                 "Please pay the late registration fee.")
170
171    # only AAUE
172    @property
173    def remark(self):
174        certificate = getattr(self.__parent__,'certificate',None)
175        end_level = getattr(certificate, 'end_level', None)
176        failed_limit = 1.5
177        if self.student.entry_session < 2013:
178            failed_limit = 1.0
179        # final level student remark
180        if end_level and self.level >= end_level:
181            if self.gpa_params[1] == 0:
182                # No credits weighted at this level
183                return 'FRNS'
184            if self.level_verdict in ('FRNS', 'NEOR', 'NEOV'):
185                return self.level_verdict
186            if '_m' in self.passed_params[4]:
187                return 'FRNS'
188            if not self.cumulative_params[0]:
189                return 'FRNS'
190            if len(self.passed_params[5]):
191                return 'FRNS'
192            if self.cumulative_params[0] < failed_limit:
193                return 'Fail'
194            if self.cumulative_params[0] < 2.4:
195                return '3s_rd_s'
196            if self.cumulative_params[0] < 3.5:
197                return '2s_2_s'
198            if self.cumulative_params[0] < 4.5:
199                return '2s_1_s'
200            if self.cumulative_params[0] < 5.1:
201                return '1s_st_s'
202            return 'N/A'
203        # returning student remark
204        if self.gpa_params[1] == 0:
205            # No credits weighted at this level
206            return 'Probation'
207        if self.level_verdict in ('FRNS', 'NEOR', 'NEOV'):
208            return 'Probation'
209        if self.cumulative_params[0] < failed_limit:
210            return 'Probation'
211        if self.cumulative_params[0] < 5.1:
212            return 'Proceed'
213        return 'N/A'
214
215    def _schoolfeePaymentMade(self):
216        if len(self.student['payments']):
217            for ticket in self.student['payments'].values():
218                if ticket.p_state == 'paid' and \
219                    ticket.p_category in (
220                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
221                    ticket.p_session == self.student[
222                        'studycourse'].current_session:
223                    return True
224        return False
225
226    def _coursePaymentsMade(self, course):
227        if self.level_session < 2016:
228            return True
229        if not course.code[:3] in ('GST', 'ENT'):
230            return True
231        if len(self.student['payments']):
232            paid_cats = list()
233            for pticket in self.student['payments'].values():
234                if pticket.p_state == 'paid':
235                    paid_cats.append(pticket.p_category)
236            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
237                not 'gst_registration_1' in paid_cats:
238                return False
239            if course.code in ('GST222',) and \
240                not 'gst_registration_2' in paid_cats:
241                return False
242            if course.code in ('ENT201',) and \
243                not 'ent_registration_1' in paid_cats:
244                return False
245            if course.code in ('GST101', 'GST102') and \
246                not 'gst_text_book_1' in paid_cats and \
247                not 'gst_text_book_0' in paid_cats:
248                return False
249            if course.code in ('GST111', 'GST112') and \
250                not 'gst_text_book_2' in paid_cats and \
251                not 'gst_text_book_0' in paid_cats:
252                return False
253            if course.code in ('GST222',) and \
254                not 'gst_text_book_3' in paid_cats:
255                return False
256            if course.code in ('ENT201',) and \
257                not 'ent_text_book_1' in paid_cats:
258                return False
259            return True
260        return False
261
262    def addCourseTicket(self, ticket, course):
263        """Add a course ticket object.
264        """
265        if not ICourseTicket.providedBy(ticket):
266            raise TypeError(
267                'StudentStudyLeves contain only ICourseTicket instances')
268        # Raise TicketError if course is in 2nd semester but
269        # schoolfee has not yet been fully paid.
270        if course.semester == 2 and not self._schoolfeePaymentMade():
271            raise TicketError(
272                _('%s is a 2nd semester course which can only be added '
273                  'if school fees have been fully paid.' % course.code))
274        # Raise TicketError if registration fee or text
275        # book fee haven't been paid.
276        if not self._coursePaymentsMade(course):
277            raise TicketError(
278                _('%s can only be added if both registration fee and text '
279                  'book fee have been paid.'
280                  % course.code))
281        ticket.code = course.code
282        ticket.title = course.title
283        ticket.fcode = course.__parent__.__parent__.__parent__.code
284        ticket.dcode = course.__parent__.__parent__.code
285        ticket.credits = course.credits
286        if self.student.entry_session < 2013:
287            ticket.passmark = course.passmark - 5
288        else:
289            ticket.passmark = course.passmark
290        ticket.semester = course.semester
291        self[ticket.code] = ticket
292        return
293
294    def addCertCourseTickets(self, cert):
295        """Collect all certificate courses and create course
296        tickets automatically.
297        """
298        if cert is not None:
299            for key, val in cert.items():
300                if val.level != self.level:
301                    continue
302                ticket = createObject(u'waeup.CourseTicket')
303                ticket.automatic = True
304                ticket.mandatory = val.mandatory
305                ticket.carry_over = False
306                try:
307                    self.addCourseTicket(ticket, val.course)
308                except TicketError:
309                    pass
310        return
311
312CustomStudentStudyLevel = attrs_to_fields(
313    CustomStudentStudyLevel, omit=[
314    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])
315
316class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
317    """A factory for student study levels.
318    """
319
320    def __call__(self, *args, **kw):
321        return CustomStudentStudyLevel()
322
323    def getInterfaces(self):
324        return implementedBy(CustomStudentStudyLevel)
325
326class CustomCourseTicket(CourseTicket):
327    """This is a course ticket which allows the
328    student to attend the course. Lecturers will enter scores and more at
329    the end of the term.
330
331    A course ticket contains a copy of the original course and
332    course referrer data. If the courses and/or their referrers are removed, the
333    corresponding tickets remain unchanged. So we do not need any event
334    triggered actions on course tickets.
335    """
336    grok.implements(ICustomCourseTicket, IStudentNavigation)
337    grok.provides(ICustomCourseTicket)
338
339    @property
340    def total_score(self):
341        """Returns ca + score.
342        """
343        if not None in (self.score, self.ca):
344            return self.score + self.ca
345
346    @property
347    def editable_by_lecturer(self):
348        """True if lecturer is allowed to edit the ticket.
349        """
350        return True
351
352CustomCourseTicket = attrs_to_fields(CustomCourseTicket)
353
354class CustomCourseTicketFactory(CourseTicketFactory):
355    """A factory for student study levels.
356    """
357
358    def __call__(self, *args, **kw):
359        return CustomCourseTicket()
360
361    def getInterfaces(self):
362        return implementedBy(CustomCourseTicket)
Note: See TracBrowser for help on using the repository browser.