## $Id: utils.py 9450 2012-10-29 06:54:51Z henrik $
##
## Copyright (C) 2011 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
##
"""General helper functions and utilities for the student section.
"""
import grok
from time import time
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph, Image, Table, Spacer
from zope.component import getUtility, createObject
from zope.formlib.form import setUpEditWidgets
from zope.i18n import translate
from waeup.kofa.interfaces import (
    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED)
from waeup.kofa.interfaces import MessageFactory as _
from waeup.kofa.students.interfaces import IStudentsUtils

SLIP_STYLE = [
    ('VALIGN',(0,0),(-1,-1),'TOP'),
    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
    ]

CONTENT_STYLE = [
    ('VALIGN',(0,0),(-1,-1),'TOP'),
    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
    ('BACKGROUND',(0,0),(-1,0),colors.black),
    ]

FONT_SIZE = 10
FONT_COLOR = 'black'

def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
    tag1 ='<font color=%s size=%d>' % (color, size)
    return tag1 + '%s:</font>'

def trans(text, lang):
    # shortcut
    return translate(text, 'waeup.kofa', target_language=lang)

def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
    """Turn `text`, `color` and `size` into an HTML snippet.

    The snippet is suitable for use with reportlab and generating PDFs.
    Wraps the `text` into a ``<font>`` tag with passed attributes.

    Also non-strings are converted. Raw strings are expected to be
    utf-8 encoded (usually the case for widgets etc.).

    Finally, a br tag is added if widgets contain div tags
    which are not supported by reportlab.

    The returned snippet is unicode type.
    """
    try:
        # In unit tests IKofaUtils has not been registered
        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
    except:
        portal_language = 'en'
    if not isinstance(text, unicode):
        if isinstance(text, basestring):
            text = text.decode('utf-8')
        else:
            text = unicode(text)
    # Mainly for boolean values we need our customized
    # localisation of the zope domain
    text = translate(text, 'zope', target_language=portal_language)
    text = text.replace('</div>', '<br /></div>')
    tag1 = u'<font color="%s" size="%d">' % (color, size)
    return tag1 + u'%s</font>' % text

def generate_student_id():
    students = grok.getSite()['students']
    new_id = students.unique_student_id
    return new_id

def set_up_widgets(view, ignore_request=False):
    view.adapters = {}
    view.widgets = setUpEditWidgets(
        view.form_fields, view.prefix, view.context, view.request,
        adapters=view.adapters, for_display=True,
        ignore_request=ignore_request
        )

def render_student_data(studentview):
    """Render student table for an existing frame.
    """
    width, height = A4
    set_up_widgets(studentview, ignore_request=True)
    data_left = []
    data_right = []
    style = getSampleStyleSheet()
    img = getUtility(IExtFileStore).getFileByContext(
        studentview.context, attr='passport.jpg')
    if img is None:
        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
    data_left.append([doc_img])
    #data.append([Spacer(1, 12)])
    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE

    f_label = formatted_label(size=12) % _('Name')
    f_label = Paragraph(f_label, style["Normal"])
    f_text = formatted_text(studentview.context.display_fullname, size=12)
    f_text = Paragraph(f_text, style["Normal"])
    data_right.append([f_label,f_text])

    for widget in studentview.widgets:
        if 'name' in widget.name:
            continue
        f_label = formatted_label(size=12) % translate(
            widget.label.strip(), 'waeup.kofa',
            target_language=portal_language)
        f_label = Paragraph(f_label, style["Normal"])
        f_text = formatted_text(widget(), size=12)
        f_text = Paragraph(f_text, style["Normal"])
        data_right.append([f_label,f_text])

    if getattr(studentview.context, 'certcode'):
        f_label = formatted_label(size=12) % _('Study Course')
        f_label = Paragraph(f_label, style["Normal"])
        f_text = formatted_text(
            studentview.context['studycourse'].certificate.longtitle(), size=12)
        f_text = Paragraph(f_text, style["Normal"])
        data_right.append([f_label,f_text])

        f_label = formatted_label(size=12) % _('Department')
        f_label = Paragraph(f_label, style["Normal"])
        f_text = formatted_text(
            studentview.context[
            'studycourse'].certificate.__parent__.__parent__.longtitle(),
            size=12)
        f_text = Paragraph(f_text, style["Normal"])
        data_right.append([f_label,f_text])

        f_label = formatted_label(size=12) % _('Faculty')
        f_label = Paragraph(f_label, style["Normal"])
        f_text = formatted_text(
            studentview.context[
            'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle(),
            size=12)
        f_text = Paragraph(f_text, style["Normal"])
        data_right.append([f_label,f_text])

    table_left = Table(data_left,style=SLIP_STYLE)
    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
    table = Table([[table_left, table_right],],style=SLIP_STYLE)
    return table

def render_table_data(tableheader,tabledata):
    """Render children table for an existing frame.
    """
    data = []
    #data.append([Spacer(1, 12)])
    line = []
    style = getSampleStyleSheet()
    for element in tableheader:
        field = formatted_text(element[0], color='white')
        field = Paragraph(field, style["Normal"])
        line.append(field)
    data.append(line)
    for ticket in tabledata:
        line = []
        for element in tableheader:
              field = formatted_text(getattr(ticket,element[1],u' '))
              field = Paragraph(field, style["Normal"])
              line.append(field)
        data.append(line)
    table = Table(data,colWidths=[
        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
    return table

def get_signature_table(signatures, lang='en'):
    """Return a reportlab table containing signature fields (with date).
    """
    style = getSampleStyleSheet()
    space_width = 0.4  # width in cm of space between signatures
    table_width = 16.0 # supposed width of signature table in cms
    # width of signature cells in cm...
    sig_col_width = table_width - ((len(signatures) - 1) * space_width)
    sig_col_width = sig_col_width / len(signatures)
    data = []
    col_widths = [] # widths of columns

    sig_style = [
        ('VALIGN',(0,-1),(-1,-1),'TOP'),
        ('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
        ('BOTTOMPADDING', (0,0), (-1,0), 36),
        ('TOPPADDING', (0,-1), (-1,-1), 0),
        ]
    for num, elem in enumerate(signatures):
        # draw a line above each signature cell (not: empty cells in between)
        sig_style.append(
            ('LINEABOVE', (num*2,-1), (num*2, -1), 1, colors.black))

    row = []
    for signature in signatures:
        row.append(trans(_('Date:'), lang))
        row.append('')
        if len(signatures) > 1:
            col_widths.extend([sig_col_width*cm, space_width*cm])
        else:
            col_widths.extend([sig_col_width/2*cm, sig_col_width/2*cm])
            row.append('') # empty spaceholder on right
    data.append(row[:-1])
    data.extend(([''],)*3) # insert 3 empty rows...
    row = []
    for signature in signatures:
        row.append(Paragraph(trans(signature, lang), style["Normal"]))
        row.append('')
    data.append(row[:-1])
    table = Table(data, style=sig_style, repeatRows=len(data),
                  colWidths=col_widths)
    return table

def docs_as_flowables(view, lang='en'):
    """Create reportlab flowables out of scanned docs.
    """
    # XXX: fix circular import problem
    from waeup.kofa.students.viewlets import FileManager
    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
    from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
    style = getSampleStyleSheet()
    data = []

    # Collect viewlets
    fm = FileManager(view.context, view.request, view)
    fm.update()
    if fm.viewlets:
        sc_translation = trans(_('Scanned Documents'), lang)
        data.append(Paragraph(sc_translation, style["Heading3"]))
        # Insert list of scanned documents
        table_data = []
        for viewlet in fm.viewlets:
            f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
            img_path = getattr(getUtility(IExtFileStore).getFileByContext(
                view.context, attr=viewlet.download_name), 'name', None)
            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
            if img_path is None:
                pass
            elif not img_path[-4:] in ('.jpg', '.JPG'):
                # reportlab requires jpg images, I think.
                f_text = Paragraph('%s (not displayable)' % (
                    viewlet.title,), ENTRY1_STYLE)
            else:
                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
            table_data.append([f_label, f_text])
        if table_data:
            # safety belt; empty tables lead to problems.
            data.append(Table(table_data, style=SLIP_STYLE))
    return data

class StudentsUtils(grok.GlobalUtility):
    """A collection of methods subject to customization.
    """
    grok.implements(IStudentsUtils)

    def getReturningData(self, student):
        """ Define what happens after school fee payment
        depending on the student's senate verdict.

        In the base configuration current level is always increased
        by 100 no matter which verdict has been assigned.
        """
        new_level = student['studycourse'].current_level + 100
        new_session = student['studycourse'].current_session + 1
        return new_session, new_level

    def setReturningData(self, student):
        """ Define what happens after school fee payment
        depending on the student's senate verdict.

        This method folllows the same algorithm as getReturningData but
        it also sets the new values.
        """
        new_session, new_level = self.getReturningData(student)
        student['studycourse'].current_level = new_level
        student['studycourse'].current_session = new_session
        verdict = student['studycourse'].current_verdict
        student['studycourse'].current_verdict = '0'
        student['studycourse'].previous_verdict = verdict
        return

    def setPaymentDetails(self, category, student,
            previous_session, previous_level):
        """Create Payment object and set the payment data of a student for
        the payment category specified.

        """
        p_item = u''
        amount = 0.0
        if previous_session:
            p_session = previous_session
            p_level = previous_level
            p_current = False
        else:
            p_session = student['studycourse'].current_session
            p_level = student['studycourse'].current_level
            p_current = True
        session = str(p_session)
        try:
            academic_session = grok.getSite()['configuration'][session]
        except KeyError:
            return _(u'Session configuration object is not available.'), None
        if category == 'schoolfee':
            try:
                certificate = student['studycourse'].certificate
                p_item = certificate.code
            except (AttributeError, TypeError):
                return _('Study course data are incomplete.'), None
            if previous_session:
                if previous_session < student['studycourse'].entry_session:
                    return _('The previous session must not fall below '
                             'your entry session.'), None
                if previous_session > student['studycourse'].current_session - 1:
                    return _('This is not a previous session.'), None
                if previous_level == 100:
                    amount = getattr(certificate, 'school_fee_1', 0.0)
                else:
                    amount = getattr(certificate, 'school_fee_2', 0.0)
            else:
                if student.state == CLEARED:
                    amount = getattr(certificate, 'school_fee_1', 0.0)
                elif student.state == RETURNING:
                    # In case of returning school fee payment the payment session
                    # and level contain the values of the session the student
                    # has paid for.
                    p_session, p_level = self.getReturningData(student)
                    amount = getattr(certificate, 'school_fee_2', 0.0)
                elif student.is_postgrad and student.state == PAID:
                    # Returning postgraduate students also pay for the next session
                    # but their level always remains the same.
                    p_session += 1
                    amount = getattr(certificate, 'school_fee_2', 0.0)
        elif category == 'clearance':
            try:
                p_item = student['studycourse'].certificate.code
            except (AttributeError, TypeError):
                return _('Study course data are incomplete.'), None
            amount = academic_session.clearance_fee
        elif category == 'bed_allocation':
            p_item = self.getAccommodationDetails(student)['bt']
            amount = academic_session.booking_fee
        elif category == 'hostel_maintenance':
            amount = academic_session.maint_fee
            bedticket = student['accommodation'].get(
                str(student.current_session), None)
            if bedticket:
                p_item = bedticket.bed_coordinates
            else:
                # Should not happen because this is already checked
                # in the browser module, but anyway ...
                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
                p_item = trans(_('no bed allocated'), portal_language)
        if amount in (0.0, None):
            return _('Amount could not be determined.' +
                     ' Would you like to pay for a previous session?'), None
        for key in student['payments'].keys():
            ticket = student['payments'][key]
            if ticket.p_state == 'paid' and\
               ticket.p_category == category and \
               ticket.p_item == p_item and \
               ticket.p_session == p_session:
                  return _('This type of payment has already been made.' +
                           ' Would you like to pay for a previous session?'), None
        payment = createObject(u'waeup.StudentOnlinePayment')
        timestamp = ("%d" % int(time()*10000))[1:]
        payment.p_id = "p%s" % timestamp
        payment.p_category = category
        payment.p_item = p_item
        payment.p_session = p_session
        payment.p_level = p_level
        payment.p_current = p_current
        payment.amount_auth = amount
        return None, payment

    def getAccommodationDetails(self, student):
        """Determine the accommodation data of a student.
        """
        d = {}
        d['error'] = u''
        hostels = grok.getSite()['hostels']
        d['booking_session'] = hostels.accommodation_session
        d['allowed_states'] = hostels.accommodation_states
        d['startdate'] = hostels.startdate
        d['enddate'] = hostels.enddate
        d['expired'] = hostels.expired
        # Determine bed type
        studycourse = student['studycourse']
        certificate = getattr(studycourse,'certificate',None)
        entry_session = studycourse.entry_session
        current_level = studycourse.current_level
        if None in (entry_session, current_level, certificate):
            return d
        end_level = certificate.end_level
        if current_level == 10:
            bt = 'pr'
        elif entry_session == grok.getSite()['hostels'].accommodation_session:
            bt = 'fr'
        elif current_level >= end_level:
            bt = 'fi'
        else:
            bt = 're'
        if student.sex == 'f':
            sex = 'female'
        else:
            sex = 'male'
        special_handling = 'regular'
        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
        return d

    def selectBed(self, available_beds):
        """Select a bed from a list of available beds.

        In the base configuration we select the first bed found,
        but can also randomize the selection if we like.
        """
        return available_beds[0]

    def renderPDFAdmissionLetter(self, view, student=None):
        """Render pdf admission letter.
        """
        # XXX: we have to fix the import problems here.
        from waeup.kofa.browser.interfaces import IPDFCreator
        from waeup.kofa.browser.pdf import format_html, NOTE_STYLE
        if student is None:
            return
        style = getSampleStyleSheet()
        creator = getUtility(IPDFCreator)
        data = []
        doc_title = view.label
        author = '%s (%s)' % (view.request.principal.title,
                              view.request.principal.id)
        footer_text = view.label
        if getattr(student, 'student_id', None) is not None:
            footer_text = "%s - %s - " % (student.student_id, footer_text)

        # Admission text
        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
        inst_name = grok.getSite()['configuration'].name
        text = trans(_(
            'This is to inform you that you have been provisionally'
            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
            portal_language)
        html = format_html(text)
        data.append(Paragraph(html, NOTE_STYLE))
        data.append(Spacer(1, 20))

        # Student data
        data.append(render_student_data(view))

        # Insert history
        data.append(Spacer(1, 20))
        datelist = student.history.messages[0].split()[0].split('-')
        creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
        text = trans(_(
            'Your Kofa student record was created on ${a}.',
            mapping = {'a': creation_date}),
            portal_language)
        html = format_html(text)
        data.append(Paragraph(html, NOTE_STYLE))

        # Create pdf stream
        view.response.setHeader(
            'Content-Type', 'application/pdf')
        pdf_stream = creator.create_pdf(
            data, None, doc_title, author=author, footer=footer_text,
            note=None)
        return pdf_stream

    def renderPDF(self, view, filename='slip.pdf', student=None,
                  studentview=None, tableheader=None, tabledata=None,
                  note=None, signatures=None):
        """Render pdf slips for various pages.
        """
        # XXX: we have to fix the import problems here.
        from waeup.kofa.browser.interfaces import IPDFCreator
        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
        style = getSampleStyleSheet()
        creator = getUtility(IPDFCreator)
        data = []
        doc_title = view.label
        author = '%s (%s)' % (view.request.principal.title,
                              view.request.principal.id)
        footer_text = view.label
        if getattr(student, 'student_id', None) is not None:
            footer_text = "%s - %s - " % (student.student_id, footer_text)

        # Insert history
        if not filename.startswith('payment'):
            data.extend(creator.fromStringList(student.history.messages))

        # Insert student data table
        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
        if student is not None:
            bd_translation = trans(_('Base Data'), portal_language)
            data.append(Paragraph(bd_translation, style["Heading3"]))
            data.append(render_student_data(studentview))

        # Insert widgets
        if view.form_fields:
            data.append(Paragraph(view.title, style["Heading3"]))
            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
            separators = getattr(self, 'SEPARATORS_DICT', {})
            table = creator.getWidgetsTable(
                view.form_fields, view.context, None, lang=portal_language,
                separators=separators)
            data.append(table)

        # Insert scanned docs
        data.extend(docs_as_flowables(view, portal_language))

        # Insert content table (optionally on second page)
        if tabledata and tableheader:
            #data.append(PageBreak())
            data.append(Spacer(1, 20))
            data.append(Paragraph(view.content_title, style["Heading3"]))
            contenttable = render_table_data(tableheader,tabledata)
            data.append(contenttable)

        # Insert signatures
        if signatures:
            data.append(Spacer(1, 20))
            signaturetable = get_signature_table(signatures)
            data.append(signaturetable)

        view.response.setHeader(
            'Content-Type', 'application/pdf')
        try:
            pdf_stream = creator.create_pdf(
                data, None, doc_title, author=author, footer=footer_text,
                note=note)
        except IOError:
            view.flash('Error in image file.')
            return view.redirect(view.url(view.context))
        return pdf_stream

    VERDICTS_DICT = {
        '0': _('(not yet)'),
        'A': 'Successful student',
        'B': 'Student with carryover courses',
        'C': 'Student on probation',
        }

    SEPARATORS_DICT = {
        }

    #: A prefix used when generating new student ids. Each student id will
    #: start with this string. The default is 'K' for ``Kofa``.
    STUDENT_ID_PREFIX = u'K'
