Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/__init__.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/__init__.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/__init__.py	(revision 8862)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/interfaces.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/interfaces.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/payments/interfaces.py	(revision 8862)
@@ -0,0 +1,70 @@
+## $Id$
+##
+## 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
+##
+from zope.interface import Attribute
+from zope import schema
+from waeup.kofa.payments.interfaces import IPayment
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+class ICustomOnlinePayment(IPayment):
+    """A payment via payment gateways.
+
+    """
+
+    ac = schema.TextLine(
+        title = _(u'Activation Code'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_amount_approved = schema.Float(
+        title = _(u'Response Amount Approved'),
+        default = 0.0,
+        required = False,
+        readonly = False,
+        )
+
+    r_code = schema.TextLine(
+        title = _(u'Response Code'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_desc = schema.TextLine(
+        title = _(u'Response Description'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    # Only defined in custom package
+
+    r_pay_reference = schema.TextLine(
+        title = _(u'Response Payment Reference'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_card_num = schema.TextLine(
+        title = _(u'Response Card Number'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/batching.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/batching.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/batching.py	(revision 8862)
@@ -0,0 +1,37 @@
+## $Id$
+##
+## 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
+##
+"""Batch processing components for student objects.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+
+Here we define the processors for students specific objects like
+students, studycourses, payment tickets and accommodation tickets.
+"""
+from kofacustom.nigeria.students.interfaces import (
+    ICustomStudent, ICustomStudentUpdateByRegNo,
+    ICustomStudentUpdateByMatricNo
+    )
+from waeup.kofa.students.batching import StudentProcessor
+
+class CustomStudentProcessor(StudentProcessor):
+    """A batch processor for IStudent objects.
+    """
+    iface = ICustomStudent
+    iface_byregnumber = ICustomStudentUpdateByRegNo
+    iface_bymatricnumber = ICustomStudentUpdateByMatricNo
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py	(revision 8862)
@@ -0,0 +1,212 @@
+## $Id$
+##
+## 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
+##
+import grok
+from zope.formlib.textwidgets import BytesDisplayWidget
+from zope.component import getUtility
+from zope.i18n import translate
+from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
+from waeup.kofa.interfaces import IExtFileStore
+from waeup.kofa.browser.layout import action
+from waeup.kofa.students.browser import (
+    StudentPersonalDisplayFormPage,
+    StudentClearanceManageFormPage, StudentClearanceEditFormPage,
+    StudentClearanceDisplayFormPage, OnlinePaymentFakeApprovePage,
+    ExportPDFClearanceSlipPage, StudentBaseManageFormPage,
+    StudentBaseEditFormPage, StudentPersonalEditFormPage,
+    OnlinePaymentDisplayFormPage, OnlinePaymentAddFormPage,
+    OnlinePaymentBreadcrumb, ExportPDFPaymentSlipPage,
+    StudentFilesUploadPage, emit_lock_message)
+from waeup.kofa.students.viewlets import (
+    PaymentReceiptActionButton, StudentPassportActionButton)
+from kofacustom.nigeria.students.interfaces import (
+    ICustomStudentBase, ICustomStudent, ICustomStudentPersonal,
+    ICustomUGStudentClearance,ICustomPGStudentClearance,
+    ICustomStudentOnlinePayment,
+    )
+from waeup.kofa.students.workflow import ADMITTED
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+class CustomOnlinePaymentBreadcrumb(OnlinePaymentBreadcrumb):
+    """A breadcrumb for payments.
+    """
+    grok.context(ICustomStudentOnlinePayment)
+
+class PaymentReceiptActionButton(PaymentReceiptActionButton):
+    grok.order(4)
+    grok.context(ICustomStudentOnlinePayment)
+
+class CustomStudentBaseManageFormPage(StudentBaseManageFormPage):
+    """ View to manage student base data
+    """
+    form_fields = grok.AutoFields(ICustomStudentBase).omit('student_id')
+
+class CustomStudentBaseEditFormPage(StudentBaseEditFormPage):
+    """ View to edit student base data
+    """
+    form_fields = grok.AutoFields(ICustomStudentBase).select(
+        'email', 'phone')
+
+
+class CustomStudentPersonalDisplayFormPage(StudentPersonalDisplayFormPage):
+    """ Page to display student personal data
+    """
+    grok.context(ICustomStudent)
+    form_fields = grok.AutoFields(ICustomStudentPersonal)
+    form_fields['perm_address'].custom_widget = BytesDisplayWidget
+
+
+class CustomStudentPersonalEditFormPage(StudentPersonalEditFormPage):
+    """ Page to edit personal data
+    """
+    form_fields = grok.AutoFields(ICustomStudentPersonal)
+
+
+class CustomStudentClearanceDisplayFormPage(StudentClearanceDisplayFormPage):
+    """ Page to display student clearance data
+    """
+    grok.context(ICustomStudent)
+
+    @property
+    def form_fields(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            form_fields = grok.AutoFields(
+                ICustomPGStudentClearance).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(
+                ICustomUGStudentClearance).omit('clearance_locked')
+        return form_fields
+
+class CustomExportPDFClearanceSlipPage(ExportPDFClearanceSlipPage):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(ICustomStudent)
+
+    @property
+    def form_fields(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            form_fields = grok.AutoFields(
+                ICustomPGStudentClearance).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(
+                ICustomUGStudentClearance).omit('clearance_locked')
+        return form_fields
+
+class CustomStudentClearanceManageFormPage(StudentClearanceManageFormPage):
+    """ Page to edit student clearance data
+    """
+    grok.context(ICustomStudent)
+
+    @property
+    def form_fields(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            form_fields = grok.AutoFields(ICustomPGStudentClearance)
+        else:
+            form_fields = grok.AutoFields(ICustomUGStudentClearance)
+        return form_fields
+
+class CustomStudentClearanceEditFormPage(StudentClearanceEditFormPage):
+    """ View to edit student clearance data by student
+    """
+    grok.context(ICustomStudent)
+
+    @property
+    def form_fields(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            form_fields = grok.AutoFields(ICustomPGStudentClearance).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(ICustomUGStudentClearance).omit('clearance_locked')
+        return form_fields
+
+    def dataNotComplete(self):
+        store = getUtility(IExtFileStore)
+        if not store.getFileByContext(self.context, attr=u'birth_certicicate.jpg'):
+            return _('No birth certificate uploaded.')
+        if not store.getFileByContext(self.context, attr=u'ref_let.jpg'):
+            return _('No referee letter uploaded.')
+        if not store.getFileByContext(self.context, attr=u'acc_let.jpg'):
+            return _('No acceptance letter uploaded.')
+        if not store.getFileByContext(self.context, attr=u'fst_sit_scan.jpg'):
+            return _('No first sitting result uploaded.')
+        return False
+
+class CustomOnlinePaymentDisplayFormPage(OnlinePaymentDisplayFormPage):
+    """ Page to view an online payment ticket
+    """
+    grok.context(ICustomStudentOnlinePayment)
+    form_fields = grok.AutoFields(ICustomStudentOnlinePayment)
+    form_fields[
+        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields[
+        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+class CustomOnlinePaymentAddFormPage(OnlinePaymentAddFormPage):
+    """ Page to add an online payment ticket
+    """
+    form_fields = grok.AutoFields(ICustomStudentOnlinePayment).select(
+        'p_category')
+
+class CustomOnlinePaymentFakeApprovePage(OnlinePaymentFakeApprovePage):
+    """ Disable payment approval view for students.
+
+    This view is used for browser tests only and
+    has to be neutralized here!
+    """
+
+    grok.name('fake_approve')
+    grok.require('waeup.managePortal')
+
+    def update(self):
+        return
+
+class CustomExportPDFPaymentSlipPage(ExportPDFPaymentSlipPage):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(ICustomStudentOnlinePayment)
+    form_fields = grok.AutoFields(ICustomStudentOnlinePayment)
+    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+class StudentPassportActionButton(StudentPassportActionButton):
+
+    @property
+    def target_url(self):
+        slip = getUtility(IExtFileStore).getFileByContext(
+            self.context, 'application_slip')
+        if self.context.state != ADMITTED or slip is not None:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class CustomStudentFilesUploadPage(StudentFilesUploadPage):
+    """ View to upload passport picture.
+
+    Students are not allowed to change the picture if they
+    passed the regular Kofa application.
+    """
+
+    def update(self):
+        slip = getUtility(IExtFileStore).getFileByContext(
+            self.context, 'application_slip')
+        if self.context.state != ADMITTED or slip is not None:
+            emit_lock_message(self)
+            return
+        super(StudentFilesUploadPage, self).update()
+        return
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py	(revision 8862)
@@ -0,0 +1,400 @@
+## $Id$
+##
+## 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
+##
+from zope import schema
+from waeup.kofa.schema import TextLineChoice
+from waeup.kofa.interfaces import SimpleKofaVocabulary, academic_sessions_vocab
+from waeup.kofa.schema import FormattedDate
+from waeup.kofa.schoolgrades import ResultEntryField
+from waeup.kofa.students.vocabularies import nats_vocab
+from waeup.kofa.students.interfaces import (
+    IStudentBase, IUGStudentClearance, IPGStudentClearance,
+    IStudentPersonal, IStudentNavigation, IStudentStudyLevel,
+    IStudentStudyCourse, ICourseTicket
+    )
+from waeup.kofa.students.vocabularies import (
+    nats_vocab, contextual_reg_num_source)
+from kofacustom.nigeria.interfaces import (
+    high_qual, high_grade, exam_types, LGASource)
+from kofacustom.nigeria.interfaces import MessageFactory as _
+from kofacustom.nigeria.payments.interfaces import ICustomOnlinePayment
+
+
+class ICustomStudentBase(IStudentBase):
+    """Representation of student base data.
+
+    """
+
+    reg_number = TextLineChoice(
+        title = _(u'Registration Number'),
+        required = False,
+        readonly = False,
+        source = contextual_reg_num_source,
+        )
+
+class ICustomStudentPersonal(IStudentPersonal):
+    """Student personal data.
+
+    """
+
+    marit_stat = schema.Choice(
+        title = u'Maritual Status',
+        default = 'unmarried',
+        required = False,
+        vocabulary = SimpleKofaVocabulary(
+            (_('Unmarried'), 'unmarried'),
+            (_('Married'), 'married'),)
+        )
+
+class ICustomUGStudentClearance(IUGStudentClearance):
+    """Representation of ug student clearance data.
+
+    """
+    date_of_birth = FormattedDate(
+        title = _(u'Date of Birth'),
+        required = False,
+        show_year = True,
+        )
+
+    nationality = schema.Choice(
+        source = nats_vocab,
+        title = _(u'Nationality'),
+        required = False,
+        )
+
+    lga = schema.Choice(
+        source = LGASource(),
+        title = _(u'State/LGA (Nigerians only)'),
+        required = False,
+        )
+
+    def_adm = schema.Bool(
+        title = _(u'Deferent of Admission'),
+        required = False,
+        readonly = False,
+        )
+
+    fst_sit_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    fst_sit_no = schema.TextLine(
+        title = _(u'Exam Number'),
+        required = False,
+        readonly = False,
+        )
+
+    fst_sit_date = FormattedDate(
+        title = _(u'Exam Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    fst_sit_type = schema.Choice(
+        title = _(u'Exam Type'),
+        required = False,
+        readonly = False,
+        vocabulary = exam_types,
+        )
+
+    fst_sit_results = schema.List(
+        title = _(u'Exam Results'),
+        value_type = ResultEntryField(),
+        required = False,
+        readonly = False,
+        default = [],
+        )
+
+    scd_sit_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    scd_sit_no = schema.TextLine(
+        title = _(u'Exam Number'),
+        required = False,
+        readonly = False,
+        )
+
+    scd_sit_date = FormattedDate(
+        title = _(u'Exam Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    scd_sit_type = schema.Choice(
+        title = _(u'Exam Type'),
+        required = False,
+        readonly = False,
+        vocabulary = exam_types,
+        )
+
+    scd_sit_results = schema.List(
+        title = _(u'Exam Results'),
+        value_type = ResultEntryField(),
+        required = False,
+        readonly = False,
+        default = [],
+        )
+
+    alr_fname = schema.TextLine(
+        title = _(u'Full Name'),
+        required = False,
+        readonly = False,
+        )
+    alr_no = schema.TextLine(
+        title = _(u'Exam Number'),
+        required = False,
+        readonly = False,
+        )
+
+    alr_date = FormattedDate(
+        title = _(u'Exam Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    alr_results = schema.List(
+        title = _(u'Exam Results'),
+        value_type = ResultEntryField(),
+        required = False,
+        readonly = False,
+        default = [],
+        )
+
+    hq_type = schema.Choice(
+        title = _(u'Qualification Obtained'),
+        required = False,
+        readonly = False,
+        vocabulary = high_qual,
+        )
+
+    hq_matric_no = schema.TextLine(
+        title = _(u'Former Matric Number'),
+        required = False,
+        readonly = False,
+        )
+
+    hq_degree = schema.Choice(
+        title = _(u'Class of Degree'),
+        required = False,
+        readonly = False,
+        vocabulary = high_grade,
+        )
+
+    hq_school = schema.TextLine(
+        title = _(u'Institution Attended'),
+        required = False,
+        readonly = False,
+        )
+
+    hq_session = schema.TextLine(
+        title = _(u'Years Attended'),
+        required = False,
+        readonly = False,
+        )
+
+    hq_disc = schema.TextLine(
+        title = _(u'Discipline'),
+        required = False,
+        readonly = False,
+        )
+
+
+class ICustomPGStudentClearance(ICustomUGStudentClearance):
+    """Representation of pg student clearance data.
+
+    """
+
+    hq2_type = schema.Choice(
+        title = _(u'Qualification Obtained'),
+        required = False,
+        readonly = False,
+        vocabulary = high_qual,
+        )
+
+    hq2_matric_no = schema.TextLine(
+        title = _(u'Former Matric Number'),
+        required = False,
+        readonly = False,
+        )
+
+    hq2_degree = schema.Choice(
+        title = _(u'Class of Degree'),
+        required = False,
+        readonly = False,
+        vocabulary = high_grade,
+        )
+
+    hq2_school = schema.TextLine(
+        title = _(u'Institution Attended'),
+        required = False,
+        readonly = False,
+        )
+
+    hq2_session = schema.TextLine(
+        title = _(u'Years Attended'),
+        required = False,
+        readonly = False,
+        )
+
+    hq2_disc = schema.TextLine(
+        title = _(u'Discipline'),
+        required = False,
+        readonly = False,
+        )
+
+    nysc_year = schema.Int(
+        title = _(u'Nysc Year'),
+        required = False,
+        readonly = False,
+        )
+
+    nysc_lga = schema.Choice(
+        source = LGASource(),
+        title = _(u'Nysc Location'),
+        required = False,
+        )
+
+    employer = schema.TextLine(
+        title = _(u'Employer'),
+        required = False,
+        readonly = False,
+        )
+
+    emp_position = schema.TextLine(
+        title = _(u'Employer Position'),
+        required = False,
+        readonly = False,
+        )
+
+    emp_start = FormattedDate(
+        title = _(u'Start Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    emp_end = FormattedDate(
+        title = _(u'End Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    emp_reason = schema.TextLine(
+        title = _(u'Reason for Leaving'),
+        required = False,
+        readonly = False,
+        )
+
+    employer2 = schema.TextLine(
+        title = _(u'2nd Employer'),
+        required = False,
+        readonly = False,
+        )
+
+    emp2_position = schema.TextLine(
+        title = _(u'2nd Employer Position'),
+        required = False,
+        readonly = False,
+        )
+
+    emp2_start = FormattedDate(
+        title = _(u'Start Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+    emp2_end = FormattedDate(
+        title = _(u'End Date'),
+        required = False,
+        readonly = False,
+        show_year = True,
+        )
+
+    emp2_reason = schema.TextLine(
+        title = _(u'Reason for Leaving'),
+        required = False,
+        readonly = False,
+        )
+
+    former_matric = schema.TextLine(
+        title = _(u'If yes, matric number'),
+        required = False,
+        readonly = False,
+        )
+
+
+class ICustomStudent(ICustomStudentBase,ICustomUGStudentClearance,
+    ICustomPGStudentClearance,ICustomStudentPersonal):
+    """Representation of a student.
+
+    """
+
+class ICustomStudentStudyCourse(IStudentStudyCourse):
+    """A container for student study levels.
+
+    """
+
+class ICustomStudentStudyLevel(IStudentStudyLevel):
+    """A container for course tickets.
+
+    """
+
+class ICustomStudentOnlinePayment(ICustomOnlinePayment):
+    """A student payment via payment gateways.
+
+    This Interface does not inherit from IStudentOnlinePayment.
+    Thus all fields from IStudentOnlinePayment have to be repeated here.
+    """
+
+    p_level = schema.Int(
+        title = _(u'Payment Level'),
+        required = False,
+        readonly = True,
+        )
+
+ICustomStudentOnlinePayment['p_level'].order = ICustomStudentOnlinePayment[
+    'p_session'].order
+
+class ICustomCourseTicket(ICourseTicket):
+    """A course ticket.
+
+    """
+
+class ICustomStudentUpdateByRegNo(ICustomStudent):
+    """Representation of a student. Skip regular reg_number validation.
+
+    """
+    reg_number = schema.TextLine(
+        title = _(u'Registration Number'),
+        required = False,
+        )
+
+class ICustomStudentUpdateByMatricNo(ICustomStudent):
+    """Representation of a student. Skip regular matric_number validation.
+
+    """
+    matric_number = schema.TextLine(
+        title = _(u'Matriculation Number'),
+        required = False,
+        )
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/payments.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/payments.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/payments.py	(revision 8862)
@@ -0,0 +1,59 @@
+## $Id$
+##
+## 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
+##
+"""
+Student payment components.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.students.interfaces import IStudentNavigation
+from waeup.kofa.students.payments import (
+    StudentOnlinePayment, StudentOnlinePaymentFactory)
+from waeup.kofa.utils.helpers import attrs_to_fields
+from kofacustom.nigeria.students.interfaces import ICustomStudentOnlinePayment
+
+class CustomStudentOnlinePayment(StudentOnlinePayment):
+    """This is a custom online payment for students.
+
+    Since this class inherits from StudentOnlinePayment, it automatically
+    implements the interfaces of the parent class which means
+    all viewlets and views meant for StudentOnlinePayment
+    can also be accessed in the customized system.
+    """
+    grok.implements(ICustomStudentOnlinePayment, IStudentNavigation)
+    grok.provides(ICustomStudentOnlinePayment)
+
+    def __init__(self):
+        super(CustomStudentOnlinePayment, self).__init__()
+        return
+
+    @property
+    def student(self):
+        return self.__parent__.__parent__
+
+CustomStudentOnlinePayment = attrs_to_fields(CustomStudentOnlinePayment)
+
+class CustomStudentOnlinePaymentFactory(StudentOnlinePaymentFactory):
+    """A factory for student online payments.
+    """
+
+    def __call__(self, *args, **kw):
+        return CustomStudentOnlinePayment()
+
+    def getInterfaces(self):
+        return implementedBy(CustomStudentOnlinePayment)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/student.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/student.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/student.py	(revision 8862)
@@ -0,0 +1,50 @@
+## $Id$
+##
+## 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
+##
+"""
+Container for the various objects owned by students.
+"""
+import grok
+from zope.interface import implementedBy
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.students.student import Student, StudentFactory
+from kofacustom.nigeria.students.interfaces import ICustomStudent, IStudentNavigation
+
+
+class CustomStudent(Student):
+    """This is a student container for the various objects
+    owned by students.
+    """
+    grok.implements(ICustomStudent, IStudentNavigation)
+    grok.provides(ICustomStudent)
+
+
+# Set all attributes of Student required in IStudent as field
+# properties. Doing this, we do not have to set initial attributes
+# ourselves and as a bonus we get free validation when an attribute is
+# set.
+CustomStudent = attrs_to_fields(CustomStudent)
+
+class CustomStudentFactory(StudentFactory):
+    """A factory for students.
+    """
+
+    def __call__(self, *args, **kw):
+        return CustomStudent()
+
+    def getInterfaces(self):
+        return implementedBy(CustomStudent)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/studycourse.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/studycourse.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/studycourse.py	(revision 8862)
@@ -0,0 +1,72 @@
+## $Id$
+##
+## 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 the student study courses
+and contains the (student) study level objects.
+"""
+import grok
+from zope.interface import implementedBy
+from waeup.kofa.students.studycourse import (
+    StudentStudyCourse, StudentStudyCourseFactory)
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.students.workflow import CLEARED, RETURNING, PAID
+from kofacustom.nigeria.students.interfaces import (
+    ICustomStudentStudyCourse, IStudentNavigation)
+
+class CustomStudentStudyCourse(StudentStudyCourse):
+    """This is a container for study levels.
+    """
+
+    grok.implements(ICustomStudentStudyCourse, IStudentNavigation)
+    grok.provides(ICustomStudentStudyCourse)
+
+    @property
+    def next_session_allowed(self):
+        state = self.student.state
+        certificate = getattr(self, 'certificate', None)
+        if certificate == None:
+            return False
+        if state == CLEARED:
+            return True
+        cond0 = state == RETURNING
+        cond1 = self.previous_verdict in (
+            'A','B','C','F','J','L','M','N','O','X','Z')
+        cond2 = self.current_level in (0, 100)
+        cond3 = certificate.study_mode.startswith('de') and \
+            self.current_level == 200
+        cond4 = certificate.study_mode.startswith('ph') and \
+            self.current_level == 300
+        if cond0 and (cond1 or cond2 or cond3 or cond4):
+            return True
+        cond5 = self.is_postgrad
+        cond6 = state == PAID
+        if  cond5 and cond6:
+            return True
+        return False
+
+CustomStudentStudyCourse = attrs_to_fields(CustomStudentStudyCourse)
+
+class CustomStudentStudyCourseFactory(StudentStudyCourseFactory):
+    """A factory for student study courses.
+    """
+
+    def __call__(self, *args, **kw):
+        return CustomStudentStudyCourse()
+
+    def getInterfaces(self):
+        return implementedBy(CustomStudentStudyCourse)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/studylevel.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/studylevel.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/studylevel.py	(revision 8862)
@@ -0,0 +1,74 @@
+## $Id$
+##
+## 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
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.students.studylevel import (
+    StudentStudyLevel, CourseTicket,
+    CourseTicketFactory, StudentStudyLevelFactory)
+from kofacustom.nigeria.students.interfaces import (
+    ICustomStudentStudyLevel, IStudentNavigation, ICustomCourseTicket)
+
+
+class CustomStudentStudyLevel(StudentStudyLevel):
+    """This is a container for course tickets.
+    """
+    grok.implements(ICustomStudentStudyLevel, IStudentNavigation)
+    grok.provides(ICustomStudentStudyLevel)
+
+CustomStudentStudyLevel = attrs_to_fields(CustomStudentStudyLevel)
+
+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)
+
+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)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/__init__.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/__init__.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/__init__.py	(revision 8862)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/sample_student_data.csv
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/sample_student_data.csv	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/sample_student_data.csv	(revision 8862)
@@ -0,0 +1,4 @@
+firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex
+Aaren,Pieri,1,,100000,aa@aa.ng,1234,m
+Aaren,Finau,2,,100001,aa@aa.ng,1234,m
+Aaren,Berson,3,,100002,aa@aa.ng,1234,m
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_browser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_browser.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_browser.py	(revision 8862)
@@ -0,0 +1,208 @@
+## $Id$
+##
+## 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
+##
+import os
+import shutil
+import tempfile
+from StringIO import StringIO
+from hurry.workflow.interfaces import IWorkflowState
+from zope.component.hooks import setSite, clearSite
+from zope.component import getUtility, createObject
+from zope.interface import verify
+from waeup.kofa.app import University
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.testing import FunctionalTestCase
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser)
+from waeup.kofa.students.batching import StudentProcessor
+from waeup.kofa.students.interfaces import IStudentsUtils
+from kofacustom.nigeria.students.batching import CustomStudentProcessor
+from kofacustom.nigeria.testing import FunctionalLayer
+from kofacustom.nigeria.students.interfaces import (
+    ICustomStudentStudyCourse, ICustomStudent,
+    ICustomStudentStudyLevel, ICustomCourseTicket)
+
+
+STUDENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_student_data.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS = STUDENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+class StudentProcessorTest(FunctionalTestCase):
+    """Perform some batching tests.
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentProcessorTest, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        self.processor_base = StudentProcessor()
+        self.processor = CustomStudentProcessor()
+        self.workdir = tempfile.mkdtemp()
+        self.csv_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        open(self.csv_file, 'wb').write(STUDENT_SAMPLE_DATA)
+
+    def tearDown(self):
+        super(StudentProcessorTest, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_import(self):
+        # We have an empty column 'date_of_birth' in  the import file.
+        # The original processor will fail because 'date_of_birth' is required
+        # in the base package.
+        num, num_warns, fin_file, fail_file = self.processor_base.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        self.assertEqual(num_warns,3)
+        assert len(self.app['students'].keys()) == 0
+        # The customized processor does not complain since 'date_of_birth' is
+        # not required in the custom package.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        #print open(fail_file).read()
+        self.assertEqual(num_warns,0)
+        assert len(self.app['students'].keys()) == 3
+        shutil.rmtree(os.path.dirname(fin_file))
+
+
+class StudentUITests(StudentsFullSetup):
+    """Tests for customized student class views and pages
+    """
+
+    layer = FunctionalLayer
+
+    def test_classes(self):
+        # Let's see if objects created in the customized
+        # portal really implement the customized interfaces
+        verify.verifyObject(ICustomStudent, self.student)
+        verify.verifyObject(
+            ICustomStudentStudyCourse, self.student['studycourse'])
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        verify.verifyObject(ICustomStudentStudyLevel, studylevel)
+        ticket = createObject(u'waeup.CourseTicket')
+        verify.verifyObject(ICustomCourseTicket, ticket)
+        IWorkflowState(self.student).setState('returning')
+        # Let's see if next_session_allowed works as expected
+        # A, ug_ft, 100
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+        # O, ug_ft, 100
+        self.student['studycourse'].current_verdict = 'O'
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+        # O, ug_ft, 200
+        self.student['studycourse'].current_level = 200
+        self.assertFalse(self.student['studycourse'].next_session_allowed)
+        # O, de_ft, 200
+        self.student['studycourse'].certificate.study_mode = 'de_ft'
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+        # O, ph_ft, 300
+        self.student['studycourse'].certificate.study_mode = 'ph_ft'
+        self.student['studycourse'].current_level = 300
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+        # O, ph_ft, 400
+        self.student['studycourse'].current_level = 400
+        self.assertFalse(self.student['studycourse'].next_session_allowed)
+
+        # Now we convert the certificate into a postgraduate certificate
+        IWorkflowState(self.student).setState('school fee paid')
+        self.certificate.study_mode = 'pg_ft'
+        # ... and voila next session registration is allowed
+        self.assertTrue(self.student['studycourse'].next_session_allowed)
+
+    def test_manage_access(self):
+        # Managers can access the pages of students
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # The student created in the base package is an ug student
+        self.browser.open(self.student_path)
+        self.browser.getLink("Clearance Data").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.clearance_path)
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_clearance_path)
+        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+        self.assertMatches('...First Sitting Record...',
+                           self.browser.contents)
+        # Managers can open clearance slip of ug students
+        self.browser.open(self.student_path + '/clearance.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        # There is no pg field in the clearance form
+        self.assertFalse('Second Higher Education Record'
+            in self.browser.contents)
+        # Now we change the study mode ...
+        self.certificate.study_mode = 'pg_ft'
+        self.browser.open(self.clearance_path)
+        # ... and additional pg clearance fields appear
+        self.assertMatches('...Second Higher Education Record...',
+                           self.browser.contents)
+        # But also fields from the ug form are displayed
+        self.assertMatches('...First Sitting Record...',
+                           self.browser.contents)
+        # Managers can open clearance slip of pg students
+        self.browser.open(self.student_path + '/clearance.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+
+    def test_student_access(self):
+        # Students can edit clearance data
+        IWorkflowState(self.student).setState('cleared')
+        self.student.clearance_locked = False
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Even in state admitted students can't change the portait if
+        # application slip exists.
+        IWorkflowState(self.student).setState('admitted')
+        self.browser.open(self.student_path)
+        self.assertTrue('Change portrait' in self.browser.contents)
+        file_store = getUtility(IExtFileStore)
+        applicant_slip = 'My application slip'
+        file_id = IFileStoreNameChooser(self.student).chooseName(
+            attr="application_slip.pdf")
+        file_store.createFile(file_id, StringIO(applicant_slip))
+        self.browser.open(self.student_path)
+        self.assertFalse('Change portrait' in self.browser.contents)
+        self.browser.open(self.student_path + '/change_portrait')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+        # Student can view and edit clearance data
+        self.browser.getLink("Clearance Data").click()
+        self.browser.getLink("Edit").click()
+        self.assertTrue('Save' in self.browser.contents)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_student.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_student.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_student.py	(revision 8862)
@@ -0,0 +1,127 @@
+## $Id$
+##
+## 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
+##
+"""Tests for students and related.
+"""
+from datetime import tzinfo
+from zope.component.interfaces import IFactory
+from zope.interface import verify
+from waeup.kofa.testing import FunctionalTestCase
+from kofacustom.nigeria.students.student import CustomStudent, CustomStudentFactory
+from kofacustom.nigeria.students.studycourse import (
+    CustomStudentStudyCourse, CustomStudentStudyCourseFactory)
+from kofacustom.nigeria.students.studylevel import (
+    CustomStudentStudyLevel, CustomCourseTicket,
+    CustomStudentStudyLevelFactory,
+    CustomCourseTicketFactory)
+from kofacustom.nigeria.students.interfaces import (
+    ICustomStudent, ICustomStudentStudyCourse,
+    ICustomStudentStudyLevel, ICustomCourseTicket)
+from kofacustom.nigeria.testing import FunctionalLayer
+
+
+class CustomStudentFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomStudentFactoryTest, self).setUp()
+        self.factory = CustomStudentFactory()
+
+    def tearDown(self):
+        super(CustomStudentFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, CustomStudentFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, CustomStudent)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(ICustomStudent)
+
+class CustomStudentStudyCourseFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomStudentStudyCourseFactoryTest, self).setUp()
+        self.factory = CustomStudentStudyCourseFactory()
+
+    def tearDown(self):
+        super(CustomStudentStudyCourseFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, CustomStudentStudyCourseFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, CustomStudentStudyCourse)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(ICustomStudentStudyCourse)
+
+class CustomStudentStudyLevelFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomStudentStudyLevelFactoryTest, self).setUp()
+        self.factory = CustomStudentStudyLevelFactory()
+
+    def tearDown(self):
+        super(CustomStudentStudyLevelFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, CustomStudentStudyLevelFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, CustomStudentStudyLevel)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(ICustomStudentStudyLevel)
+
+class CustomCourseTicketFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CustomCourseTicketFactoryTest, self).setUp()
+        self.factory = CustomCourseTicketFactory()
+
+    def tearDown(self):
+        super(CustomCourseTicketFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, CustomCourseTicketFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, CustomCourseTicket)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(ICustomCourseTicket)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/viewlets.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/viewlets.py	(revision 8862)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/viewlets.py	(revision 8862)
@@ -0,0 +1,514 @@
+## $Id$
+##
+## 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
+##
+
+import grok
+from waeup.kofa.students.viewlets import (
+    FileDisplay, FileUpload, Image)
+from waeup.kofa.students.browser import ExportPDFClearanceSlipPage
+
+from kofacustom.nigeria.interfaces import MessageFactory as _
+
+# LGA Identification
+
+class LGAIdentificationDisplay(FileDisplay):
+    """LGA Identification display viewlet.
+    """
+    grok.order(1)
+    label = _(u'LGA Identification')
+    title = _(u'LGA Identification Scan')
+    download_name = u'lga_ident'
+
+class LGAIdentificationSlip(LGAIdentificationDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class LGAIdentificationUpload(FileUpload):
+    """LGA Identification upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'LGA Identification')
+    title = _(u'LGA Identification Scan')
+    mus = 1024 * 150
+    download_name = u'lga_ident'
+
+class LGAIdentificationImage(Image):
+    """Renders LGA Identification scan.
+    """
+    grok.name('lga_ident')
+    download_name = u'lga_ident'
+
+# First Sitting Result
+
+class FirstSittingResultDisplay(FileDisplay):
+    """First Sitting Result display viewlet.
+    """
+    grok.order(1)
+    label = _(u'First Sitting Result')
+    title = _(u'First Sitting Result')
+    download_name = u'fst_sit_scan'
+
+class FirstSittingResultSlip(FirstSittingResultDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class FirstSittingResultUpload(FileUpload):
+    """First Sitting Result upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'First Sitting Result')
+    title = _(u'First Sitting Result Scan')
+    mus = 1024 * 150
+    download_name = u'fst_sit_scan'
+
+class FirstSittingResultImage(Image):
+    """Renders First Sitting Result scan.
+    """
+    grok.name('fst_sit_scan')
+    download_name = u'fst_sit_scan'
+
+# Second Sitting Result
+
+class SecondSittingResultDisplay(FileDisplay):
+    """Second Sitting Result display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Second Sitting Result')
+    title = _(u'Second Sitting Result')
+    download_name = u'scd_sit_scan'
+
+class SecondSittingResultSlip(SecondSittingResultDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class SecondSittingResultUpload(FileUpload):
+    """Second Sitting Result upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Second Sitting Result')
+    title = _(u'Second Sitting Result Scan')
+    mus = 1024 * 150
+    download_name = u'scd_sit_scan'
+
+class SecondSittingResultImage(Image):
+    """Renders Second Sitting Result scan.
+    """
+    grok.name('scd_sit_scan')
+    download_name = u'scd_sit_scan'
+
+# Higher Qualification Result
+
+class HigherQualificationResultDisplay(FileDisplay):
+    """Higher Qualification Result display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Higher Qualification Result')
+    title = _(u'Higher Qualification Result')
+    download_name = u'hq_scan'
+
+class HigherQualificationResultSlip(HigherQualificationResultDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class HigherQualificationResultUpload(FileUpload):
+    """Higher Qualification Result upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Higher Qualification Result')
+    title = _(u'Higher Qualification Result Scan')
+    mus = 1024 * 150
+    download_name = u'hq_scan'
+
+class HigherQualificationResultImage(Image):
+    """Renders Higher Qualification Result scan.
+    """
+    grok.name('hq_scan')
+    download_name = u'hq_scan'
+
+# 2nd Higher Qualification Result (PG Students only)
+
+class SecondHigherQualificationResultDisplay(FileDisplay):
+    """Second Higher Qualification Result display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Second Higher Qualification Result')
+    title = _(u'Second Higher Qualification Result')
+    download_name = u'hq_scan2'
+
+class SecondHigherQualificationResultSlip(SecondHigherQualificationResultDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class SecondHigherQualificationResultUpload(FileUpload):
+    """Second Higher Qualification Result upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Second Higher Qualification Result')
+    title = _(u'Second Higher Qualification Result Scan')
+    mus = 1024 * 150
+    download_name = u'hq_scan2'
+
+    @property
+    def show_viewlet(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            return True
+        return False
+
+class SecondHigherQualificationResultImage(Image):
+    """Renders Second Higher Qualification Result scan.
+    """
+    grok.name('hq_scan2')
+    download_name = u'hq_scan2'
+
+# Advanced Level Result
+
+class AdvancedLevelResultDisplay(FileDisplay):
+    """Advanced Level Result display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Advanced Level Result')
+    title = _(u'Advanced Level Result')
+    download_name = u'alr_scan'
+
+class AdvancedLevelResultSlip(AdvancedLevelResultDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class AdvancedLevelResultUpload(FileUpload):
+    """Advanced Level Result upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Advanced Level Result')
+    title = _(u'Advanced Level Result Scan')
+    mus = 1024 * 150
+    download_name = u'alr_scan'
+
+class AdvancedLevelResultImage(Image):
+    """Renders Advanced Level Result scan.
+    """
+    grok.name('alr_scan')
+    download_name = u'alr_scan'
+
+# Certificate 
+
+class CertificateDisplay(FileDisplay):
+    """Certificate display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Certificate')
+    title = _(u'Certificate')
+    download_name = u'certificate'
+
+class CertificateSlip(CertificateDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class CertificateUpload(FileUpload):
+    """Certificate upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Certificate')
+    title = _(u'Certificate Scan')
+    mus = 1024 * 150
+    download_name = u'certificate'
+
+class CertificateImage(Image):
+    """Renders Certificate scan.
+    """
+    grok.name('certificate')
+    download_name = u'certificate'
+
+# Second Certificate (PG Students only)
+
+class SecondCertificateDisplay(FileDisplay):
+    """ Second Certificate display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Second Certificate')
+    title = _(u'Second Certificate')
+    download_name = u'certificate2'
+
+class SecondCertificateSlip(SecondCertificateDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class SecondCertificateUpload(FileUpload):
+    """Second Certificate upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Second Certificate')
+    title = _(u'Second Certificate Scan')
+    mus = 1024 * 150
+    download_name = u'certificate2'
+
+    @property
+    def show_viewlet(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            return True
+        return False
+
+class CertificateImage(Image):
+    """Renders Second Certificate scan.
+    """
+    grok.name('certificate2')
+    download_name = u'certificate2'
+
+# Third Certificate (PG Students only)
+
+class ThirdCertificateDisplay(FileDisplay):
+    """ Third Certificate display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Third Certificate')
+    title = _(u'Third Certificate')
+    download_name = u'certificate3'
+
+class ThirdCertificateSlip(ThirdCertificateDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class ThirdCertificateUpload(FileUpload):
+    """Third Certificate upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Third Certificate')
+    title = _(u'Third Certificate Scan')
+    mus = 1024 * 150
+    download_name = u'certificate3'
+
+    @property
+    def show_viewlet(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            return True
+        return False
+
+class CertificateImage(Image):
+    """Renders Third Certificate scan.
+    """
+    grok.name('certificate3')
+    download_name = u'certificate3'
+
+# Evidence of Name
+
+class EvidenceNameDisplay(FileDisplay):
+    """Evidence of Name display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Evidence of Name')
+    title = _(u'Evidence of Name')
+    download_name = u'evid'
+
+class EvidenceNameSlip(EvidenceNameDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class EvidenceNameUpload(FileUpload):
+    """Evidence of Name upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Evidence of Name')
+    title = _(u'Evidence of Name Scan')
+    mus = 1024 * 150
+    download_name = u'evid'
+
+class EvidenceNameImage(Image):
+    """Renders Evidence of Name scan.
+    """
+    grok.name('evid')
+    download_name = u'evid'
+
+# Result Statement
+
+class ResultStatementDisplay(FileDisplay):
+    """Result Statement display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Result Statement')
+    title = _(u'Result Statement')
+    download_name = u'res_stat'
+
+class ResultStatementSlip(ResultStatementDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class ResultStatementUpload(FileUpload):
+    """Result Statement upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Result Statement')
+    title = _(u'Result Statement Scan')
+    mus = 1024 * 150
+    download_name = u'res_stat'
+
+class ResultStatementImage(Image):
+    """Renders Result Statement scan.
+    """
+    grok.name('res_stat')
+    download_name = u'res_stat'
+
+# Referee Letter
+
+class RefereeLetterDisplay(FileDisplay):
+    """Referee Letter display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Referee Letter')
+    title = _(u'Referee Letter')
+    download_name = u'ref_let'
+
+class RefereeLetterSlip(RefereeLetterDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class RefereeLetterUpload(FileUpload):
+    """Referee Letter upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Referee Letter')
+    title = _(u'Referee Letter Scan')
+    mus = 1024 * 150
+    download_name = u'ref_let'
+
+class RefereeLetterImage(Image):
+    """Renders Referee Letter scan.
+    """
+    grok.name('ref_let')
+    download_name = u'ref_let'
+
+# Second Referee Letter (PG Students only)
+
+class SecondRefereeLetterDisplay(FileDisplay):
+    """Second Referee Letter display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Second Referee Letter')
+    title = _(u'Second Referee Letter')
+    download_name = u'ref_let2'
+
+class SecondRefereeLetterSlip(SecondRefereeLetterDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class SecondRefereeLetterUpload(FileUpload):
+    """Referee Letter upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Second Referee Letter')
+    title = _(u'Second Referee Letter Scan')
+    mus = 1024 * 150
+    download_name = u'ref_let2'
+
+    @property
+    def show_viewlet(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            return True
+        return False
+
+class SecondRefereeLetterImage(Image):
+    """Renders Referee Letter scan.
+    """
+    grok.name('ref_let2')
+    download_name = u'ref_let2'
+
+# Third Referee Letter (PG Students only)
+
+class ThirdRefereeLetterDisplay(FileDisplay):
+    """Third Referee Letter display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Third Referee Letter')
+    title = _(u'Third Referee Letter')
+    download_name = u'ref_let3'
+
+class ThirdRefereeLetterSlip(ThirdRefereeLetterDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class ThirdRefereeLetterUpload(FileUpload):
+    """Referee Letter upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Third Referee Letter')
+    title = _(u'Third Referee Letter Scan')
+    mus = 1024 * 150
+    download_name = u'ref_let3'
+
+    @property
+    def show_viewlet(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            return True
+        return False
+
+class ThirdRefereeLetterImage(Image):
+    """Renders Referee Letter scan.
+    """
+    grok.name('ref_let3')
+    download_name = u'ref_let3'
+
+# Statutory Declaration of Good Conduct
+
+class StatutoryDeclarationDisplay(FileDisplay):
+    """Statutory Declaration of Good Conduct display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Statutory Declaration of Good Conduct')
+    title = _(u'Statutory Declaration of Good Conduct')
+    download_name = u'stat_dec'
+
+class StatutoryDeclarationSlip(StatutoryDeclarationDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class StatutoryDeclarationUpload(FileUpload):
+    """Statutory Declaration of Good Conduct upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Statutory Declaration of Good Conduct')
+    title = _(u'Statutory Declaration of Good Conduct Scan')
+    mus = 1024 * 150
+    download_name = u'stat_dec'
+
+class StatutoryDeclarationImage(Image):
+    """Renders Statutory Declaration of Good Conduct scan.
+    """
+    grok.name('stat_dec')
+    download_name = u'stat_dec'
+
+# Letter of Admission (PG Students only)
+
+class LetterAdmissionDisplay(FileDisplay):
+    """Letter of Admission display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Letter of Admission')
+    title = _(u'Letter of Admission')
+    download_name = u'admission_let'
+
+class LetterAdmissionSlip(LetterAdmissionDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class LetterAdmissionUpload(FileUpload):
+    """Letter of Admission upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Letter of Admission')
+    title = _(u'Letter of Admission Scan')
+    mus = 1024 * 150
+    download_name = u'admission_let'
+
+    @property
+    def show_viewlet(self):
+        cm = getattr(self.context,'current_mode', None)
+        if cm is not None and cm.startswith('pg'):
+            return True
+        return False
+
+class LetterAdmissionImage(Image):
+    """Renders Letter of Admission scan.
+    """
+    grok.name('admission_let')
+    download_name = u'admission_let'
