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

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

Remark depends on studylevel.level not student.current_level.

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