## $Id: interfaces.py 7708 2012-02-27 11:52:23Z 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
##
"""Interfaces of the university application package.
"""

from grokcore.content.interfaces import IContainer

from zope import schema
from zope.interface import Interface, Attribute, implements, directlyProvides
from zope.component import getUtilitiesFor, queryUtility, getUtility
from zope.catalog.interfaces import ICatalog
from zope.schema.interfaces import (
    ValidationError, ISource, IContextSourceBinder)
from zc.sourcefactory.basic import BasicSourceFactory
from zc.sourcefactory.contextual import BasicContextualSourceFactory
from waeup.sirp.schema import TextLineChoice
from waeup.sirp.interfaces import (
    ISIRPObject, year_range, validate_email, academic_sessions_vocab)
from waeup.sirp.interfaces import MessageFactory as _
from waeup.sirp.university.vocabularies import (
    course_levels, AppCatSource)
from waeup.sirp.students.vocabularies import (
    lgas_vocab, CertificateSource, GenderSource)
from waeup.sirp.payments.interfaces import IOnlinePayment

#: Maximum upload size for applicant passport photographs (in bytes)
MAX_UPLOAD_SIZE = 1024 * 20

class RegNumInSource(ValidationError):
    """Registration number exists already
    """
    # The docstring of ValidationErrors is used as error description
    # by zope.formlib.
    pass

class RegNumberSource(object):
    implements(ISource)
    cat_name = 'applicants_catalog'
    field_name = 'reg_number'
    validation_error = RegNumInSource
    def __init__(self, context):
        self.context = context
        return

    def __contains__(self, value):
        cat = queryUtility(ICatalog, self.cat_name)
        if cat is None:
            return True
        kw = {self.field_name: (value, value)}
        results = cat.searchResults(**kw)
        for entry in results:
            if entry.applicant_id != self.context.applicant_id:
                # XXX: sources should simply return False.
                #      But then we get some stupid error message in forms
                #      when validation fails.
                raise self.validation_error(value)
                #return False
        return True

def contextual_reg_num_source(context):
    source = RegNumberSource(context)
    return source
directlyProvides(contextual_reg_num_source, IContextSourceBinder)

class AppCatCertificateSource(CertificateSource):
    """An application certificate source delivers all courses which belong to
    a certain application_category.
    """
    def getValues(self, context):
        # appliction category not available when certificate was deleted.
        # shouldn't that info be part of applicant info instead?
        # when we cannot determine the appcat, we will display all courses.
        appcat = getattr(getattr(context, '__parent__', None),
                         'application_category', None)
        catalog = getUtility(ICatalog, name='certificates_catalog')
        result = catalog.searchResults(
            application_category=(appcat,appcat))
        result = sorted(result, key=lambda value: value.code)
        curr_course = context.course1
        if curr_course is not None and curr_course not in result:
            # display also current course even if it is not catalogued
            # (any more)
            result = [curr_course,] + result
        return result

class ApplicationTypeSource(BasicContextualSourceFactory):
    """An application type source delivers screening types defined in the
    portal.
    """
    def getValues(self, context):
        appcats_dict = getUtility(
            IApplicantsUtils).getApplicationTypeDict()
        return sorted(appcats_dict.keys())

    def getToken(self, context, value):
        return value

    def getTitle(self, context, value):
        appcats_dict = getUtility(
            IApplicantsUtils).getApplicationTypeDict()
        return appcats_dict[value][0]

# Maybe Uniben still needs this ...
#class ApplicationPinSource(BasicContextualSourceFactory):
#    """An application pin source delivers PIN prefixes for application
#    defined in the portal.
#    """
#    def getValues(self, context):
#        apppins_dict = getUtility(
#            IApplicantsUtils).getApplicationTypeDict()
#        return sorted(appcats_dict.keys())
#
#    def getToken(self, context, value):
#        return value
#
#    def getTitle(self, context, value):
#        apppins_dict = getUtility(
#            IApplicantsUtils).getApplicationTypeDict()
#        return u"%s (%s)" % (
#            apppins_dict[value][1],self.apppins_dict[value][0])

class ApplicantContainerProviderSource(BasicSourceFactory):
    """A source offering all available applicants container types.

    The values returned by this source are names of utilities that can
    create :class:`ApplicantContainer` instances. So, if you get a
    name like ``'myactype'`` from this source, then you can do:

      >>> from zope.component import getUtility
      >>> p = getUtility(IApplicantsContainerProvider, name=myactype)
      >>> my_applicants_container = p.factory()

    Or you can access class-attributes like

      >>> my_applicants_container.container_title
      'Pretty'

    """
    def getValues(self):
        """Returns a list of ``(<name>, <provider>)`` tuples.

        Here ``<name>`` is the name under which an
        :class:``ApplicantContainerProvider`` was registered as a
        utility and ``<provider>`` is the utility itself.
        """
        return getUtilitiesFor(IApplicantsContainerProvider)

    def getToken(self, value):
        """Return the name of the ``(<name>, <provider>)`` tuple.
        """
        return value[0]

    def getTitle(self, value):
        """Get a 'title - description' string for a container type.
        """
        factory = value[1].factory
        return "%s - %s" % (
            factory.container_title, factory.container_description)

class IApplicantsUtils(Interface):
    """A collection of methods which are subject to customization.
    """
    pass

class IApplicantsRoot(ISIRPObject, IContainer):
    """A container for university applicants containers.
    """
    pass

class IApplicantsContainer(ISIRPObject):
    """An applicants container contains university applicants.

    """

    container_title = Attribute(
        u'classattribute: title for type of container')
    container_description = Attribute(
        u'classattribute: description for type of container')


    code = schema.TextLine(
        title = _(u'Code'),
        default = u'-',
        required = True,
        readonly = True,
        )

    title = schema.TextLine(
        title = _(u'Title'),
        required = True,
        default = u'-',
        readonly = True,
        )

    prefix = schema.Choice(
        title = _(u'Application Target'),
        required = True,
        default = None,
        source = ApplicationTypeSource(),
        readonly = True,
        )

    entry_level = schema.Choice(
        title = _(u'Entry Level'),
        vocabulary = course_levels,
        default = 100,
        required = True,
        )

    year = schema.Choice(
        title = _(u'Year of Entrance'),
        required = True,
        default = None,
        values = year_range(),
        readonly = True,
        )

    provider = schema.Choice(
        title = _(u'Applicants Container Type'),
        required = True,
        default = None,
        source = ApplicantContainerProviderSource(),
        readonly = True,
        )

    # Maybe Uniben still needs this ...
    #ac_prefix = schema.Choice(
    #    title = u'Activation code prefix',
    #    required = True,
    #    default = None,
    #    source = ApplicationPinSource(),
    #    )

    application_category = schema.Choice(
        title = _(u'Category for the grouping of certificates'),
        required = True,
        default = None,
        source = AppCatSource(),
        )

    description = schema.Text(
        title = _(u'Human readable description in reST format'),
        required = False,
        default = u'''This text can been seen by anonymous users.
Here we put mult-lingual information about the study courses provided, the application procedure and deadlines.
>>de<<
Dieser Text kann von anonymen Benutzern gelesen werden.
Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
        )

    description_dict = schema.Dict(
        title = u'Content as language dictionary with values in html format',
        required = False,
        default = {},
        )

    startdate = schema.Date(
        title = _(u'Application Start Date'),
        required = False,
        default = None,
        )

    enddate = schema.Date(
        title = _(u'Application Closing Date'),
        required = False,
        default = None,
        )

    strict_deadline = schema.Bool(
        title = _(u'Forbid additions after deadline (enddate)'),
        required = True,
        default = True,
        )

    def archive(id=None):
        """Create on-dist archive of applicants stored in this term.

        If id is `None`, all applicants are archived.

        If id contains a single id string, only the respective
        applicants are archived.

        If id contains a list of id strings all of the respective
        applicants types are saved to disk.
        """

    def clear(id=None, archive=True):
        """Remove applicants of type given by 'id'.

        Optionally archive the applicants.

        If id is `None`, all applicants are archived.

        If id contains a single id string, only the respective
        applicants are archived.

        If id contains a list of id strings all of the respective
        applicant types are saved to disk.

        If `archive` is ``False`` none of the archive-handling is done
        and respective applicants are simply removed from the
        database.
        """

class IApplicantsContainerAdd(IApplicantsContainer):
    """An applicants container contains university applicants.
    """
    prefix = schema.Choice(
        title = _(u'Application Target'),
        required = True,
        default = None,
        source = ApplicationTypeSource(),
        readonly = False,
        )

    year = schema.Choice(
        title = _(u'Year of Entrance'),
        required = True,
        default = None,
        values = year_range(),
        readonly = False,
        )

    provider = schema.Choice(
        title = _(u'Applicants Container Type'),
        required = True,
        default = None,
        source = ApplicantContainerProviderSource(),
        readonly = False,
        )

IApplicantsContainerAdd[
    'prefix'].order =  IApplicantsContainer['prefix'].order
IApplicantsContainerAdd[
    'year'].order =  IApplicantsContainer['year'].order
IApplicantsContainerAdd[
    'provider'].order =  IApplicantsContainer['provider'].order

class IApplicantBaseData(ISIRPObject):
    """The data for an applicant.

    This is a base interface with no field
    required. For use with importers, forms, etc., please use one of
    the derived interfaces below, which set more fields to required
    state, depending on use-case.

    This base interface is also implemented by the StudentApplication
    class in the students package. Thus, these are the data which are saved
    after admission.
    """

    applicant_id = schema.TextLine(
        title = _(u'Applicant Id'),
        required = False,
        readonly = False,
        )
    reg_number = TextLineChoice(
        title = _(u'JAMB Registration Number'),
        readonly = False,
        required = True,
        default = None,
        source = contextual_reg_num_source,
        )
    #access_code = schema.TextLine(
    #    title = u'Activation Code',
    #    required = False,
    #    readonly = True,
    #    )
    firstname = schema.TextLine(
        title = _(u'First Name'),
        required = True,
        )
    middlename = schema.TextLine(
        title = _(u'Middle Name'),
        required = False,
        )
    lastname = schema.TextLine(
        title = _(u'Last Name (Surname)'),
        required = True,
        )
    date_of_birth = schema.Date(
        title = _(u'Date of Birth'),
        required = True,
        )
    lga = schema.Choice(
        source = lgas_vocab,
        title = _(u'State/LGA'),
        default = 'foreigner',
        required = True,
        )
    sex = schema.Choice(
        title = _(u'Sex'),
        source = GenderSource(),
        default = u'm',
        required = True,
        )
    email = schema.ASCIILine(
        title = _(u'Email Address'),
        required = True,
        constraint=validate_email,
        )
    phone = schema.TextLine(
        title = _(u'Phone'),
        description = u'',
        required = False,
        )
    course1 = schema.Choice(
        title = _(u'1st Choice Course of Study'),
        source = CertificateSource(),
        required = True,
        )
    course2 = schema.Choice(
        title = _(u'2nd Choice Course of Study'),
        source = CertificateSource(),
        required = False,
        )

    #
    # Data to be imported after screening
    #
    screening_score = schema.Int(
        title = _(u'Screening Score'),
        required = False,
        )
    screening_venue = schema.TextLine(
        title = _(u'Screening Venue'),
        required = False,
        )
    course_admitted = schema.Choice(
        title = _(u'Admitted Course of Study'),
        source = CertificateSource(),
        default = None,
        required = False,
        )
    notice = schema.Text(
        title = _(u'Notice'),
        required = False,
        )

class IApplicantProcessData(IApplicantBaseData):
    """An applicant.

    Here we add process attributes and methods to the base data.
    """

    history = Attribute('Object history, a list of messages.')
    state = Attribute('The application state of an applicant')
    display_fullname = Attribute('The fullname of an applicant')
    application_date = Attribute('Date of submission, used for export only')
    password = Attribute('Encrypted password of a applicant')
    application_number = Attribute('The key under which the record is stored')

    def loggerInfo(ob_class, comment):
        """Adds an INFO message to the log file
        """

    student_id = schema.TextLine(
        title = _(u'Student Id'),
        required = False,
        readonly = False,
        )
    locked = schema.Bool(
        title = _(u'Form locked'),
        default = False,
        )

class IApplicant(IApplicantProcessData):
    """An applicant.

    This is basically the applicant base data. Here we repeat the
    fields from base data if we have to set the `required` attribute
    to True (which is the default).
    """

class IApplicantEdit(IApplicantProcessData):
    """An applicant.

    Here we can repeat the fields from base data and set the
    `required` and `readonly` attributes to True to further restrict
    the data access. Or we can allow only certain certificates to be
    selected by choosing the appropriate source.

    We cannot omit fields here. This has to be done in the
    respective form page.
    """

    course1 = schema.Choice(
        title = _(u'1st Choice Course of Study'),
        source = AppCatCertificateSource(),
        required = True,
        )
    course2 = schema.Choice(
        title = _(u'2nd Choice Course of Study'),
        source = AppCatCertificateSource(),
        required = False,
        )
    screening_score = schema.Int(
        title = _(u'Screening Score'),
        required = False,
        readonly = True,
        )
    screening_venue = schema.TextLine(
        title = _(u'Screening Venue'),
        required = False,
        readonly = True,
        )
    course_admitted = schema.Choice(
        title = _(u'Admitted Course of Study'),
        source = CertificateSource(),
        default = None,
        required = False,
        readonly = True,
        )
    notice = schema.Text(
        title = _(u'Notice'),
        required = False,
        readonly = True,
        )

    def createStudent():
        """Create a student object from applicatnt data
        and copy applicant object.
        """

class IApplicantUpdateByRegNo(IApplicant):
    """Representation of an applicant.

    Skip regular reg_number validation if reg_number is used for finding
    the applicant object.
    """
    reg_number = schema.TextLine(
        title = u'Registration Number',
        default = None,
        required = False,
        )

class IApplicantOnlinePayment(IOnlinePayment):
    """An applicant payment via payment gateways.

    """
    p_year = schema.Choice(
        title = _(u'Payment Session'),
        source = academic_sessions_vocab,
        required = False,
        )

IApplicantOnlinePayment['p_year'].order = IApplicantOnlinePayment[
    'p_year'].order

class IApplicantsContainerProvider(Interface):
    """A provider for applicants containers.

    Applicants container providers are meant to be looked up as
    utilities. This way we can find all applicant container types
    defined somewhere.

    Each applicants container provider registered as utility provides
    one container type and one should be able to call the `factory`
    attribute to create an instance of the requested container type.

    .. THE FOLLOWING SHOULD GO INTO SPHINX DOCS (and be tested)

    Samples:

    Given, you had an IApplicantsContainer implementation somewhere
    and you would like to make it findable on request, then you would
    normally create an appropriate provider utility like this::

      import grok
      from waeup.sirp.applicants.interfaces import IApplicantsContainerProvider

      class MyContainerProvider(grok.GlobalUtility):
          grok.implements(IApplicantsContainerProvider)
          grok.name('MyContainerProvider') # Must be unique
          factory = MyContainer # A class implementing IApplicantsContainer
                                # or derivations thereof.

    This utility would be registered on startup and could then be used
    like this:

      >>> from zope.component import getAllUtilitiesRegisteredFor
      >>> from waeup.sirp.applicants.interfaces import (
      ...     IApplicantsContainerProvider)
      >>> all_providers = getAllUtilitiesRegisteredFor(
      ...     IApplicantsContainerProvider)
      >>> all_providers
      [<MyContainerProvider object at 0x...>]

    You could look up this specific provider by name:

      >>> from zope.component import getUtility
      >>> p = getUtility(IApplicantsContainerProvider, name='MyProvider')
      >>> p
      <MyContainerProvider object at 0x...>

    An applicants container would then be created like this:

      >>> provider = all_providers[0]
      >>> container = provider.factory()
      >>> container
      <MyContainer object at 0x...>

    """
    factory = Attribute("A class that can create instances of the "
                        "requested container type")
