Index: main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser.py
===================================================================
--- main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser.py	(revision 10088)
+++ main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser.py	(revision 10096)
@@ -19,6 +19,12 @@
 """
 import grok
+import os
+from zope.component import getUtility
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser)
 from zope.formlib.textwidgets import BytesDisplayWidget
+from waeup.kofa.utils.helpers import string_from_bytes, file_size
 from waeup.futminna.applicants.interfaces import (
+    ICustomApplicant,
     ICustomUGApplicant,
     ICustomPGApplicant,
@@ -34,4 +40,5 @@
     PG_OMIT_EDIT_FIELDS,
     )
+from waeup.futminna.interfaces import MessageFactory as _
 from kofacustom.nigeria.applicants.browser import (
     NigeriaApplicantDisplayFormPage,
@@ -40,4 +47,28 @@
     NigeriaPDFApplicationSlip)
 
+MAX_FILE_UPLOAD_SIZE = 1024 * 500
+
+def handle_file_upload(upload, context, view, attr=None):
+    """Handle upload of applicant files.
+
+    Returns `True` in case of success or `False`.
+
+    Please note that file pointer passed in (`upload`) most probably
+    points to end of file when leaving this function.
+    """
+    size = file_size(upload)
+    if size > MAX_FILE_UPLOAD_SIZE:
+        view.flash(_('Uploaded file is too big!'))
+        return False
+    dummy, ext = os.path.splitext(upload.filename)
+    ext.lower()
+    if ext != '.pdf':
+        view.flash(_('pdf file extension expected.'))
+        return False
+    upload.seek(0) # file pointer moved when determining size
+    store = getUtility(IExtFileStore)
+    file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
+    store.createFile(file_id, upload)
+    return True
 
 class CustomApplicantDisplayFormPage(NigeriaApplicantDisplayFormPage):
@@ -48,4 +79,13 @@
 
     @property
+    def file_links(self):
+        html = ''
+        pdf = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr='formextension.pdf')
+        if pdf:
+            html += '<a href="formextension.pdf">Offline Form Extension</a>'
+        return html
+
+    @property
     def form_fields(self):
         target = getattr(self.context.__parent__, 'prefix', None)
@@ -58,5 +98,6 @@
             for field in UG_OMIT_DISPLAY_FIELDS:
                 form_fields = form_fields.omit(field)
-        form_fields['perm_address'].custom_widget = BytesDisplayWidget
+        if form_fields.get('perm_address', None):
+            form_fields['perm_address'].custom_widget = BytesDisplayWidget
         form_fields['notice'].custom_widget = BytesDisplayWidget
         if not getattr(self.context, 'student_id'):
@@ -70,4 +111,9 @@
         return form_fields
 
+    def update(self):
+        super(CustomApplicantDisplayFormPage, self).update()
+        self.formextension_url = self.url(self.context, 'formextension.pdf')
+        return
+
 class CustomPDFApplicationSlip(NigeriaPDFApplicationSlip):
 
@@ -96,5 +142,6 @@
     """A full edit view for applicant data.
     """
-    
+    grok.template('applicanteditpage')
+
     @property
     def form_fields(self):
@@ -112,7 +159,20 @@
         return form_fields
 
+    def update(self):
+        super(CustomApplicantManageFormPage, self).update()
+        upload_formextension = self.request.form.get('form.formextension', None)
+        if upload_formextension:
+            # We got a fresh formextension upload
+            self.upload_success = handle_file_upload(
+                upload_formextension, self.context, self, attr='formextension.pdf')
+            if self.upload_success:
+                self.context.writeLogMessage(self, 'saved: formextension')
+        self.max_file_upload_size = string_from_bytes(MAX_FILE_UPLOAD_SIZE)
+        return
+
 class CustomApplicantEditFormPage(NigeriaApplicantEditFormPage):
     """An applicant-centered edit view for applicant data.
     """
+    grok.template('applicanteditpage')
 
     @property
@@ -130,2 +190,40 @@
         form_fields['reg_number'].for_display = True
         return form_fields
+
+    def dataNotComplete(self):
+        store = getUtility(IExtFileStore)
+        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
+            return _('No passport picture uploaded.')
+        if not store.getFileByContext(self.context, attr=u'formextension.pdf'):
+            return _('No form extension pdf file uploaded.')
+        if not self.request.form.get('confirm_passport', False):
+            return _('Passport picture confirmation box not ticked.')
+        return False
+
+    def update(self):
+        if self.context.locked or (
+            self.context.__parent__.expired and
+            self.context.__parent__.strict_deadline):
+            self.emit_lock_message()
+            return
+        super(CustomApplicantEditFormPage, self).update()
+        upload_formextension = self.request.form.get('form.formextension', None)
+        if upload_formextension:
+            # We got a fresh formextension upload
+            self.upload_success = handle_file_upload(
+                upload_formextension, self.context, self, attr='formextension.pdf')
+        self.max_file_upload_size = string_from_bytes(MAX_FILE_UPLOAD_SIZE)
+        return
+
+class FormExtension(grok.View):
+    """Renders the pdf form extension for applicants.
+    """
+    grok.name('formextension.pdf')
+    grok.context(ICustomApplicant)
+    grok.require('waeup.viewApplication')
+
+    def render(self):
+        pdf = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr='formextension.pdf')
+        self.response.setHeader('Content-Type', 'application/pdf')
+        return pdf
Index: main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser_templates/applicantdisplaypage.pt
===================================================================
--- main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser_templates/applicantdisplaypage.pt	(revision 10088)
+++ main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser_templates/applicantdisplaypage.pt	(revision 10096)
@@ -36,4 +36,12 @@
       </td>
     <tr>
+    <tr>
+      <td class="fieldname" i18n:translate="">
+        Files:
+      </td>
+      <td>
+        <span tal:replace="structure view/file_links" />
+      </td>
+    </tr>
   </tbody>
 </table>
Index: main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser_templates/applicanteditpage.pt
===================================================================
--- main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser_templates/applicanteditpage.pt	(revision 10096)
+++ main/waeup.futminna/trunk/src/waeup/futminna/applicants/browser_templates/applicanteditpage.pt	(revision 10096)
@@ -0,0 +1,153 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <h2 i18n:domain="waeup.kofa">
+    ...
+    <span tal:replace="context/translated_state">APPLICATIONSTATE
+    </span>
+    <span tal:omit-tag=""
+      i18n:translate="" tal:condition="context/suspended">(account suspended)
+    </span>
+    ...
+  </h2>
+
+  <div>
+    <span  i18n:translate="">
+      <strong>Notice:</strong>
+      Download, fill, scan and re-upload the following forms:
+    </span>
+    <a href="https://futminna.waeup.org/downloads/ApplicationFormExtension.pdf"
+        i18n:translate="">
+      Offline Form Extension
+    </a><br /><br />
+  </div>
+
+  <div class="workflow">
+    <div tal:repeat="msg context/history/messages">
+      <span tal:replace="msg">MESSAGE</span><br />
+    </div>
+  </div>
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tr>
+        <td class="fieldname" i18n:translate="">
+          Passport Photo:
+        </td>
+        <td>
+          <img src="passport.jpg" height="180px" /><br />
+          <input type="file" name="form.passport" />
+          <br />
+          <span i18n:translate="">
+            Max. file size:
+          </span>
+          <span tal:replace="view/max_upload_size">10 KB</span>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+          Offline Form Extension:
+        </td>
+        <td>
+          <p tal:condition="python: view.file_exists('formextension.pdf')">
+            <a href="formextension.pdf"
+               i18n:translate="">
+              Download pdf file
+            </a>
+          </p>
+          <input type="file" name="form.formextension" />
+          <br />
+          <span i18n:translate="">
+            Max. file size:
+          </span>
+          <span tal:replace="view/max_file_upload_size">10 KB</span>
+      </tr>
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Password:</td>
+        <td>
+          <input name="password" type="password"  />
+        </td>
+      </tr>
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Retype Password:</td>
+        <td>
+          <input name="control_password" type="password" />
+        </td>
+      </tr>
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Application Transition:</td>
+        <td>
+          <select id="transition" name="transition">
+            <option tal:repeat="transition view/getTransitions"
+                    tal:attributes="value transition/name">
+              <span tal:replace="transition/title">TRANSITIONTITLE</span>
+            </option>
+          </select>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div tal:condition="not: view/manage_applications">
+    <input id="confirm_passport" name="confirm_passport"
+           type="checkbox" value="True"/>
+    <span i18n:translate="">
+    I confirm that the Passport Photograph uploaded on this form is a
+    true picture of me.
+    </span>
+  </div>
+  <br />
+
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:condition="python:action.label in view.display_actions[0]"
+             tal:replace="structure action/render"/>
+    </span>
+  </div>
+
+  <br /><br />
+  <h3 i18n:translate="">
+      Acceptance Fee Payment Tickets
+  </h3>
+
+  <table>
+    <thead>
+    <tr>
+      <th>&nbsp;</th>
+      <th i18n:translate="">Payment Id</th>
+      <th i18n:translate="">Creation Date</th>
+      <th i18n:translate="">Payment Date</th>
+      <th i18n:translate="">Category</th>
+      <th i18n:translate="">Item</th>
+      <th i18n:translate="">State</th>
+    </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="cl context/values">
+         <td>
+          <input type="checkbox"
+    		 name="val_id"
+    		 tal:attributes="value cl/__name__"
+         tal:condition="python: not view.unremovable(cl)" />
+        </td>
+        <td> <a tal:attributes="href cl/__name__">
+        <span tal:content="cl/p_id">PID</span></a></td>
+        <td tal:content="python: layout.formatDatetime(cl.creation_date)">CREATION DATE</td>
+        <td tal:content="python: layout.formatDatetime(cl.payment_date)">PAYMENT DATE</td>
+      <td tal:content ="cl/category">CATEGORY</td>
+      <td tal:content ="cl/display_item">ITEM</td>
+      <td tal:content ="cl/state">STATE</td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:condition="python:action.label in view.display_actions[1]"
+             tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: main/waeup.futminna/trunk/src/waeup/futminna/applicants/interfaces.py
===================================================================
--- main/waeup.futminna/trunk/src/waeup/futminna/applicants/interfaces.py	(revision 10088)
+++ main/waeup.futminna/trunk/src/waeup/futminna/applicants/interfaces.py	(revision 10096)
@@ -136,5 +136,5 @@
         """
 
-class ICustomUGApplicantEdit(INigeriaUGApplicantEdit):
+class ICustomUGApplicantEdit(ICustomUGApplicant):
     """An undergraduate applicant interface for edit forms.
 
@@ -148,5 +148,5 @@
     """
 
-class ICustomPGApplicantEdit(INigeriaPGApplicantEdit):
+class ICustomPGApplicantEdit(ICustomPGApplicant):
     """A postgraduate applicant interface for editing.
 
@@ -160,19 +160,8 @@
     """
 
+
 class ICustomApplicantOnlinePayment(INigeriaApplicantOnlinePayment):
     """An applicant payment via payment gateways.
 
-    """
-
-class IPUTMEApplicantEdit(IPUTMEApplicantEdit):
-    """An undergraduate applicant interface for editing.
-
-    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.
     """
 
Index: main/waeup.futminna/trunk/src/waeup/futminna/applicants/tests/test_browser.py
===================================================================
--- main/waeup.futminna/trunk/src/waeup/futminna/applicants/tests/test_browser.py	(revision 10096)
+++ main/waeup.futminna/trunk/src/waeup/futminna/applicants/tests/test_browser.py	(revision 10096)
@@ -0,0 +1,152 @@
+## $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
+##
+"""
+Test the applicant-related UI components.
+"""
+
+import os
+from StringIO import StringIO
+from zope.component import createObject, getUtility
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser)
+
+from waeup.futminna.testing import FunctionalLayer
+
+PH_LEN = 15911  # Length of placeholder file
+
+
+class ApplicantUITests(ApplicantsFullSetup):
+    # Tests for uploading/browsing the passport image of appplicants
+
+    layer = FunctionalLayer
+
+    def test_upload_image_by_student(self):
+        self.login()
+        self.browser.open(self.edit_path)
+        # Create a pseudo image file and select it to be uploaded in form
+        photo_content = 'A' * 1024 * 21  # A string of 21 KB size
+        pseudo_image = StringIO(photo_content)
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click() # submit form
+        # There is a correct <img> link included
+        self.assertTrue(
+            '<img src="passport.jpg" height="180px" />' in self.browser.contents)
+        # We get a warning message
+        self.assertTrue(
+            'Uploaded image is too big' in self.browser.contents)
+        # Browsing the image gives us the default image, not the
+        # uploaded one.
+        image_url = self.edit_path.replace('edit', 'passport.jpg')
+        self.browser.open(image_url)
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), PH_LEN)
+        # There is really no file stored for the applicant
+        img = getUtility(IExtFileStore).getFile(
+            IFileStoreNameChooser(self.applicant).chooseName())
+        self.assertTrue(img is None)
+
+    def test_upload_formextension_by_student(self):
+        self.login()
+        self.browser.open(self.edit_path)
+        # Create a pseudo file and select it to be uploaded in form
+        pdf_content = 'A' * 1024 * 600  # A string of 600 KB size
+        pseudo_pdf = StringIO(pdf_content)
+        ctrl = self.browser.getControl(name='form.formextension')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_pdf, filename='myform.pdf')
+        self.browser.getControl("Save").click() # submit form
+        # We get a warning message
+        self.assertTrue(
+            'Uploaded file is too big' in self.browser.contents)
+        # Create a pseudo file with acceptable size
+        pdf_content = 'A' * 1024 * 300  # A string of 300 KB size
+        pseudo_pdf = StringIO(pdf_content)
+        ctrl = self.browser.getControl(name='form.formextension')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_pdf, filename='myform.pdf')
+        self.browser.getControl("Save").click() # submit form
+        # Even though the form could not be saved ...
+        self.assertTrue('Required input is missing' in self.browser.contents)
+        # ... the file has been successfully uploaded
+        pdf_url = self.edit_path.replace('edit', 'formextension.pdf')
+        self.browser.open(pdf_url)
+        self.assertEqual(
+            self.browser.headers['content-type'], 'application/pdf')
+        self.assertEqual(len(self.browser.contents), 307200)
+        # There is rally a file stored for the applicant
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.applicant).chooseName(
+            attr='formextension.pdf')
+        # The stored file can be fetched
+        fd = storage.getFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(file_len, 307200)
+        # A file link is displayed on the edit view ...
+        self.browser.open(self.edit_path)
+        self.assertTrue('<a href="formextension.pdf">' in self.browser.contents)
+        # ... and on the dislay view
+        self.browser.open(self.view_path)
+        self.assertTrue('<a href="formextension.pdf">Offline Form Extension</a>'
+            in self.browser.contents)
+
+    def test_upload_formextension_by_manager(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        # Create a pseudo file with acceptable size
+        pdf_content = 'A' * 1024 * 300  # A string of 300 KB size
+        pseudo_pdf = StringIO(pdf_content)
+        ctrl = self.browser.getControl(name='form.formextension')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_pdf, filename='myform.pdf')
+        self.browser.getControl("Save").click() # submit form
+        # Even though the form could not be saved ...
+        self.assertTrue('Required input is missing' in self.browser.contents)
+        # ... the file has been successfully uploaded
+        pdf_url = self.manage_path.replace('manage', 'formextension.pdf')
+        self.browser.open(pdf_url)
+        self.assertEqual(
+            self.browser.headers['content-type'], 'application/pdf')
+        self.assertEqual(len(self.browser.contents), 307200)
+        # There is rally a file stored for the applicant
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.applicant).chooseName(
+            attr='formextension.pdf')
+        # The stored file can be fetched
+        fd = storage.getFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(file_len, 307200)
+        # A file link is displayed on the edit view ...
+        self.browser.open(self.manage_path)
+        self.assertTrue('<a href="formextension.pdf">' in self.browser.contents)
+        # ... and on the dislay view
+        self.browser.open(self.view_path)
+        self.assertTrue('<a href="formextension.pdf">Offline Form Extension</a>'
+            in self.browser.contents)
+        # Adding file is properly logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - waeup.futminna.applicants.browser.CustomApplicantManageFormPage'
+            ' - %s - saved: formextension'
+            % (self.applicant.applicant_id)
+            in logcontent)
Index: main/waeup.futminna/trunk/src/waeup/futminna/applicants/viewlets.py
===================================================================
--- main/waeup.futminna/trunk/src/waeup/futminna/applicants/viewlets.py	(revision 10096)
+++ main/waeup.futminna/trunk/src/waeup/futminna/applicants/viewlets.py	(revision 10096)
@@ -0,0 +1,36 @@
+## $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.browser.viewlets import ManageActionButton
+from waeup.futminna.applicants.interfaces import ICustomApplicant
+
+from waeup.futminna.interfaces import MessageFactory as _
+
+
+#class OriginalApplicationButton(ManageActionButton):
+#    grok.order(4)
+#    grok.context(ICustomApplicant)
+#    grok.require('waeup.viewApplication')
+#    icon = 'actionicon_pdf.png'
+#    text = _('Download application form extension')
+
+#    @property
+#    def target_url(self):
+#        """Get a URL to the target...
+#        """
+#        return 'https://futminna.waeup.org/downloads/ApplicationFormExtension.pdf'
