## $Id: studylevel.py 14613 2017-03-09 07:42:05Z henrik $
##
## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
"""
Container which holds the data of a student study level
and contains the course tickets.
"""
import grok
import pytz
from datetime import datetime
from zope.component.interfaces import IFactory
from zope.component import createObject
from zope.interface import implementedBy
from waeup.kofa.utils.helpers import attrs_to_fields
from waeup.kofa.interfaces import CREATED
from waeup.kofa.students.browser import TicketError
from waeup.kofa.students.studylevel import (
    StudentStudyLevel, CourseTicket,
    CourseTicketFactory, StudentStudyLevelFactory)
from waeup.kofa.students.interfaces import IStudentNavigation, ICourseTicket
from waeup.aaue.students.interfaces import (
    ICustomStudentStudyLevel, ICustomCourseTicket)
from waeup.aaue.interfaces import MessageFactory as _


class CustomStudentStudyLevel(StudentStudyLevel):
    """This is a container for course tickets.
    """
    grok.implements(ICustomStudentStudyLevel, IStudentNavigation)
    grok.provides(ICustomStudentStudyLevel)

    @property
    def total_credits_s1(self):
        total = 0
        for ticket in self.values():
            if ticket.semester == 1 and not ticket.outstanding:
                total += ticket.credits
        return total

    @property
    def total_credits_s2(self):
        total = 0
        for ticket in self.values():
            if ticket.semester == 2 and not ticket.outstanding:
                total += ticket.credits
        return total

    @property
    def gpa_params(self):
        """Calculate gpa parameters for this level.
        """
        credits_weighted = 0.0
        credits_counted = 0
        level_gpa = 0.0
        for ticket in self.values():
            if ticket.total_score is not None:
                credits_counted += ticket.credits
                credits_weighted += ticket.credits * ticket.weight
        if credits_counted:
            level_gpa = credits_weighted/credits_counted
        # Override level_gpa if value has been imported
        imported_gpa = getattr(self, 'imported_gpa', None)
        if imported_gpa:
            level_gpa = imported_gpa
        return level_gpa, credits_counted, credits_weighted

    @property
    def gpa_params_rectified(self):
        return self.gpa_params

    @property
    def passed_params(self):
        """Determine the number and credits of passed and failed courses.
        This method is used for level reports.
        """
        passed = failed = 0
        courses_failed = ''
        credits_failed = 0
        credits_passed = 0
        courses_not_taken = ''
        for ticket in self.values():
            if ticket.total_score is not None:
                if ticket.total_score < ticket.passmark:
                    failed += 1
                    credits_failed += ticket.credits
                    if ticket.mandatory:
                        courses_failed += 'm_%s_m ' % ticket.code
                    else:
                        courses_failed += '%s ' % ticket.code
                else:
                    passed += 1
                    credits_passed += ticket.credits
            else:
                courses_not_taken += '%s ' % ticket.code
        if not len(courses_failed):
            courses_failed = 'Nil'
        if not len(courses_not_taken):
            courses_not_taken = 'Nil'
        return (passed, failed, credits_passed,
                credits_failed, courses_failed,
                courses_not_taken)

    @property
    def course_registration_forbidden(self):
        #fac_dep_paid = True
        #if self.student.entry_session >= 2016:
        #    fac_dep_paid = False
        #    for ticket in self.student['payments'].values():
        #        if ticket.p_category == 'fac_dep' and \
        #            ticket.p_session == self.level_session and \
        #            ticket.p_state == 'paid':
        #                fac_dep_paid = True
        #                continue
        #if not fac_dep_paid:
        #    return _("Please pay faculty and departmental dues first.")


        ######################################################
        # Temporarily disable ug_ft course registration
        #if self.student.current_mode == 'ug_ft':
        #    return _("Please check back later, course registration is not yet available.")
        ######################################################


        restitution_paid = True
        if self.student.entry_session < 2016 \
            and self.student.current_mode == 'ug_ft':
            restitution_paid = False
            for ticket in self.student['payments'].values():
                if ticket.p_category == 'restitution' and \
                    ticket.p_session == self.level_session and \
                    ticket.p_state == 'paid':
                        restitution_paid = True
                        continue
        if not restitution_paid:
            return _("Please pay restitution fee first.")
        if self.student.is_fresh:
            return
        try:
            if self.student.is_postgrad:
                deadline = grok.getSite()['configuration'][
                        str(self.level_session)].coursereg_deadline_pg
            elif self.student.current_mode.startswith('dp'):
                deadline = grok.getSite()['configuration'][
                        str(self.level_session)].coursereg_deadline_dp
            elif self.student.current_mode.endswith('_pt'):
                deadline = grok.getSite()['configuration'][
                        str(self.level_session)].coursereg_deadline_pt
            elif self.student.current_mode == 'found':
                deadline = grok.getSite()['configuration'][
                        str(self.level_session)].coursereg_deadline_found
            else:
                deadline = grok.getSite()['configuration'][
                        str(self.level_session)].coursereg_deadline
        except (TypeError, KeyError):
            return
        if not deadline or deadline > datetime.now(pytz.utc):
            return
        if len(self.student['payments']):
            for ticket in self.student['payments'].values():
                if ticket.p_category == 'late_registration' and \
                    ticket.p_session == self.level_session and \
                    ticket.p_state == 'paid':
                        return
        return _("Course registration has ended. "
                 "Please pay the late registration fee.")

    # only AAUE
    @property
    def remark(self):
        certificate = getattr(self.__parent__,'certificate',None)
        end_level = getattr(certificate, 'end_level', None)
        failed_limit = 1.5
        if self.student.entry_session < 2013:
            failed_limit = 1.0
        # final level student remark
        if end_level and self.level >= end_level:
            if self.level > end_level:
                # spill-over level
                if self.gpa_params[1] == 0:
                    # no credits taken
                    return 'NER'
            elif self.gpa_params[1] < 30:
                # credits taken below limit
                return 'NER'
            if self.level_verdict in ('FRNS', 'NER', 'NYV'):
                return self.level_verdict
            if '_m' in self.passed_params[4]:
                return 'FRNS'
            if not self.cumulative_params[0]:
                return 'FRNS'
            if len(self.passed_params[5]) \
                and not self.passed_params[5] == 'Nil':
                return 'FRNS'
            if self.cumulative_params[0] < failed_limit:
                return 'Fail'
            if self.cumulative_params[0] < 1.5:
                return 'Pass'
            if self.cumulative_params[0] < 2.4:
                return '3s_rd_s'
            if self.cumulative_params[0] < 3.5:
                return '2s_2_s'
            if self.cumulative_params[0] < 4.5:
                return '2s_1_s'
            if self.cumulative_params[0] < 5.1:
                return '1s_st_s'
            return 'N/A'
        # returning student remark
        if self.level_verdict in ('FRNS', 'NER', 'NYV'):
            return 'Probation'
        if self.level_verdict == 'D':
            return 'Withdrawn'
        if self.gpa_params[1] < 30:
            # credits taken below limit
            return 'Probation'
        if self.cumulative_params[0] < failed_limit:
            return 'Probation'
        if self.cumulative_params[0] < 5.1:
            return 'Proceed'
        return 'N/A'

    def _schoolfeePaymentMade(self):
        if len(self.student['payments']):
            for ticket in self.student['payments'].values():
                if ticket.p_state == 'paid' and \
                    ticket.p_category in (
                        'schoolfee', 'schoolfee_incl', 'schoolfee_2',)  and \
                    ticket.p_session == self.student[
                        'studycourse'].current_session:
                    return True
        return False

    def _coursePaymentsMade(self, course):
        if self.level_session < 2016:
            return True
        if not course.code[:3] in ('GST', 'ENT'):
            return True
        if len(self.student['payments']):
            paid_cats = list()
            for pticket in self.student['payments'].values():
                if pticket.p_state == 'paid':
                    paid_cats.append(pticket.p_category)
            if course.code in ('GST101', 'GST102', 'GST111', 'GST112') and \
                not 'gst_registration_1' in paid_cats:
                return False
            if course.code in ('GST222',) and \
                not 'gst_registration_2' in paid_cats:
                return False
            if course.code in ('ENT201',) and \
                not 'ent_registration_1' in paid_cats:
                return False
            if course.code in ('GST101', 'GST102') and \
                not 'gst_text_book_1' in paid_cats and \
                not 'gst_text_book_0' in paid_cats:
                return False
            if course.code in ('GST111', 'GST112') and \
                not 'gst_text_book_2' in paid_cats and \
                not 'gst_text_book_0' in paid_cats:
                return False
            if course.code in ('GST222',) and \
                not 'gst_text_book_3' in paid_cats:
                return False
            if course.code in ('ENT201',) and \
                not 'ent_text_book_1' in paid_cats:
                return False
            return True
        return False

    def addCourseTicket(self, ticket, course):
        """Add a course ticket object.
        """
        if not ICourseTicket.providedBy(ticket):
            raise TypeError(
                'StudentStudyLeves contain only ICourseTicket instances')
        # Raise TicketError if course is in 2nd semester but
        # schoolfee has not yet been fully paid.
        if course.semester == 2 and not self._schoolfeePaymentMade():
            raise TicketError(
                _('%s is a 2nd semester course which can only be added '
                  'if school fees have been fully paid.' % course.code))
        # Raise TicketError if registration fee or text
        # book fee haven't been paid.
        if not self._coursePaymentsMade(course):
            raise TicketError(
                _('%s can only be added if both registration fee and text '
                  'book fee have been paid.'
                  % course.code))
        ticket.code = course.code
        ticket.title = course.title
        ticket.fcode = course.__parent__.__parent__.__parent__.code
        ticket.dcode = course.__parent__.__parent__.code
        ticket.credits = course.credits
        if self.student.entry_session < 2013:
            ticket.passmark = course.passmark - 5
        else:
            ticket.passmark = course.passmark
        ticket.semester = course.semester
        self[ticket.code] = ticket
        return

    def addCertCourseTickets(self, cert):
        """Collect all certificate courses and create course
        tickets automatically.
        """
        if cert is not None:
            for key, val in cert.items():
                if val.level != self.level:
                    continue
                ticket = createObject(u'waeup.CourseTicket')
                ticket.automatic = True
                ticket.mandatory = val.mandatory
                ticket.carry_over = False
                try:
                    self.addCourseTicket(ticket, val.course)
                except TicketError:
                    pass
        return

CustomStudentStudyLevel = attrs_to_fields(
    CustomStudentStudyLevel, omit=[
    'total_credits', 'total_credits_s1', 'total_credits_s2', 'gpa'])

class CustomStudentStudyLevelFactory(StudentStudyLevelFactory):
    """A factory for student study levels.
    """

    def __call__(self, *args, **kw):
        return CustomStudentStudyLevel()

    def getInterfaces(self):
        return implementedBy(CustomStudentStudyLevel)

class CustomCourseTicket(CourseTicket):
    """This is a course ticket which allows the
    student to attend the course. Lecturers will enter scores and more at
    the end of the term.

    A course ticket contains a copy of the original course and
    course referrer data. If the courses and/or their referrers are removed, the
    corresponding tickets remain unchanged. So we do not need any event
    triggered actions on course tickets.
    """
    grok.implements(ICustomCourseTicket, IStudentNavigation)
    grok.provides(ICustomCourseTicket)

    @property
    def _getGradeWeightFromScore(self):
        """Nigerian Course Grading System
        """
        if self.score == -1:
            return ('-',0) # core course and result not yet available (used by AAUE)
        if self.total_score is None:
            return (None, None)
        if self.total_score >= 70:
            return ('A',5)
        if self.total_score >= 60:
            return ('B',4)
        if self.total_score >= 50:
            return ('C',3)
        if self.total_score >= 45:
            return ('D',2)
        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
            return ('E',1)
        return ('F',0)

    @property
    def total_score(self):
        """Returns ca + score.
        """
        if self.score == -1:
            return 0
        if not None in (self.score, self.ca):
            return self.score + self.ca
        return None

    @property
    def editable_by_lecturer(self):
        """True if lecturer is allowed to edit the ticket.
        """
        return True

CustomCourseTicket = attrs_to_fields(CustomCourseTicket)

class CustomCourseTicketFactory(CourseTicketFactory):
    """A factory for student study levels.
    """

    def __call__(self, *args, **kw):
        return CustomCourseTicket()

    def getInterfaces(self):
        return implementedBy(CustomCourseTicket)
