Index: /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/applicant.py
===================================================================
--- /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/applicant.py	(revision 15877)
+++ /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/applicant.py	(revision 15878)
@@ -19,4 +19,6 @@
 import grok
 from zope.interface import implementedBy
+from zope.component import getUtility
+from waeup.kofa.interfaces import IExtFileStore
 from waeup.kofa.applicants.applicant import ApplicantFactory
 from waeup.kofa.utils.helpers import attrs_to_fields
@@ -24,4 +26,12 @@
 from kofacustom.iuokada.applicants.interfaces import(
     ICustomApplicant, ICustomUGApplicantEdit, ICustomPGApplicantEdit, IPUTMEApplicantEdit)
+
+@grok.subscribe(ICustomApplicant, grok.IObjectRemovedEvent)
+def custom_handle_applicant_removed(applicant, event):
+    """If an applicant is removed also pdf files of this applicant are removed.
+    """
+    file_store = getUtility(IExtFileStore)
+    file_store.deleteFileByContext(applicant, attr='stateresult.pdf')
+    return
 
 class CustomApplicant(NigeriaApplicant):
Index: /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser.py
===================================================================
--- /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser.py	(revision 15877)
+++ /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser.py	(revision 15878)
@@ -19,5 +19,10 @@
 """
 import grok
+import os
+from zope.component import getUtility
 from zope.formlib.textwidgets import BytesDisplayWidget
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser)
+from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
 from waeup.kofa.applicants.browser import (
     ApplicantRegistrationPage, ApplicantsContainerPage)
@@ -43,9 +48,34 @@
     )
 from kofacustom.iuokada.applicants.interfaces import (
-    ICustomPGApplicant, ICustomUGApplicant,
+    ICustomPGApplicant, ICustomUGApplicant, ICustomApplicant,
     ICustomPGApplicantEdit, ICustomUGApplicantEdit,
     ICustomApplicantOnlinePayment
     )
 from kofacustom.iuokada.interfaces import MessageFactory as _
+
+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 CustomApplicantsContainerPage(ApplicantsContainerPage):
@@ -73,4 +103,14 @@
 
     @property
+    def file_links(self):
+        html = ''
+        pdf = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr='stateresult.pdf')
+        if pdf:
+            html += '<a href="%s">Statement of Result</a>' % self.url(
+                self.context, 'stateresult.pdf')
+        return html
+
+    @property
     def form_fields(self):
         if self.target is not None and self.target.startswith('pg'):
@@ -112,8 +152,35 @@
         return form_fields
 
+    def update(self):
+        super(CustomApplicantManageFormPage, self).update()
+        upload_stateresult = self.request.form.get('form.stateresult', None)
+        if upload_stateresult:
+            # We got a fresh stateresult upload
+            success = handle_file_upload(
+                upload_stateresult, self.context, self, attr='stateresult.pdf')
+            if success:
+                self.context.writeLogMessage(self, 'saved: stateresult')
+            else:
+                self.upload_success = False
+        self.max_file_upload_size = string_from_bytes(MAX_FILE_UPLOAD_SIZE)
+        return
+
 class CustomApplicantEditFormPage(NigeriaApplicantEditFormPage):
     """An applicant-centered edit view for applicant data.
     """
 
+    def dataNotComplete(self, data):
+        store = getUtility(IExtFileStore)
+        if self.context.__parent__.with_picture:
+            store = getUtility(IExtFileStore)
+            if not store.getFileByContext(self.context, attr=u'passport.jpg'):
+                return _('No passport picture uploaded.')
+            if not self.request.form.get('confirm_passport', False):
+                return _('Passport picture confirmation box not ticked.')
+        if self.context.subtype == 'transfer' and \
+            not store.getFileByContext(self.context, attr=u'stateresult.pdf'):
+            return _('No statement of result pdf file uploaded.')
+        return False
+
     @property
     def form_fields(self):
@@ -129,4 +196,21 @@
         form_fields['reg_number'].for_display = True
         return form_fields
+
+    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_stateresult = self.request.form.get('form.stateresult', None)
+        if upload_stateresult:
+            # We got a fresh stateresult upload
+            success = handle_file_upload(
+                upload_stateresult, self.context, self, attr='stateresult.pdf')
+            if not success:
+                self.upload_success = False
+        self.max_file_upload_size = string_from_bytes(MAX_FILE_UPLOAD_SIZE)
+        return
 
 class CustomPDFApplicationSlip(NigeriaPDFApplicationSlip):
@@ -152,2 +236,14 @@
         return form_fields
 
+class StateResult(grok.View):
+    """Renders the pdf form extension for applicants.
+    """
+    grok.name('stateresult.pdf')
+    grok.context(ICustomApplicant)
+    grok.require('waeup.viewApplication')
+
+    def render(self):
+        pdf = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr='stateresult.pdf')
+        self.response.setHeader('Content-Type', 'application/pdf')
+        return pdf
Index: /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser_templates/applicantdisplaypage.pt
===================================================================
--- /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser_templates/applicantdisplaypage.pt	(revision 15878)
+++ /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser_templates/applicantdisplaypage.pt	(revision 15878)
@@ -0,0 +1,80 @@
+<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 class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<img tal:condition="python: context.__parent__.with_picture"
+     src="" height="180px" tal:attributes="src view/passport_url" />
+
+<table i18n:domain="waeup.kofa" class="form-table">
+  <tbody>
+    <tal:files tal:condition="python: view.context.subtype == 'transfer'">
+      <tr>
+        <td class="separator" colspan=2>
+          Files
+        </td>
+      </tr>
+      <tr>
+        <td colspan=2>
+          <span tal:replace="structure view/file_links" />
+        </td>
+      </tr>
+    </tal:files>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="view/getCourseAdmitted">
+      <td class="fieldname" i18n:translate="">
+          Admitted Course of Study:
+      </td>
+      <td>
+        <span tal:replace="structure view/getCourseAdmitted" />
+      </td>
+    </tr>
+    <tr>
+      <td class="fieldname" i18n:translate="">
+          Password:
+      </td>
+      <td>
+          <tal:password replace="view/hasPassword" />
+      </td>
+    <tr>
+  </tbody>
+</table>
+<tal:payments condition="view/display_payments">
+  <h3 i18n:domain="waeup.kofa" i18n:translate="">
+      Payment Tickets
+  </h3>
+  <table i18n:domain="waeup.kofa" class="table table-condensed">
+    <thead>
+      <tr>
+        <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="value context/payments">
+        <td> <a tal:attributes="href python:view.url(value)">
+        <span tal:content="value/p_id">PID</span></a></td>
+        <td tal:content="python: layout.formatDatetime(value.creation_date)">CREATION DATE</td>
+        <td tal:content="python: layout.formatDatetime(value.payment_date)">PAYMENT DATE</td>
+        <td tal:content ="value/category">CATEGORY</td>
+        <td tal:content ="value/display_item">ITEM</td>
+        <td tal:content ="value/p_state_title">STATE</td>
+      </tr>
+    </tbody>
+  </table>
+</tal:payments>
Index: /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser_templates/applicanteditpage.pt
===================================================================
--- /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser_templates/applicanteditpage.pt	(revision 15878)
+++ /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/browser_templates/applicanteditpage.pt	(revision 15878)
@@ -0,0 +1,191 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <h2 i18n:domain="waeup.kofa" tal:condition="context/translated_state">
+    ...
+    <span tal:replace="context/translated_state">APPLICATIONSTATE
+    </span>
+    <span tal:omit-tag=""
+      i18n:translate="" tal:condition="context/suspended">(account suspended)
+    </span>
+    ...
+  </h2>
+
+  <div class="workflow" tal:condition="context/history">
+    <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 tal:condition="python: context.__parent__.with_picture">
+        <td class="fieldname" i18n:translate="">
+          Passport Photo:
+        </td>
+        <td>
+          <img src="passport.jpg" height="180px" /><br />
+          <br />
+      		<div class="input-group half">
+      			<div class="input-group-btn">
+      				<div class="btn btn-default btn-file">
+                Select&hellip;
+                <input type="file" name="form.passport" />
+              </div>
+      			</div>
+      			<input type="text" class="form-control" readonly>
+          </div>
+          <span i18n:translate="">
+            Max. file size:
+          </span>
+          <span tal:replace="view/max_upload_size">10 KB</span>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+          Statement of Result:
+        </td>
+        <td>
+          <p tal:condition="python: view.file_exists('stateresult.pdf')">
+            <a href="stateresult.pdf"
+               i18n:translate="">
+              Download pdf file
+            </a>
+          </p>
+          <div class="input-group half">
+            <div class="input-group-btn">
+              <div class="btn btn-default btn-file">
+                Select&hellip;
+              <input type="file" name="form.stateresult" />
+              </div>
+            </div>
+            <input type="text" class="form-control" readonly>
+          </div>
+          <span i18n:translate="">
+            PDF files only. 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 class="form-control" name="password" type="password"  />
+        </td>
+      </tr>
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Retype Password:</td>
+        <td>
+          <input class="form-control" 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" class="form-control half">
+            <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="python: not view.manage_applications
+                              and context.__parent__.with_picture">
+    <input id="confirm_passport" name="confirm_passport"
+           type="checkbox" value="True"/>&nbsp;&nbsp;
+    <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>
+
+  <tal:refereereports condition="view/display_refereereports">
+    <br /><br />
+    <h3 i18n:translate="">
+        Referee Reports
+    </h3>
+    <table i18n:domain="waeup.kofa" class="table table-condensed">
+      <thead>
+      <tr>
+        <th i18n:translate="">Report Id</th>
+        <th i18n:translate="">Creation Date</th>
+        <th i18n:translate="">Referee</th>
+        <th i18n:translate="">Email Address</th>
+      </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="cl context/refereereports">
+          <td> <a tal:attributes="href python:view.url(cl)">
+            <span tal:content="cl/r_id">RID</span></a></td>
+          <td tal:content="python: layout.formatDatetime(cl.creation_date)">CREATION DATE</td>
+          <td tal:content="cl/name">REFEREE</td>
+          <td tal:content="cl/email">EMAIL</td>
+        </tr>
+      </tbody>
+    </table>
+  </tal:refereereports>
+
+  <tal:payments condition="view/display_payments">
+    <br /><br />
+    <h3 i18n:translate="">
+        Payment Tickets
+    </h3>
+
+    <table i18n:domain="waeup.kofa" class="table table-condensed">
+      <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/payments">
+          <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 python:view.url(cl)">
+          <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/p_state_title">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>
+    <br />
+    <p i18n:translate="" tal:condition="context/special">
+      <strong>Notice:</strong> If you need to pay for another service,
+      select the corresponding special application category above,
+      click 'Save' and then 'Add online payment ticket' again.
+    </p>
+  </tal:payments>
+</form>
Index: /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/tests/test_browser.py
===================================================================
--- /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/tests/test_browser.py	(revision 15877)
+++ /main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/applicants/tests/test_browser.py	(revision 15878)
@@ -19,13 +19,23 @@
 Test the applicant-related UI components.
 """
+import grok
+import datetime
+import pytz
 import os
-import datetime
+from StringIO import StringIO
+from zope.event import notify
 from zope.component import createObject, getUtility
 from zope.catalog.interfaces import ICatalog
 from zope.intid.interfaces import IIntIds
 from hurry.workflow.interfaces import IWorkflowState
+from zope.component import createObject, getUtility
+from waeup.kofa.configuration import SessionConfiguration
+from waeup.kofa.interfaces import (
+    IUserAccount, IExtFileStore, IFileStoreNameChooser)
 from kofacustom.iuokada.testing import FunctionalLayer
+from waeup.kofa.applicants.container import ApplicantsContainer
 from waeup.kofa.browser.tests.test_pdf import samples_dir
-from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.applicants.tests.test_browser import (ApplicantsFullSetup,
+    ApplicantsFullSetup, container_name_1, session_1)
 from waeup.kofa.applicants.tests.test_batching import ApplicantImportExportSetup
 from kofacustom.iuokada.applicants.export import CustomApplicantExporter
@@ -91,2 +101,75 @@
         open(path, 'wb').write(self.browser.contents)
         print "Sample application_slip.pdf written to %s" % path
+
+    def test_upload_stateresult_by_manager(self):
+        # Add trans applicants container
+        self.transcontainer = ApplicantsContainer()
+        self.transcontainer.mode = 'create'
+        self.transcontainer.code = u'ug%s' % session_1
+        self.transcontainer.prefix = u'ug'
+        self.transcontainer.application_category = u'no'
+        self.transcontainer.year = session_1
+        self.transcontainer.application_fee = 300.0
+        self.transcontainer.title = u'This is the ug%s container' % session_1
+        self.app['applicants'][self.transcontainer.code] = self.transcontainer
+        delta = datetime.timedelta(days=10)
+        self.transcontainer.startdate = datetime.datetime.now(pytz.utc) - delta
+        self.transcontainer.enddate = datetime.datetime.now(pytz.utc) + delta
+        # Add applicant
+        transapplicant = createObject(u'waeup.Applicant')
+        transapplicant.firstname = u'Anna'
+        transapplicant.lastname = u'Post'
+        transapplicant.subtype = 'transfer'
+        self.app['applicants'][self.transcontainer.code].addApplicant(transapplicant)
+        self.transapplicant = self.app['applicants'][self.transcontainer.code][
+            transapplicant.application_number]
+        self.transapplicant_view_path = ('http://localhost/app/applicants/ug%s/%s'
+            % (session_1, transapplicant.application_number))
+        self.transapplicant_manage_path = ('http://localhost/app/applicants/ug%s/%s/manage'
+            % (session_1, transapplicant.application_number))
+        # Login
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.transapplicant_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.stateresult')
+        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.transapplicant_manage_path.replace('manage', 'stateresult.pdf')
+        self.browser.open(pdf_url)
+        self.assertEqual(
+            self.browser.headers['content-type'], 'application/pdf')
+        self.assertEqual(len(self.browser.contents), 307200)
+        # There is really a file stored for the applicant
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.transapplicant).chooseName(
+            attr='stateresult.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.transapplicant_manage_path)
+        self.assertTrue('<a href="stateresult.pdf">' in self.browser.contents)
+        # ... and on the dislay view
+        self.browser.open(self.transapplicant_view_path)
+        self.assertTrue('stateresult.pdf">Statement of Result</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 - kofacustom.iuokada.applicants.browser.CustomApplicantManageFormPage'
+            ' - %s - saved: stateresult'
+            % (self.transapplicant.applicant_id)
+            in logcontent)
+        # When an applicant is removed, also the pdf files are gone.
+        del self.app['applicants'][self.transcontainer.code][self.transapplicant.application_number]
+        fd = storage.getFile(file_id)
+        self.assertTrue(fd is None)
