Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/app.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/app.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/app.py	(revision 7002)
@@ -9,5 +9,6 @@
 from waeup.sirp.students.container import StudentsContainer
 from waeup.sirp.hostels.container import HostelsContainer
-from waeup.sirp.imagestorage import ImageStorageFileRetrieval, ImageStorage
+from waeup.sirp.imagestorage import (
+    ImageStorageFileRetrieval, ImageStorage, ExtFileStore,)
 from waeup.sirp.interfaces import (
     IUniversity, IWAeUPSIRPPluggable, IObjectUpgradeEvent, )
@@ -28,7 +29,9 @@
         setup = setup_authentication,)
 
-    grok.local_utility(
-        ImageStorageFileRetrieval, provides = IFileRetrieval)
+    #grok.local_utility(
+    #    ImageStorageFileRetrieval, provides = IFileRetrieval)
 
+    #grok.local_utility(
+    #    ExtFileStore, provides = IFileRetrieval)
     def __init__(self, *args, **kw):
         super(University, self).__init__(*args, **kw)
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/__init__.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/__init__.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/__init__.py	(revision 7002)
@@ -3,5 +3,6 @@
 # Make this a package.
 from waeup.sirp.applicants.applicant import (
-    ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser,
+    ResultEntry, Applicant, ApplicantFactory, ApplicantImageStoreHandler,
+    get_regno_or_ac, ApplicantImageNameChooser,
     )
 from waeup.sirp.applicants.container import ApplicantsContainer
@@ -14,8 +15,10 @@
     'Applicant',
     'ApplicantFactory',
+    'ApplicantImageNameChooser',
+    'ApplicantImageStoreHandler',
     'ApplicantsContainer',
     'ApplicantsRoot',
-    'ApplicantTraverser',
     'application_exists',
     'get_applicant_data',
+    'get_regno_or_ac',
     ]
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/applicant.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/applicant.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/applicant.py	(revision 7002)
@@ -20,15 +20,24 @@
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##
+import os
 import grok
 from grok import index
+from zope.component import getUtility
 from zope.component.interfaces import IFactory
 from zope.interface import implementedBy
 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
-from waeup.sirp.interfaces import IObjectHistory
+from waeup.sirp.interfaces import IObjectHistory, IExtFileStore
 from waeup.sirp.app import University
 from waeup.sirp.applicants.interfaces import (
     IResultEntry, IApplicant, IApplicantEdit, default_passport_image,
     )
+from waeup.sirp.image import WAeUPImageFile
 from waeup.sirp.utils.helpers import attrs_to_fields
+
+def get_regno_or_ac(context):
+    reg_no = getattr(context, 'reg_no', None)
+    if reg_no is None:
+        return getattr(context, 'access_code', None)
+    return reg_no
 
 class ResultEntry(grok.Context):
@@ -81,30 +90,43 @@
     access_code = index.Field(attribute='access_code')
 
-class ApplicantTraverser(grok.Traverser):
-    """Get image of the context applicant.
-
-    Each applicant can provide a passport photograph which will be
-    returned by this traverser if:
-
-    - we request the exact filename of the picture or
-
-    - ask for a picture named 'passport.jpg'.
-
-    If no picture was stored yet, we get a placeholder image when
-    asking for `passport.jpg`.
-
-    If none of the above applies, we return ``None``, most probably
-    resulting a :exc:`NotFound` exception.
-
-    """
-    grok.context(IApplicant)
-    def traverse(self, name):
-        passport_filename = getattr(self.context.passport, 'filename', None)
-        if name == passport_filename:
-            return self.context.passport
-        if name == 'passport.jpg':
-            if self.context.passport is not None:
-                return self.context.passport
-        return
+#class ApplicantTraverser(grok.Traverser):
+#    """Get image of the context applicant.
+#
+#    Each applicant can provide a passport photograph which will be
+#    returned by this traverser if:
+#
+#    - we request the exact filename of the picture or
+#
+#    - ask for a picture named 'passport.jpg'.
+#
+#    If no picture was stored yet, we get a placeholder image when
+#    asking for `passport.jpg`.
+#
+#    If none of the above applies, we return ``None``, most probably
+#    resulting a :exc:`NotFound` exception.
+#
+#    """
+#    grok.context(IApplicant)
+#    def traverse(self, name):
+#        if name != 'passport.jpg':
+#            return
+#        marked_filename = '__img_appl__%s.jpg' % (
+#            get_regno_or_ac(self.context))
+#        image = getUtility(IExtFileStore).getFile(marked_filename)
+#        if image is None:
+#            # Return placeholder
+#            from waeup.sirp.applicants.interfaces import IMAGE_PATH
+#            return open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg'), 'rb')
+#            pass
+#        return image #WAeUPImageFile(marked_filename, image.read())
+#        if not hasattr(self.context, 'passport'):
+#            return None
+#        passport_filename = getattr(self.context.passport, 'filename', None)
+#        if name == passport_filename:
+#            return self.context.passport
+#        if name == 'passport.jpg':
+#            if self.context.passport is not None:
+#                return self.context.passport
+#        return
 
 class ApplicantFactory(grok.GlobalUtility):
@@ -121,2 +143,55 @@
     def getInterfaces(self):
         return implementedBy(Applicant)
+
+from waeup.sirp.interfaces import IFileStoreHandler, IFileStoreNameChooser
+from waeup.sirp.imagestorage import DefaultFileStoreHandler
+
+APPLICANT_IMAGE_STORE_NAME = 'img-applicant'
+
+class ApplicantImageNameChooser(grok.Adapter):
+    grok.context(IApplicant)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None):
+        return name == self.chooseName(name, self.context)
+
+    def chooseName(self, name=None):
+        parent_name = getattr(
+            getattr(self.context, '__parent__', None),
+            '__name__', '_default')
+        marked_filename = '__%s__%s/%s.jpg' % (
+            APPLICANT_IMAGE_STORE_NAME,
+            parent_name, get_regno_or_ac(self.context))
+        return marked_filename
+
+
+class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
+    """Applicant specific image handling.
+
+    This handler knows in which path in a filestore to store applicant
+    images and how to turn this kind of data into some (browsable)
+    file object.
+
+    It is called from the global file storage, when it wants to
+    get/store a file with a file id starting with
+    ``__img-applicant__`` (the marker string for applicant images).
+
+    Like each other file store handler it does not handle the files
+    really (this is done by the global file store) but only computes
+    paths and things like this.
+    """
+    grok.implements(IFileStoreHandler)
+    grok.name(APPLICANT_IMAGE_STORE_NAME)
+
+    def pathFromFileID(self, store, root, file_id):
+        """All applicants images are filed in directory ``applicants``.
+        """
+        marker, filename, basename, ext = store.extractMarker(file_id)
+        return os.path.join(root, 'applicants', filename)
+
+    def createFile(self, store, root, filename, file_id, file):
+        """Create a browsable file-like object.
+        """
+        # possible other actions: check for jpeg format
+        path = self.pathFromFileID(store, root, file_id)
+        return file, path, WAeUPImageFile(filename, file_id)
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser.py	(revision 7002)
@@ -22,4 +22,5 @@
 """UI components for basic applicants and related components.
 """
+import os
 import sys
 import grok
@@ -57,5 +58,6 @@
     )
 from waeup.sirp.image.image import createWAeUPImageFile
-from waeup.sirp.interfaces import IWAeUPObject, ILocalRolesAssignable
+from waeup.sirp.interfaces import (
+    IWAeUPObject, ILocalRolesAssignable, IExtFileStore, IFileStoreNameChooser)
 from waeup.sirp.permissions import get_users_with_local_roles
 from waeup.sirp.university.interfaces import ICertificate
@@ -65,8 +67,10 @@
 from waeup.sirp.widgets.objectwidget import (
     WAeUPObjectWidget, WAeUPObjectDisplayWidget)
-from waeup.sirp.applicants import ResultEntry, Applicant, get_applicant_data
+from waeup.sirp.applicants import (
+    ResultEntry, Applicant, get_applicant_data, get_regno_or_ac)
 from waeup.sirp.applicants.interfaces import (
     IApplicant, IApplicantPrincipal,IApplicantEdit, IApplicantsRoot,
-    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab
+    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab,
+    IMAGE_PATH,
     )
 from waeup.sirp.applicants.workflow import INITIALIZED, STARTED
@@ -551,9 +555,13 @@
         'locked').omit('course_admitted')
     #form_fields['fst_sit_results'].custom_widget = list_results_display_widget
-    form_fields['passport'].custom_widget = ThumbnailWidget
+    #form_fields['passport'].custom_widget = ThumbnailWidget
     form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
     label = 'Applicant'
     grok.template('form_display')
     pnav = 3
+
+    def update(self):
+        self.passport_url = self.url(self.context, 'passport.jpg')
+        return
 
     @property
@@ -705,5 +713,5 @@
     grok.require('waeup.manageApplications')
     form_fields = grok.AutoFields(IApplicant)
-    form_fields['passport'].custom_widget = EncodingImageFileWidget
+    #form_fields['passport'].custom_widget = EncodingImageFileWidget
     form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
     grok.template('form_edit')
@@ -718,5 +726,18 @@
         if upload:
             # We got a fresh upload
-            image = createWAeUPImageFile(upload.filename, upload)
+            #from waeup.sirp.interfaces import IExtFileStorage
+            #from zope.component import queryUtility
+            #storage = queryUtility(IExtFileStorage)
+            # filename = '__img_appl__' + str(self.context.reg_no) #upload.filename
+            from waeup.sirp.interfaces import (
+                IExtFileStore, IFileStoreNameChooser)
+            store = getUtility(IExtFileStore)
+            file_id = IFileStoreNameChooser(self.context).chooseName()
+            store.createFile(file_id, upload)
+            #filename = '__img_appl__' + str(
+            #    self.context.access_code) + '.jpg' #upload.filename
+            print "FILENAME: ", file_id
+            # image = createWAeUPImageFile(upload.filename, upload)
+            #image = createWAeUPImageFile(filename, upload)
             # This would normally be totally illegal. We set context
             # data without the complete form data being
@@ -731,5 +752,5 @@
             # we set the image here and not in 'save()' or other
             # methods.
-            self.context.passport = image
+            #self.context.passport = image
             upload.seek(0)
             self.passport_changed = True
@@ -782,5 +803,5 @@
         'screening_score',
         )
-    form_fields['passport'].custom_widget = EncodingImageFileWidget
+    #form_fields['passport'].custom_widget = EncodingImageFileWidget
     form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
     grok.template('form_edit')
@@ -868,2 +889,22 @@
     text = 'View application record'
     target = 'index'
+
+class PassportImage(grok.View):
+    """Renders the passport image for applicants.
+    """
+    grok.name('passport.jpg')
+    grok.context(IApplicant)
+    grok.require('waeup.handleApplication')
+
+    def render(self):
+        # A filename chooser turns a context into a filename suitable
+        # for file storage.
+        chooser = IFileStoreNameChooser(self.context)
+        file_id = chooser.chooseName()
+        image = getUtility(IExtFileStore).getFile(file_id)
+        self.response.setHeader(
+            'Content-Type', 'image/jpeg')
+        if image is None:
+            # show placeholder image
+            return open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg'), 'rb')
+        return image
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser_templates/form_display.pt
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser_templates/form_display.pt	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser_templates/form_display.pt	(revision 7002)
@@ -13,4 +13,7 @@
 <table class="zebra">
   <tbody>
+    <tr><td></td>
+    <td><img src="" tal:attributes="src view/passport_url" /></td>
+    </tr>
     <tal:block repeat="widget view/widgets">
       <tr>
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser_templates/form_edit.pt
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser_templates/form_edit.pt	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/browser_templates/form_edit.pt	(revision 7002)
@@ -46,4 +46,12 @@
         </tr>
       </tal:block>
+      <tr>
+        <td class="label">Photograph</td>
+        <td class="field">
+          <span class="widget">
+            <img src="passport.jpg" /><br />
+            <input type="file" name="form.passport" />
+          </span>
+      </tr>
       <tr tal:condition="view/manage_applications">
         <td class="label"><label>Application Transition:</label></td>
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/interfaces.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/interfaces.py	(revision 7002)
@@ -370,12 +370,12 @@
         required = False,
         )
-    passport = ImageFile(
-        title = u'Passport Photograph',
-        #default = DEFAULT_PASSPORT_IMAGE_MALE,
-        defaultFactory = default_passport_image,
-        description = u'Maximun file size is 20 kB.',
-        required = True,
-        max_size = 20480,
-        )
+    #passport = ImageFile(
+    #    title = u'Passport Photograph',
+    #    #default = DEFAULT_PASSPORT_IMAGE_MALE,
+    #    defaultFactory = default_passport_image,
+    #    description = u'Maximun file size is 20 kB.',
+    #    required = True,
+    #    max_size = 20480,
+    #    )
 
     #
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/tests/test_applicant.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 7002)
@@ -22,9 +22,12 @@
 """Tests for applicants and related.
 """
+import grok
+import md5
 import unittest
 from StringIO import StringIO
 from hurry.file.interfaces import IFileRetrieval
 from zope.component import (
-    provideAdapter, adapts, getGlobalSiteManager, provideUtility)
+    provideAdapter, adapts, getGlobalSiteManager, provideUtility,
+    queryUtility)
 from zope.component.hooks import setSite
 from zope.component.interfaces import IFactory
@@ -36,11 +39,14 @@
 from waeup.sirp.image import WAeUPImageFile, createWAeUPImageFile
 from waeup.sirp.image.interfaces import IWAeUPImageFile
+from waeup.sirp.imagestorage import ImageStorageFileRetrieval, DefaultStorage
+from waeup.sirp.interfaces import IFileStoreHandler, IFileStoreNameChooser
 from waeup.sirp.applicants import (
-    ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser,
+    ResultEntry, Applicant, ApplicantFactory, get_regno_or_ac,
+    ApplicantImageStoreHandler, ApplicantImageNameChooser,
     )
 from waeup.sirp.applicants.interfaces import (
     IResultEntry, IApplicant,
     )
-from waeup.sirp.imagestorage import ImageStorageFileRetrieval
+
 from waeup.sirp.testing import (FunctionalTestCase, FunctionalLayer)
 
@@ -51,4 +57,94 @@
         pass
 
+class HelperTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_get_regno_or_ac(self):
+        # we can get reg_no or access_code of an applicants if it is set
+        appl1 = Applicant()
+        appl2 = Applicant()
+        appl2.reg_no = u'foo'
+        appl3 = Applicant()
+        appl3.access_code = u'bar'
+        appl4 = Applicant()
+        appl4.reg_no = u'foo'
+        appl4.access_code = u'bar'
+        self.assertTrue(
+            get_regno_or_ac(appl1) is None)
+        self.assertEqual(
+            get_regno_or_ac(appl2), u'foo')
+        self.assertEqual(
+            get_regno_or_ac(appl3), u'bar')
+        self.assertEqual(
+            get_regno_or_ac(appl4), u'foo')
+        return
+
+    def test_image_store_handler_util_accessible(self):
+        # we can get an IFileStoreHandler utility for applicants
+        handler = queryUtility(IFileStoreHandler, name='img-applicant')
+        self.assertTrue(
+            isinstance(handler, ApplicantImageStoreHandler))
+        return
+
+    def test_image_store_handler(self):
+        store = DefaultStorage()
+        handler = queryUtility(IFileStoreHandler, name='img-applicant')
+        result1 = handler.pathFromFileID(
+            store, '/fake-root', '__img-applicant__sample.jpg')
+        result2 = handler.pathFromFileID(
+            store, '/fake-root', '__img-applicant__dir1/sample.jpg')
+        result3 = handler.pathFromFileID(
+            store, '/fake-root', '__img-applicant__dir1/dir2/sample.jpg')
+        self.assertEqual(
+            result1, '/fake-root/applicants/sample.jpg')
+        self.assertEqual(
+            result2, '/fake-root/applicants/dir1/sample.jpg')
+        self.assertEqual(
+            result3, '/fake-root/applicants/dir1/dir2/sample.jpg')
+        return
+
+class ApplicantImageNameChooserTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # make sure we implement promised interfaces
+        obj = ApplicantImageNameChooser(None) # needs a context normally
+        verify.verifyClass(IFileStoreNameChooser, ApplicantImageNameChooser)
+        verify.verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_name_chooser_available(self):
+        # we can get a name chooser for applicant objects as adapter
+        appl = Applicant()
+        chooser = IFileStoreNameChooser(appl)
+        self.assertTrue(chooser is not None)
+        return
+
+    def test_name_chooser_applicant_wo_container(self):
+        # we can get an image filename for applicants not in a container
+        appl = Applicant()
+        appl.reg_no = u'MY_REG_NO'
+        chooser = IFileStoreNameChooser(appl)
+        result = chooser.chooseName()
+        # the file would be stored in a ``_default`` directory.
+        self.assertEqual(
+            result, '__img-applicant___default/MY_REG_NO.jpg')
+        return
+
+    def test_name_chooser_applicant_w_container(self):
+        appl = Applicant()
+        appl.reg_no = u'MY_REG_NO'
+        fake_container = grok.Container()
+        fake_container.__name__ = 'folder'
+        fake_container['appl'] = appl
+        appl.__parent__ = fake_container
+        chooser = IFileStoreNameChooser(appl)
+        result = chooser.chooseName()
+        self.assertEqual(
+            result, '__img-applicant__folder/MY_REG_NO.jpg')
+        return
+
 class ResultEntryTest(unittest.TestCase):
 
@@ -87,5 +183,5 @@
         return
 
-    def test_passport_no_site(self):
+    def DISABLEDtest_passport_no_site(self):
         # make sure we get a real image stored in passport attr
         image = self.applicant.passport.file.read()
@@ -93,5 +189,5 @@
         return
 
-    def test_passport_insite(self):
+    def DISABLEDtest_passport_insite(self):
         # make sure we get a real image in passport attr when inside a site.
         # When an applicant is created 'inside a site', its passport
@@ -103,5 +199,11 @@
         applicant = Applicant()
         image = self.applicant.passport.file.read()
+        # The image is really the placeholder_m.jpg image
         self.assertEqual(len(image), 2059)
+        self.assertEqual(md5.new(image).hexdigest(),
+                         u'b48a1d39bbcb32e955d9ff2dea4ed0e6')
+        # As the applicant has no reg_no yet, the image data is 'empty'
+        self.assertEqual(self.applicant.passport.data,
+                         '__img_appl__placeholder_m.jpg')
         # The image contains a file_id instead of real image-data
         self.assertEqual(self.applicant.passport.data,
@@ -118,5 +220,5 @@
         # Install a IFileRetrieval utility that returns WAeUPImageFiles.
         storage = ImageStorageFileRetrieval()
-        provideUtility(storage, IFileRetrieval)
+        #provideUtility(storage, IFileRetrieval)
         self.factory = ApplicantFactory()
         return
@@ -137,65 +239,2 @@
         implemented_by = self.factory.getInterfaces()
         assert implemented_by.isOrExtends(IApplicant)
-
-
-class ApplicantTraverserTest(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(ApplicantTraverserTest, self).setUp()
-        provideAdapter(FakeImageLocation)
-        self.applicant = Applicant()
-
-        self.request = TestRequest('')
-        return
-
-    def tearDown(self):
-        gsm = getGlobalSiteManager()
-        gsm.unregisterAdapter(FakeImageLocation)
-        super(ApplicantTraverserTest, self).tearDown()
-        return
-
-    def test_traverse_wo_passport(self):
-        # Ask for some attribute not provided
-        traverser = ApplicantTraverser(
-            self.applicant, self.request
-            )
-        self.assertRaises(
-            NotFound,
-            traverser.publishTraverse, self.request, 'passport'
-            )
-        return
-
-    def test_traverse_wo_image_passport_jpg(self):
-        # Ask for applicant pic if we didn't provided one
-        # We get a placeholder.
-        traverser = ApplicantTraverser(
-            self.applicant, self.request
-            )
-        result = traverser.publishTraverse(self.request, 'passport.jpg')
-        self.assertTrue(isinstance(result, FakeImageLocation))
-        return
-
-    def test_traverse_w_image_passport_jpg(self):
-        # Ask for applicant pic that's named 'passport.jpg'
-        traverser = ApplicantTraverser(
-            self.applicant, self.request
-            )
-        self.applicant.passport = createWAeUPImageFile(
-            'nofile.jpg', StringIO('no-content'))
-        self.applicant.passport.filename = 'mypic.jpg'
-        result = traverser.publishTraverse(self.request, 'passport.jpg')
-        self.assertTrue(isinstance(result, FakeImageLocation))
-        return
-
-    def test_traverse_w_image_some_jpg(self):
-        # Ask for applicant pic by correct name
-        traverser = ApplicantTraverser(
-            self.applicant, self.request
-            )
-        self.applicant.passport = WAeUPImageFile('nofile.jpg', '')
-        self.applicant.passport.filename = 'mypic.jpg'
-        result = traverser.publishTraverse(self.request, 'mypic.jpg')
-        self.assertTrue(isinstance(result, FakeImageLocation))
-        return
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/tests/test_browser.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7002)
@@ -28,5 +28,5 @@
 from datetime import datetime, date, timedelta
 from mechanize import LinkNotFoundError
-from zope.component import createObject
+from zope.component import createObject, getUtility
 from zope.component.hooks import setSite, clearSite
 from zope.security.interfaces import Unauthorized
@@ -37,4 +37,5 @@
 from waeup.sirp.applicants.container import ApplicantsContainer
 from waeup.sirp.applicants.applicant import Applicant
+from waeup.sirp.interfaces import IExtFileStore, IFileStoreNameChooser
 from waeup.sirp.university.faculty import Faculty
 from waeup.sirp.university.department import Department
@@ -350,9 +351,9 @@
         self.browser.getControl(name="SUBMIT").click()
         pin = self.pins[2]
-        appl = self.getRootFolder()['app']['applicants']['app2009']
-        appl = appl[pin]
-        passp = appl.passport
-        passp_len = len(passp.file.read())
-        self.assertEqual(passp_len, PH_LEN)
+        #appl = self.getRootFolder()['app']['applicants']['app2009']
+        #appl = appl[pin]
+        #passp = appl.passport
+        #passp_len = len(passp.file.read())
+        #self.assertEqual(passp_len, PH_LEN)
         #image_url = "%s/%s" % (self.browser.url, 'placeholder.jpg')
         image_url = "%s/%s" % (self.browser.url, 'passport.jpg')
@@ -639,12 +640,12 @@
         # There is a correct <img> link included
         self.assertTrue(
-            '<img src="placeholder_m.jpg" />' in self.browser.contents)
+            '<img src="passport.jpg" />' in self.browser.contents)
         # Browsing the link shows a real image
-        self.browser.open(self.image_url('placeholder_m.jpg'))
+        self.browser.open(self.image_url('passport.jpg'))
         self.assertEqual(
             self.browser.headers['content-type'], 'image/jpeg')
         self.assertEqual(len(self.browser.contents), PH_LEN)
 
-    def test_after_login_default_stored_in_imagestorage(self):
+    def DISABLEDtest_after_login_default_stored_in_imagestorage(self):
         # After login the applicants placeholder image is stored in
         # an imagestorage
@@ -672,27 +673,10 @@
         # There is a correct <img> link included
         self.assertTrue(
-            '<img src="placeholder_m.jpg" />' in self.browser.contents)
+            '<img src="passport.jpg" />' in self.browser.contents)
         # Browsing the link shows a real image
-        self.browser.open(self.image_url('placeholder_m.jpg'))
+        self.browser.open(self.image_url('passport.jpg'))
         self.assertEqual(
             self.browser.headers['content-type'], 'image/jpeg')
         self.assertEqual(len(self.browser.contents), PH_LEN)
-
-    def test_after_submit_default_stored_in_imagestorage(self):
-        # After submitting an applicant form the default image is
-        # correctly stored in an imagestorage
-        self.login()
-        self.browser.getControl("Save").click() # submit form
-        storage = self.app['images']
-        self.assertEqual(
-            [x for x in storage.keys()],
-            [u'b48a1d39bbcb32e955d9ff2dea4ed0e6'])
-        file_id = self.applicant.passport.data
-        self.assertEqual(
-            file_id, u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')
-        # The stored image can be fetched
-        fd = storage.retrieveFile(file_id)
-        file_len = len(fd.read())
-        self.assertEqual(file_len, PH_LEN)
 
     def test_uploaded_image_browsable_w_errors(self):
@@ -709,7 +693,7 @@
         # There is a correct <img> link included
         self.assertTrue(
-            '<img src="myphoto.jpg" />' in self.browser.contents)
+            '<img src="passport.jpg" />' in self.browser.contents)
         # Browsing the link shows a real image
-        self.browser.open(self.image_url('myphoto.jpg'))
+        self.browser.open(self.image_url('passport.jpg'))
         self.assertEqual(
             self.browser.headers['content-type'], 'image/jpeg')
@@ -726,15 +710,10 @@
         file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
         self.browser.getControl("Save").click() # submit form
-        storage = self.app['images']
-        self.assertTrue(
-            u'18e57c7eac6ca7fb15b54b5b2bd4106d' in storage.keys())
-        # The stored image can be fetched
-        fd = storage.retrieveFile(u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')
-        file_len = len(fd.read())
-        self.assertEqual(file_len, 31)
-        # The image uploaded belongs to the applicant
-        file_id = self.applicant.passport.data
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.applicant).chooseName()
+        pseudo_image.seek(0) # reset our file data source
         self.assertEqual(
-            file_id, u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')
+            storage.getFile(file_id).read(), pseudo_image.read())
+        return
 
     def test_uploaded_image_browsable_wo_errors(self):
@@ -751,7 +730,7 @@
         # There is a correct <img> link included
         self.assertTrue(
-            '<img src="myphoto.jpg" />' in self.browser.contents)
+            '<img src="passport.jpg" />' in self.browser.contents)
         # Browsing the link shows a real image
-        self.browser.open(self.image_url('myphoto.jpg'))
+        self.browser.open(self.image_url('passport.jpg'))
         self.assertEqual(
             self.browser.headers['content-type'], 'image/jpeg')
@@ -770,15 +749,10 @@
         self.browser.getControl("Save").click() # submit form
         storage = self.app['images']
-        self.assertTrue(
-            u'18e57c7eac6ca7fb15b54b5b2bd4106d' in storage.keys())
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.applicant).chooseName()
         # The stored image can be fetched
-        fd = storage.retrieveFile(u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')
-        #fd = storage.retrieveFile(file_id)
+        fd = storage.getFile(file_id)
         file_len = len(fd.read())
         self.assertEqual(file_len, 31)
-        # The image uploaded belongs to the applicant
-        file_id = self.applicant.passport.data
-        self.assertEqual(
-            file_id, u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')
 
     def test_uploaded_images_equal(self):
@@ -797,12 +771,14 @@
         file_ctrl = ctrl.mech_control
         file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
-        passport0 = self.applicant.passport
+        file_id = IFileStoreNameChooser(self.applicant).chooseName()
+        setSite(self.app)
+        passport0 = getUtility(IExtFileStore).getFile(file_id)
         self.browser.getControl("Save").click() # submit form with changed pic
-        passport1 = self.applicant.passport
+        passport1 = getUtility(IExtFileStore).getFile(file_id).read()
         self.browser.getControl("Save").click() # submit form w/o changes
-        passport2 = self.applicant.passport
+        passport2 = getUtility(IExtFileStore).getFile(file_id).read()
+        self.assertTrue(passport0 is None)
         self.assertTrue(passport0 != passport1)
         self.assertTrue(passport1 == passport2)
-        self.assertTrue(passport1 is passport2)
         return
 
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/browser/browser.txt
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/browser/browser.txt	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/browser/browser.txt	(revision 7002)
@@ -1064,5 +1064,5 @@
 
   >>> sorted(os.listdir(uploadpath))
-  ['finished', 'logs', 'unfinished']
+  ['finished', 'logs', 'media', 'unfinished']
 
 
@@ -1105,5 +1105,5 @@
 
   >>> sorted(os.listdir(uploadpath))
-  ['finished', 'logs', 'myfaculties_zope.mgr.csv', 'unfinished']
+  ['finished', 'logs', 'media', 'myfaculties_zope.mgr.csv', 'unfinished']
 
 We create and upload also a CSV file containing departments:
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/imagestorage.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/imagestorage.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/imagestorage.py	(revision 7002)
@@ -25,4 +25,5 @@
 import hashlib
 import os
+import tempfile
 import transaction
 import warnings
@@ -30,6 +31,11 @@
 from ZODB.blob import Blob
 from persistent import Persistent
+from hurry.file import HurryFile
 from hurry.file.interfaces import IFileRetrieval
+from zope.component import queryUtility
+from zope.interface import Interface
 from waeup.sirp.image import WAeUPImageFile
+from waeup.sirp.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
 from waeup.sirp.utils.helpers import cmp_files
 
@@ -42,4 +48,26 @@
     """
     return hashlib.md5(fd.read()).hexdigest()
+
+class FileStoreNameChooser(grok.Adapter):
+    grok.context(Interface)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name):
+        """Check whether an object name is valid.
+
+        Raises a user error if the name is not valid.
+        """
+        pass
+
+    def chooseName(self, name):
+        """Choose a unique valid name for the object.
+
+        The given name and object may be taken into account when
+        choosing the name.
+
+        chooseName is expected to always choose a valid name (that
+        would pass the checkName test) and never raise an error.
+        """
+        return u'unknown_file'
 
 class Basket(grok.Container):
@@ -216,2 +244,168 @@
         file_id = storage.storeFile(f, filename)
         return WAeUPImageFile(filename, file_id)
+
+
+class ExtFileStore(object): #grok.GlobalUtility):
+    """External file store.
+
+    External file stores are meant to store files 'externally' of the
+    ZODB, i.e. in filesystem.
+
+    Most important attribute of the external file store is the `root`
+    path which gives the path to the location where files will be
+    stored within.
+
+    By default `root` is a ``'media/'`` directory in the root of the
+    datacenter root of a site.
+
+    The `root` attribute is 'read-only' because you normally don't
+    want to change this path -- it is dynamic. That means, if you call
+    the file store from 'within' a site, the root path will be located
+    inside this site (a :class:`waeup.sirp.University` instance). If
+    you call it from 'outside' a site some temporary dir (always the
+    same during lifetime of the file store instance) will be used. The
+    term 'temporary' tells what you can expect from this path
+    persistence-wise.
+
+    If you insist, you can pass a root path on initialization to the
+    constructor but when calling from within a site afterwards, the
+    site will override your setting for security measures. This way
+    you can safely use one file store for different sites in a Zope
+    instance simultanously and files from one site won't show up in
+    another.
+
+    An ExtFileStore instance is available as a global utility
+    implementing :iface:`waeup.sirp.interfaces.IExtFileStore`.
+
+    To add and retrieve files from the storage, use the appropriate
+    methods below.
+    """
+
+    grok.implements(IExtFileStore)
+
+    _root = None
+
+    @property
+    def root(self):
+        """Root dir of this storage.
+
+        The root dir is a readonly value determined dynamically. It
+        holds media files for sites or other components.
+
+        If a site is available we return a ``media/`` dir in the
+        datacenter storage dir.
+
+        Otherwise we create a temporary dir which will be remembered
+        on next call.
+
+        If a site exists and has a datacenter, it has always
+        precedence over temporary dirs, also after a temporary
+        directory was created.
+
+        Please note that retrieving `root` is expensive. You might
+        want to store a copy once retrieved in order to minimize the
+        number of calls to `root`.
+
+        """
+        site = grok.getSite()
+        if site is not None:
+            root = os.path.join(site['datacenter'].storage, 'media')
+            return root
+        if self._root is None:
+            self._root = tempfile.mkdtemp()
+        return self._root
+
+    def __init__(self, root=None):
+        self._root = root
+        return
+
+    def getFile(self, file_id):
+        """Get a file stored under file ID `file_id`.
+
+        If the file cannot be found ``None`` is returned.
+        """
+        marker, filename, base, ext = self.extractMarker(file_id)
+        handler = queryUtility(IFileStoreHandler, name=marker,
+                               default=DefaultFileStoreHandler())
+        path = handler.pathFromFileID(self, self.root, file_id)
+        if not os.path.exists(path):
+            return None
+        fd = open(path, 'rb')
+        return fd
+
+    def createFile(self, filename, f):
+        """Store a file.
+        """
+        file_id = filename
+        root = self.root # Calls to self.root are expensive
+        marker, filename, base, ext = self.extractMarker(file_id)
+        handler = queryUtility(IFileStoreHandler, name=marker,
+                               default=DefaultFileStoreHandler())
+        f, path, file_obj = handler.createFile(
+            self, root, file_id, filename, f)
+        dirname = os.path.dirname(path)
+        if not os.path.exists(dirname):
+            os.makedirs(dirname, 0755)
+        open(path, 'wb').write(f.read())
+        return file_obj
+
+    def extractMarker(self, file_id):
+        """split filename into marker, filename, basename, and extension.
+
+        A marker is a leading part of a string of form
+        ``__MARKERNAME__`` followed by the real filename. This way we
+        can put markers into a filename to request special processing.
+
+        Returns a quadruple
+
+          ``(marker, filename, basename, extension)``
+
+        where ``marker`` is the marker in lowercase, filename is the
+        complete trailing real filename, ``basename`` is the basename
+        of the filename and ``extension`` the filename extension of
+        the trailing filename. See examples below.
+
+        Example:
+
+           >>> extractMarker('__MaRkEr__sample.jpg')
+           ('marker', 'sample.jpg', 'sample', '.jpg')
+
+        If no marker is contained, we assume the whole string to be a
+        real filename:
+
+           >>> extractMarker('no-marker.txt')
+           ('', 'no-marker.txt', 'no-marker', '.txt')
+
+        Filenames without extension give an empty extension string:
+
+           >>> extractMarker('no-marker')
+           ('', 'no-marker', 'no-marker', '')
+
+        """
+        if not isinstance(file_id, basestring) or not file_id:
+            return ('', '', '', '')
+        parts = file_id.split('__', 2)
+        marker = ''
+        if len(parts) == 3 and parts[0] == '':
+            marker = parts[1].lower()
+            file_id = parts[2]
+        basename, ext = os.path.splitext(file_id)
+        return (marker, file_id, basename, ext)
+
+grok.global_utility(ExtFileStore, provides=IExtFileStore)
+
+class DefaultStorage(ExtFileStore):
+    grok.provides(IFileRetrieval)
+
+grok.global_utility(DefaultStorage, provides=IFileRetrieval)
+
+class DefaultFileStoreHandler(grok.GlobalUtility):
+    grok.implements(IFileStoreHandler)
+
+    def pathFromFileID(self, store, root, file_id):
+        return os.path.join(root, file_id)
+
+    def createFile(self, store, root, filename, file_id, f):
+        path = self.pathFromFileID(store, root, file_id)
+        return f, path, WAeUPImageFile(filename, file_id)
+        return path, HurryFile(filename, file_id)
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/interfaces.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/interfaces.py	(revision 7002)
@@ -3,4 +3,5 @@
 import os
 from datetime import datetime
+from hurry.file.interfaces import IFileRetrieval
 from hurry.workflow.interfaces import IWorkflow, IWorkflowInfo
 from zc.sourcefactory.basic import BasicSourceFactory
@@ -8,4 +9,5 @@
 from zope.component import getUtility
 from zope.component.interfaces import IObjectEvent
+from zope.container.interfaces import INameChooser
 from zope.interface import Interface, Attribute, implements
 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
@@ -548,2 +550,75 @@
         """Unregister a logger.
         """
+
+#
+# External File Storage and relatives
+#
+class IFileStoreNameChooser(INameChooser):
+    """See zope.container.interfaces.INameChooser for base methods.
+    """
+    def checkName(name):
+        """Check whether an object name is valid.
+
+        Raises a user error if the name is not valid.
+        """
+
+    def chooseName(name):
+        """Choose a unique valid name for the object.
+
+        The given name and object may be taken into account when
+        choosing the name.
+
+        chooseName is expected to always choose a valid name (that would pass
+        the checkName test) and never raise an error.
+        """
+
+class IExtFileStore(IFileRetrieval):
+    """A file storage that stores files in filesystem (not as blobs).
+    """
+    root = schema.TextLine(
+        title = u'Root path of file store.',
+        )
+
+    def getFile(file_id):
+        """Get raw file data stored under file with `file_id`.
+
+        Returns a file descriptor open for reading.
+        """
+
+    def createFile(filename, f):
+        """Create file given by f with filename `filename`
+
+        Returns a hurry.file.File-based object.
+        """
+
+class IFileStoreHandler(Interface):
+    """Filestore handlers handle specific files for file stores.
+
+    If a file to store/get provides a specific filename, a file store
+    looks up special handlers for that type of file.
+
+    """
+    def pathFromFileID(store, root, filename):
+        """Turn file id into path to store.
+
+        Returned path should be absolute.
+        """
+
+    def createFile(store, root, filename, file_id, file):
+        """Return some hurry.file based on `store` and `file_id`.
+
+        Some kind of callback method called by file stores to create
+        file objects from file_id.
+
+        Returns a tuple ``(raw_file, path, file_like_obj)`` where the
+        ``file_like_obj`` should be a HurryFile, a WAeUPImageFile or
+        similar. ``raw_file`` is the (maybe changed) input file and
+        ``path`` the relative internal path to store the file at.
+
+        Please make sure the ``raw_file`` is opened for reading and
+        the file descriptor set at position 0 when returned.
+
+        This method also gets the raw input file object that is about
+        to be stored and is expected to raise any exceptions if some
+        kind of validation or similar fails.
+        """
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/tests/test_app.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/tests/test_app.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/tests/test_app.py	(revision 7002)
@@ -43,5 +43,4 @@
         assert result is not None
         assert IFileRetrieval.providedBy(result)
-        assert isinstance(result, ImageStorageFileRetrieval)
         return
 
Index: /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/tests/test_imagestorage.py
===================================================================
--- /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/tests/test_imagestorage.py	(revision 7001)
+++ /main/waeup.sirp/branches/ulif-extimgstore/src/waeup/sirp/tests/test_imagestorage.py	(revision 7002)
@@ -5,12 +5,20 @@
 import unittest
 from StringIO import StringIO
+from hurry.file import HurryFile
 from hurry.file.interfaces import IFileRetrieval
+from zope.component import getUtility, provideUtility, queryUtility
 from zope.component.hooks import setSite
 from zope.interface.verify import verifyClass, verifyObject
+from grokcore.site import Site
 from waeup.sirp.app import University
+from waeup.sirp.datacenter import DataCenter
 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
 from waeup.sirp.image import createWAeUPImageFile
 from waeup.sirp.imagestorage import (
-    md5digest, Basket, ImageStorage, ImageStorageFileRetrieval,)
+    md5digest, Basket, ImageStorage, ImageStorageFileRetrieval,
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.sirp.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
 
 class HelperFuncsTests(unittest.TestCase):
@@ -287,5 +295,5 @@
         return
 
-    def test_waeupimagefile(self):
+    def DISABLEDtest_waeupimagefile(self):
         # Make sure WAeUPImageFile can use our file retrieval
         setSite(self.app)
@@ -294,5 +302,5 @@
         self.assertEqual(contents, 'Hi there!')
 
-    def test_waeupimagefile_raw(self):
+    def DISABLEDtest_waeupimagefile_raw(self):
         # Make sure we can retrieve a file also if it was initialized
         # with no image storage available
@@ -301,2 +309,208 @@
         contents = myfile.file.read()
         self.assertEqual(contents, 'Hi there!')
+
+class NameChooserTests(unittest.TestCase):
+
+    def test_iface(self):
+        obj = FileStoreNameChooser(None)
+        verifyClass(IFileStoreNameChooser, FileStoreNameChooser)
+        verifyObject(IFileStoreNameChooser, obj)
+        return
+
+class ExtFileStoreTests(unittest.TestCase):
+    # Test external file store (non-functional mode)
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.root = None
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        if self.root is not None:
+            shutil.rmtree(self.root)
+        return
+
+    def test_iface(self):
+        obj = ExtFileStore(None)
+        verifyClass(IExtFileStore, ExtFileStore)
+        verifyObject(IExtFileStore, obj)
+        return
+
+    def test_root_setup_wo_site(self):
+        # if no site is available we can use a temporary root
+        fs = ExtFileStore()
+        self.root = fs.root
+        self.assertTrue(isinstance(self.root, basestring))
+        self.assertTrue(os.path.exists(self.root))
+        return
+
+    def test_create_instance(self):
+        storage1 = ExtFileStore()
+        storage2 = ExtFileStore(root=self.workdir)
+        self.root = storage1.root
+        self.assertTrue(storage1.root is not None)
+        self.assertTrue(storage1.root != storage2.root)
+        self.assertEqual(storage2.root, self.workdir)
+        return
+
+    def test_create_file(self):
+        # We can store files
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample', dummy_file)
+        self.assertTrue('mysample' in os.listdir(storage.root))
+        self.assertEqual('mysample', image_file.data)
+        return
+
+    def test_get_file(self):
+        # We can get files after having them stored
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample', dummy_file)
+        result = storage.getFile(image_file.data)
+        self.assertEqual(result.read(), 'sample file')
+        return
+
+    def test_extract_marker(self):
+        # file stores support extracting markers from filenames
+        storage = ExtFileStore(root=self.workdir)
+        result1 = storage.extractMarker(None)
+        result2 = storage.extractMarker('')
+        result3 = storage.extractMarker('no-marker')
+        result4 = storage.extractMarker('no-marker.txt')
+        result5 = storage.extractMarker('__MARKER__foo.jpg')
+        result6 = storage.extractMarker('__MaRkEr__foo.jpg')
+        result7 = storage.extractMarker('__THE_MARKER__foo.jpg')
+        result8 = storage.extractMarker('__A_MARK__my__foo.jpg')
+
+        self.assertEqual(result1, ('', '', '', ''))
+        self.assertEqual(result2, ('', '', '', ''))
+        self.assertEqual(result3, ('', 'no-marker', 'no-marker', ''))
+        self.assertEqual(result4, ('', 'no-marker.txt', 'no-marker', '.txt'))
+        self.assertEqual(result5, ('marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result6, ('marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result7, ('the_marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result8, ('a_mark', 'my__foo.jpg', 'my__foo', '.jpg'))
+        return
+
+class DefaultFileStoreHandlerTests(unittest.TestCase):
+
+    def test_iface(self):
+        obj = DefaultFileStoreHandler()
+        verifyClass(IFileStoreHandler, DefaultFileStoreHandler)
+        verifyObject(IFileStoreHandler, obj)
+        return
+
+class CustomizedFileHandler(object):
+    def pathFromFileID(self, store, root, file_id):
+        """Turn filename into path to store.
+        """
+        return os.path.join(root, file_id[12:])
+
+    def createFile(self, store, root, file_id, filename, f):
+        path = self.pathFromFileID(store, root, file_id)
+        return f, path, HurryFile(filename, file_id)
+
+class FunctionalExtFileStoreTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+
+    def setUp(self):
+        super(FunctionalExtFileStoreTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.samplefile = os.path.join(self.workdir, 'sample')
+        self.otherfile = os.path.join(self.workdir, 'other')
+        open(self.samplefile, 'wb').write('Hi there!')
+        open(self.otherfile, 'wb').write('Hi from other!')
+        self.storage = ImageStorage()
+        self.fd = open(self.samplefile, 'r')
+        self.fd2 = open(self.otherfile, 'r')
+        self.getRootFolder()['app'] = University()
+        self.app = self.getRootFolder()['app']
+        self.app['datacenter'].setStoragePath(self.workdir)
+        # register a custom filename mangler
+        provideUtility(
+            CustomizedFileHandler(), IFileStoreHandler, name=u'mymarker')
+        return
+
+
+    def tearDown(self):
+        super(FunctionalExtFileStoreTests, self).tearDown()
+        self.fd.close()
+        self.fd2.close()
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_root_setup_w_site(self):
+        # if a site is available we use it to determine the root dir
+        fs = ExtFileStore()
+        setSite(self.app)
+        self.root = fs.root
+        expected_root = os.path.join(
+            self.app['datacenter'].storage, 'media')
+        self.assertTrue(isinstance(self.root, basestring))
+        self.assertEqual(self.root, expected_root)
+        return
+
+    def test_get_utility(self):
+        # we can get an ExtFileStore by global utility lookup
+        fs1 = getUtility(IExtFileStore)
+        fs2 = getUtility(IExtFileStore)
+        self.assertTrue(isinstance(fs1, ExtFileStore))
+        self.assertTrue(fs1 is fs2)
+        return
+
+    def test_default_handler_create_file(self):
+        # we can use the default handler to store files
+        fs = ExtFileStore()
+        result = fs.createFile('sample.txt', StringIO('sample text'))
+        self.assertEqual(result.data, 'sample.txt')
+        self.assertTrue('sample.txt' in os.listdir(fs.root))
+        return
+
+    def test_default_handler_get_file(self):
+        # we can get files stored by the default handler
+        fs = ExtFileStore()
+        fs.createFile('sample.txt', StringIO('sample text'))
+        result1 = fs.getFile('sample.txt')
+        result2 = fs.getFile('not-existent')
+        self.assertEqual(result1.read(), 'sample text')
+        self.assertTrue(result2 is None)
+        return
+
+    def test_customized_handler_create_file(self):
+        # we can use registered filename handlers
+        fs = ExtFileStore()
+        result = fs.createFile(
+            '__MYMARKER__sample.txt', StringIO('sample text'))
+        self.assertEqual(result.data, '__MYMARKER__sample.txt')
+        self.assertTrue('sample.txt' in os.listdir(fs.root))
+        return
+
+    def test_customized_handler_get_file(self):
+        # we consider registered filename handlers when asking for
+        # stored files.
+        fs = ExtFileStore()
+        fs.createFile('__MYMARKER__sample.txt', StringIO('sample text'))
+        result1 = fs.getFile('__MYMARKER__sample.txt')
+        result2 = fs.getFile('__MYMARKER__not-existent')
+        result3 = fs.getFile('not-existent')
+        self.assertEqual(result1.read(), 'sample text')
+        self.assertTrue(result2 is None)
+        self.assertTrue(result3 is None)
+        return
+
+    def test_get_default_handler(self):
+        # we can get a default handler
+        result = queryUtility(IFileStoreHandler)
+        self.assertTrue(
+            isinstance(result, DefaultFileStoreHandler))
+        return
+
+    def test_get_default_file_retrieval(self):
+        # we get a file store when requesting a file retrieval
+        result = queryUtility(IFileRetrieval)
+        self.assertTrue(
+            isinstance(result, DefaultStorage))
