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

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

Extend condition list for FRNS remark.

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