Index: /main/waeup.aaue/trunk/src/waeup/aaue/applicants/applicant.py
===================================================================
--- /main/waeup.aaue/trunk/src/waeup/aaue/applicants/applicant.py	(revision 15112)
+++ /main/waeup.aaue/trunk/src/waeup/aaue/applicants/applicant.py	(revision 15113)
@@ -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
@@ -25,4 +27,12 @@
     ICustomApplicant, ICustomUGApplicantEdit, ICustomPGApplicantEdit,
     IPUTMEApplicantEdit, ITranscriptApplicant, ICertificateRequest)
+
+@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/waeup.aaue/trunk/src/waeup/aaue/applicants/browser.py
===================================================================
--- /main/waeup.aaue/trunk/src/waeup/aaue/applicants/browser.py	(revision 15112)
+++ /main/waeup.aaue/trunk/src/waeup/aaue/applicants/browser.py	(revision 15113)
@@ -224,7 +224,41 @@
     )
 
+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):
     """A display view for applicant data.
     """
+
+    @property
+    def file_links(self):
+        html = ''
+        pdf = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr='stateresult.pdf')
+        if pdf:
+            html += '<a href="stateresult.pdf">Statement of Result</a>'
+        return html
 
     @property
@@ -284,4 +318,9 @@
         return ''
 
+    def update(self):
+        super(CustomApplicantDisplayFormPage, self).update()
+        self.extraform_url = self.url(self.context, 'stateresult.pdf')
+        return
+
 class CustomPDFActionButton(PDFActionButton):
 
@@ -386,4 +425,18 @@
         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.
@@ -400,4 +453,8 @@
             if not self.request.form.get('confirm_passport', False):
                 return _('Passport picture confirmation box not ticked.')
+        if self.view.target in ('trans',) and \
+            not store.getFileByContext(self.context, attr=u'stateresult.pdf'):
+            return _('No statement of result pdf file uploaded.')
+
         return False
 
@@ -460,4 +517,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 CustomApplicantRegistrationPage(NigeriaApplicantRegistrationPage):
@@ -587,2 +661,15 @@
         return students_utils.renderPDF(self,'screening_data.pdf',
             self.context, applicantview, note=self.note)
+
+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/waeup.aaue/trunk/src/waeup/aaue/applicants/browser_templates/applicantdisplaypage.pt
===================================================================
--- /main/waeup.aaue/trunk/src/waeup/aaue/applicants/browser_templates/applicantdisplaypage.pt	(revision 15112)
+++ /main/waeup.aaue/trunk/src/waeup/aaue/applicants/browser_templates/applicantdisplaypage.pt	(revision 15113)
@@ -37,4 +37,12 @@
       </td>
     <tr>
+    <tr tal:condition="python: view.target in ('trans',)">
+      <td class="fieldname" i18n:translate="">
+        Files:
+      </td>
+      <td>
+        <span tal:replace="structure view/file_links" />
+      </td>
+    </tr>
   </tbody>
 </table>
Index: /main/waeup.aaue/trunk/src/waeup/aaue/applicants/browser_templates/applicanteditpage.pt
===================================================================
--- /main/waeup.aaue/trunk/src/waeup/aaue/applicants/browser_templates/applicanteditpage.pt	(revision 15112)
+++ /main/waeup.aaue/trunk/src/waeup/aaue/applicants/browser_templates/applicanteditpage.pt	(revision 15113)
@@ -42,4 +42,29 @@
           </span>
           <span tal:replace="view/max_upload_size">10 KB</span>
+      </tr>
+      <tr tal:condition="python: view.target in ('trans',)">
+        <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="">
+            Max. file size:
+          </span>
+          <span tal:replace="view/max_file_upload_size">10 KB</span>
       </tr>
       <tr tal:condition="view/manage_applications">
Index: /main/waeup.aaue/trunk/src/waeup/aaue/applicants/tests/test_browser.py
===================================================================
--- /main/waeup.aaue/trunk/src/waeup/aaue/applicants/tests/test_browser.py	(revision 15112)
+++ /main/waeup.aaue/trunk/src/waeup/aaue/applicants/tests/test_browser.py	(revision 15113)
@@ -20,9 +20,11 @@
 import pytz
 import os
+from StringIO import StringIO
 from zope.event import notify
 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
 from zope.component import createObject, getUtility
 from waeup.kofa.configuration import SessionConfiguration
-from waeup.kofa.interfaces import IUserAccount
+from waeup.kofa.interfaces import (
+    IUserAccount, IExtFileStore, IFileStoreNameChooser)
 from waeup.kofa.applicants.container import ApplicantsContainer
 from waeup.kofa.browser.tests.test_pdf import samples_dir
@@ -285,2 +287,74 @@
         self.assertFalse('Admitted Course of Study' in self.browser.contents)
         return
+
+    def test_upload_stateresult_by_manager(self):
+        # Add trans applicants container
+        self.transcontainer = ApplicantsContainer()
+        self.transcontainer.mode = 'create'
+        self.transcontainer.code = u'trans%s' % session_1
+        self.transcontainer.prefix = u'trans'
+        self.transcontainer.application_category = u'no'
+        self.transcontainer.year = session_1
+        self.transcontainer.application_fee = 300.0
+        self.transcontainer.title = u'This is the trans%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'
+        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/trans%s/%s'
+            % (session_1, transapplicant.application_number))
+        self.transapplicant_manage_path = ('http://localhost/app/applicants/trans%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('<a href="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 - waeup.aaue.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)
