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

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

Lecturers can edit current session and previous session courses whenever they like. All restrictions are removed.

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