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

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

Students must take 30 units to proceed.

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