Index: main/waeup.sirp/trunk/src/waeup/sirp/app.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/app.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/app.py	(revision 7063)
@@ -1,4 +1,3 @@
 import grok
-from hurry.file.interfaces import IFileRetrieval
 from zope.authentication.interfaces import IAuthentication
 from zope.component import getUtilitiesFor
@@ -9,5 +8,4 @@
 from waeup.sirp.students.container import StudentsContainer
 from waeup.sirp.hostels.container import HostelsContainer
-from waeup.sirp.imagestorage import ImageStorageFileRetrieval, ImageStorage
 from waeup.sirp.interfaces import (
     IUniversity, IWAeUPSIRPPluggable, IObjectUpgradeEvent, )
@@ -28,7 +26,4 @@
         setup = setup_authentication,)
 
-    grok.local_utility(
-        ImageStorageFileRetrieval, provides = IFileRetrieval)
-
     def __init__(self, *args, **kw):
         super(University, self).__init__(*args, **kw)
@@ -44,5 +39,4 @@
         self['users'] = UserContainer()
         self['datacenter'] = DataCenter()
-        self['images'] = ImageStorage()
         self['students'] = StudentsContainer()
         self['configuration'] = ConfigurationContainer()
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py	(revision 7063)
@@ -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/trunk/src/waeup/sirp/applicants/applicant.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/applicant.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/applicant.py	(revision 7063)
@@ -20,4 +20,5 @@
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##
+import os
 import grok
 from grok import index
@@ -25,10 +26,20 @@
 from zope.interface import implementedBy
 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
-from waeup.sirp.interfaces import IObjectHistory
 from waeup.sirp.app import University
+from waeup.sirp.image import WAeUPImageFile
+from waeup.sirp.imagestorage import DefaultFileStoreHandler
+from waeup.sirp.interfaces import (
+    IObjectHistory, IFileStoreHandler, IFileStoreNameChooser)
+from waeup.sirp.utils.helpers import attrs_to_fields
 from waeup.sirp.applicants.interfaces import (
-    IResultEntry, IApplicant, IApplicantEdit, default_passport_image,
+    IResultEntry, IApplicant, IApplicantEdit,
     )
-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):
@@ -49,7 +60,4 @@
         return
 
-    #def getApplicantsRootLogger(self):
-    #    return grok.getSite()['applicants'].logger
-
     def loggerInfo(self, ob_class, comment=None):
         target = self.__name__
@@ -81,31 +89,4 @@
     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 ApplicantFactory(grok.GlobalUtility):
     """A factory for applicants.
@@ -121,2 +102,100 @@
     def getInterfaces(self):
         return implementedBy(Applicant)
+
+
+#: The file id marker for applicant passport images
+APPLICANT_IMAGE_STORE_NAME = 'img-applicant'
+
+class ApplicantImageNameChooser(grok.Adapter):
+    """A file id chooser for :class:`Applicant` objects.
+
+    `context` is an :class:`Applicant` instance.
+
+    The :class:`ApplicantImageNameChooser` can build/check file ids
+    for :class:`Applicant` objects suitable for use with
+    :class:`ExtFileStore` instances. The delivered file_id contains
+    the file id marker for :class:`Applicant` object and the
+    registration number or access code of the context applicant. Also
+    the name of the connected applicant container will be part of the
+    generated file id.
+
+    This chooser is registered as an adapter providing
+    :class:`waeup.sirp.interfaces.IFileStoreNameChooser`.
+
+    File store name choosers like this one are only convenience
+    components to ease the task of creating file ids for applicant
+    objects. You are nevertheless encouraged to use them instead of
+    manually setting up filenames for applicants.
+
+    .. seealso:: :mod:`waeup.sirp.imagestorage`
+
+    """
+    grok.context(IApplicant)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None):
+        """Check whether the given name is a valid file id for the context.
+
+        Returns ``True`` only if `name` equals the result of
+        :meth:`chooseName`.
+        """
+        return name == self.chooseName()
+
+    def chooseName(self, name=None):
+        """Get a valid file id for applicant context.
+
+        *Example:*
+
+        For an applicant with registration no. ``'My_reg_no_1234'``
+        and stored in an applicants container called
+        ``'mycontainer'``, this chooser would create:
+
+          ``'__img-applicant__mycontainer/My_reg_no_1234.jpg'``
+
+        meaning that the passport image of this applicant would be
+        stored in the site-wide file storage in path:
+
+          ``mycontainer/My_reg_no_1234.jpg``
+
+        If the context applicant has no parent, ``'_default'`` is used
+        as parent name.
+        """
+        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/trunk/src/waeup/sirp/applicants/authentication.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/authentication.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/authentication.py	(revision 7063)
@@ -232,7 +232,7 @@
         if ac.state == 'used' and appl_ac != ac.representation:
             return None
-        if appl_ac is not None and appl_ac != ac.representation:
-            # This should never happen. It means a catalog error.
-            return None
+        # If the following fails we have a catalog error. Bad enough
+        # to pull emergency break.
+        assert appl_ac is None or appl_ac == ac.representation
         return ApplicantPrincipalInfo(accesscode)
 
@@ -256,5 +256,5 @@
     systems (regular users, officers, etc.) were set up.
     """
-    grok.provides(IAuthPluginUtility)
+    grok.implements(IAuthPluginUtility)
     grok.name('applicants_auth_setup')
 
@@ -285,4 +285,12 @@
 
     def unregister(self, pau):
+        """Unregister applicant specific authentication components from PAU.
+        """
+        pau.credentialsPlugins = tuple(
+            [x for x in list(pau.credentialsPlugins)
+             if x != 'applicant_credentials'])
+        pau.authenticatorPlugins = tuple(
+            [x for x in list(pau.authenticatorPlugins)
+             if x != 'applicants'])
         return pau
 
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py	(revision 7063)
@@ -22,4 +22,5 @@
 """UI components for basic applicants and related components.
 """
+import os
 import sys
 import grok
@@ -53,9 +54,6 @@
     ManageActionButton, PrimaryNavTab, LeftSidebarLink
     )
-from waeup.sirp.image.browser.widget import (
-    ThumbnailWidget, EncodingImageFileWidget,
-    )
-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
@@ -68,5 +66,6 @@
 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
@@ -425,5 +424,6 @@
         # If application has ended and applicant record doesn't exist,
         # logout without marking AC as used
-        if not pin in self.context.keys() and self.context.enddate < date.today():
+        if not pin in self.context.keys() and (
+            self.context.enddate < date.today()):
             self.flash('Application has ended.')
             auth = getUtility(IAuthentication)
@@ -433,7 +433,5 @@
 
         # Mark AC as used (this also fires a pin related transition)
-        if get_access_code(pin).state == USED:
-            pass
-        else:
+        if get_access_code(pin).state != USED:
             comment = u"AC invalidated"
             # Here we know that the ac is in state initialized so we do not
@@ -514,6 +512,5 @@
             applicant_object = get_applicant_data(access_code)
             return absoluteURL(applicant_object, self.request) + self.target
-        #else:
-        #    return ''
+        return ''
 
 class AccessCodeEditLink(AccessCodeViewLink):
@@ -534,6 +531,5 @@
                 return ''
             return absoluteURL(applicant_object, self.request) + self.target
-        #else:
-        #    return ''
+        return ''
 
 class AccessCodeSlipLink(AccessCodeViewLink):
@@ -550,10 +546,12 @@
     form_fields = grok.AutoFields(IApplicant).omit(
         'locked').omit('course_admitted')
-    #form_fields['fst_sit_results'].custom_widget = list_results_display_widget
-    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
@@ -582,5 +580,4 @@
 class PDFActionButton(ManageActionButton):
     grok.context(IApplicant)
-    #grok.view(DisplayApplicant)
     grok.require('waeup.manageApplications')
     icon = 'actionicon_pdf.png'
@@ -624,9 +621,4 @@
 
     def render(self):
-        # (0,0),(-1,-1) = whole table
-        # (0,0),(0,-1) = first column
-        # (-1,0),(-1,-1) = last column
-        # (0,0),(-1,0) = first row
-        # (0,-1),(-1,-1) = last row
         SLIP_STYLE = TableStyle(
             [('VALIGN',(0,0),(-1,-1),'TOP')]
@@ -652,5 +644,4 @@
         frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
         story.append(Paragraph(self.label, style["Heading2"]))
-        #story.append(HRFlowable())
         story.append(Spacer(1, 18))
         for msg in self.context.history.messages:
@@ -658,17 +649,21 @@
             story.append(Paragraph(f_msg, style["Normal"]))
         story.append(Spacer(1, 24))
+
+        data = []
+        # insert passport photograph
+        img = getUtility(IExtFileStore).getFileByContext(self.context)
+        if img is None:
+            img = open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg'), 'rb')
+        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
+        data.append(['', doc_img])
+
+        # render widget fields
         self.setUpWidgets()
-        data = []
         for widget in self.widgets:
             f_label = '<font size=12>%s</font>:' % widget.label.strip()
             f_label = Paragraph(f_label, style["Normal"])
-            if widget.name != 'form.passport':
-                f_text = '<font size=12>%s</font>' % widget()
-                f_text = Paragraph(f_text, style["Normal"])
-                data.append([f_label,f_text])
-            else:
-                filename = widget._data.file.name
-                im = Image(filename,width=4*cm, height=3*cm,kind='bound')
-                data.append([f_label,im])
+            f_text = '<font size=12>%s</font>' % widget()
+            f_text = Paragraph(f_text, style["Normal"])
+            data.append([f_label,f_text])
         f_label = '<font size=12>Admitted Course of Study:</font>'
         f_text = '<font size=12>%s</font>' % self.getCourseAdmitted()
@@ -698,4 +693,13 @@
     target = 'edit_full'
 
+def handle_img_upload(upload, context):
+    """Handle upload of applicant image.
+    """
+    store = getUtility(IExtFileStore)
+    file_id = IFileStoreNameChooser(context).chooseName()
+    store.createFile(file_id, upload)
+    upload.seek(0) # XXX: really neccessary?
+    return
+
 class EditApplicantFull(WAeUPEditFormPage):
     """A full edit view for applicant data.
@@ -705,5 +709,4 @@
     grok.require('waeup.manageApplications')
     form_fields = grok.AutoFields(IApplicant)
-    form_fields['passport'].custom_widget = EncodingImageFileWidget
     form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
     grok.template('form_edit')
@@ -718,19 +721,5 @@
         if upload:
             # We got a fresh upload
-            image = createWAeUPImageFile(upload.filename, upload)
-            # This would normally be totally illegal. We set context
-            # data without the complete form data being
-            # verified. Normally data is set in methods like `save`
-            # only, which is only called when all form data is correct
-            # (and if not, the context is not touched). With images
-            # and their uploads the problem then is, that the uploaded
-            # image won't be shown in a partially erranous form
-            # because the new uploaded image is still not part of the
-            # context obeject and the image widget cannot work around
-            # that (it has no access to the new data). For this reason
-            # we set the image here and not in 'save()' or other
-            # methods.
-            self.context.passport = image
-            upload.seek(0)
+            handle_img_upload(upload, self.context)
             self.passport_changed = True
         return
@@ -760,5 +749,6 @@
         changed_fields = self.applyData(self.context, **data)
         changed_fields = changed_fields.values()
-        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
+        fields_string = '+'.join(
+            ' + '.join(str(i) for i in b) for b in changed_fields)
         self.context._p_changed = True
         form = self.request.form
@@ -782,5 +772,4 @@
         'screening_score',
         )
-    form_fields['passport'].custom_widget = EncodingImageFileWidget
     form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
     grok.template('form_edit')
@@ -788,5 +777,4 @@
     title = u'Your Application Form'
 
-
     def emitLockMessage(self):
         self.flash('The requested form is locked (read-only).')
@@ -802,19 +790,5 @@
         if upload:
             # We got a fresh upload
-            image = createWAeUPImageFile(upload.filename, upload)
-            # This would normally be totally illegal. We set context
-            # data without the complete form data being
-            # verified. Normally data is set in methods like `save`
-            # only, which is only called when all form data is correct
-            # (and if not, the context is not touched). With images
-            # and their uploads the problem then is, that the uploaded
-            # image won't be shown in a partially erranous form
-            # because the new uploaded image is still not part of the
-            # context obeject and the image widget cannot work around
-            # that (it has no access to the new data). For this reason
-            # we set the image here and not in 'save()' or other
-            # methods.
-            self.context.passport = image
-            upload.seek(0)
+            handle_img_upload(upload, self.context)
             self.passport_changed = True
         super(EditApplicantStudent, self).update()
@@ -824,13 +798,8 @@
         if not self.request.form.get('confirm_passport', False):
             return 'Passport confirmation box not ticked.'
-        #if len(self.errors) > 0:
-        #    return 'Form has errors.'
         return False
 
     @grok.action('Save')
     def save(self, **data):
-        #if self.context.locked:
-        #    self.emitLockMessage()
-        #    return
         self.applyData(self.context, **data)
         self.context._p_changed = True
@@ -840,7 +809,4 @@
     @grok.action('Final Submit')
     def finalsubmit(self, **data):
-        #if self.context.locked:
-        #    self.emitLockMessage()
-        #    return
         self.applyData(self.context, **data)
         self.context._p_changed = True
@@ -868,2 +834,20 @@
     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.
+        image = getUtility(IExtFileStore).getFileByContext(self.context)
+        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/trunk/src/waeup/sirp/applicants/browser_templates/form_display.pt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser_templates/form_display.pt	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser_templates/form_display.pt	(revision 7063)
@@ -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/trunk/src/waeup/sirp/applicants/browser_templates/form_edit.pt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser_templates/form_edit.pt	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser_templates/form_edit.pt	(revision 7063)
@@ -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/trunk/src/waeup/sirp/applicants/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py	(revision 7063)
@@ -29,11 +29,9 @@
 
 from zope import schema
-from zope.interface import Interface, Attribute, provider
+from zope.interface import Interface, Attribute
 from zope.component import getUtilitiesFor
 from zope.pluggableauth.interfaces import IPrincipalInfo
 from zope.security.interfaces import IGroupClosureAwarePrincipal as IPrincipal
 from zc.sourcefactory.basic import BasicSourceFactory
-from waeup.sirp.image import createWAeUPImageFile
-from waeup.sirp.image.schema import ImageFile
 from waeup.sirp.interfaces import IWAeUPObject, year_range
 from waeup.sirp.university.vocabularies import application_categories
@@ -66,24 +64,4 @@
     return True
 
-@provider(schema.interfaces.IContextAwareDefaultFactory)
-def default_passport_image(context):
-    """A default value factory for ImageFile fields.
-
-    Returns some default image as WAeUPImageFile. We cannot set the
-    default directly in ImageFile fields, as, if we want to set
-    max_size or min_size as well, some utility lookups are needed
-    which are not possible during startup.
-
-    Developers which use IContextAwareDefaultFactories like this one
-    always should make sure that the delivered default meets all
-    constraints of the field that makes use of this default value
-    provider.
-    """
-    imagefile = createWAeUPImageFile(
-        'placeholder_m.jpg',
-        open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg'), 'r')
-        )
-    return imagefile
-
 class ApplicantContainerProviderSource(BasicSourceFactory):
     """A source offering all available applicants container types.
@@ -370,12 +348,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/trunk/src/waeup/sirp/applicants/root.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/root.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/root.py	(revision 7063)
@@ -47,10 +47,5 @@
         """
         user = get_current_principal()
-        if user is None:
-            user = 'system'
-        elif user.title == 'Applicant':
-            user = 'applicant'
-        else:
-            user = user.id
+        user = getattr(user, 'id', 'system')
         self.logger.info('%s - %s - %s - %s' % (
                 user, target, ob_class, comment))
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicant.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 7063)
@@ -22,26 +22,19 @@
 """Tests for applicants and related.
 """
+import grok
 import unittest
-from StringIO import StringIO
-from hurry.file.interfaces import IFileRetrieval
-from zope.component import (
-    provideAdapter, adapts, getGlobalSiteManager, provideUtility)
-from zope.component.hooks import setSite
+from zope.component import adapts, queryUtility
 from zope.component.interfaces import IFactory
 from zope.interface import verify, implements
 from zope.location.interfaces import ILocation
-from zope.publisher.base import TestRequest
-from zope.publisher.interfaces import NotFound
-from waeup.sirp.app import University
-from waeup.sirp.image import WAeUPImageFile, createWAeUPImageFile
 from waeup.sirp.image.interfaces import IWAeUPImageFile
+from waeup.sirp.imagestorage import 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)
+from waeup.sirp.applicants.interfaces import IResultEntry, IApplicant
+from waeup.sirp.testing import FunctionalTestCase, FunctionalLayer
 
 class FakeImageLocation(object):
@@ -51,4 +44,109 @@
         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
+
+    def test_name_chooser_check_name(self):
+        # we can check file ids for applicants
+        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)
+        result1 = chooser.checkName('foo')
+        result2 = chooser.checkName('__img-applicant__folder/MY_REG_NO.jpg')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, True)
+        return
+
 class ResultEntryTest(unittest.TestCase):
 
@@ -87,27 +185,4 @@
         return
 
-    def test_passport_no_site(self):
-        # make sure we get a real image stored in passport attr
-        image = self.applicant.passport.file.read()
-        self.assertEqual(len(image), 2059)
-        return
-
-    def test_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
-        # photograph should be put inside the images folder of the
-        # site, even if it is only a placeholder.
-        self.getRootFolder()['app'] = University()
-        app = self.getRootFolder()['app']
-        setSite(app)
-        applicant = Applicant()
-        image = self.applicant.passport.file.read()
-        self.assertEqual(len(image), 2059)
-        # The image contains a file_id instead of real image-data
-        self.assertEqual(self.applicant.passport.data,
-                         u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')
-        assert u'b48a1d39bbcb32e955d9ff2dea4ed0e6' in app['images'].keys()
-        return
-
 class ApplicantFactoryTest(FunctionalTestCase):
 
@@ -116,7 +191,4 @@
     def setUp(self):
         super(ApplicantFactoryTest, self).setUp()
-        # Install a IFileRetrieval utility that returns WAeUPImageFiles.
-        storage = ImageStorageFileRetrieval()
-        provideUtility(storage, IFileRetrieval)
         self.factory = ApplicantFactory()
         return
@@ -137,65 +209,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/trunk/src/waeup/sirp/applicants/tests/test_authentication.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_authentication.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_authentication.py	(revision 7063)
@@ -24,5 +24,5 @@
 import unittest
 from zope.authentication.interfaces import IAuthentication
-from zope.component import provideAdapter, getUtility
+from zope.component import provideAdapter, getUtility, queryUtility
 from zope.component.hooks import setSite
 from zope.interface import verify
@@ -35,9 +35,10 @@
 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
 from waeup.sirp.app import University
+from waeup.sirp.interfaces import IAuthPluginUtility
 from waeup.sirp.applicants import  ApplicantsContainer, Applicant
 from waeup.sirp.applicants.authentication import (
     ApplicantsAuthenticatorPlugin, WAeUPApplicantCredentialsPlugin,
     ApplicantCredentials, AuthenticatedApplicantPrincipalFactory,
-    ApplicantPrincipalInfo, ApplicantPrincipal,)
+    ApplicantPrincipalInfo, ApplicantPrincipal, ApplicantsAuthUtility)
 
 
@@ -367,14 +368,45 @@
         return
 
-def test_suite():
-    suite = unittest.TestSuite()
-    for testcase in [
-        AuthenticatorPluginTest, CredentialsPluginTest,
-        ApplicantCredentialsTest, PrincipalFactoryTest,
-        PAUSetupTest,
-        ]:
-        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
-                testcase
-                )
-        )
-    return suite
+class FakePAU(object):
+    credentialsPlugins = ()
+    authenticatorPlugins = ()
+
+class ApplicantsAuthUtilityTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # make sure we fullfill the interface promises
+        obj = ApplicantsAuthUtility()
+        verify.verifyClass(IAuthPluginUtility, ApplicantsAuthUtility)
+        verify.verifyObject(IAuthPluginUtility, obj)
+        return
+
+    def test_utility_available(self):
+        # we can get an applicant auth utility by lookup
+        util = queryUtility(IAuthPluginUtility,
+                            name='applicants_auth_setup')
+        self.assertTrue(util is not None)
+        return
+
+    def test_register(self):
+        # make sure we can register additional components
+        pau = FakePAU()
+        result = ApplicantsAuthUtility().register(pau)
+        self.assertEqual(
+            result.credentialsPlugins, ('applicant_credentials',))
+        self.assertEqual(
+            result.authenticatorPlugins, ('applicants',))
+        return
+
+    def test_unregister(self):
+        # make sure we can unregister applicant auth components
+        pau = FakePAU()
+        util = ApplicantsAuthUtility()
+        pau = util.register(pau)
+        result = util.unregister(pau)
+        self.assertEqual(
+            result.credentialsPlugins, ())
+        self.assertEqual(
+            result.authenticatorPlugins, ())
+        return
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7063)
@@ -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')
@@ -769,16 +748,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())
+        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 +770,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/trunk/src/waeup/sirp/applicants/tests/test_interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_interfaces.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_interfaces.py	(revision 7063)
@@ -25,12 +25,7 @@
 import unittest
 from zc.sourcefactory.browser.source import FactoredTerms
-from zope.browser.interfaces import ITerms
-from zope.component import getMultiAdapter
-from zope.interface.verify import verifyClass, verifyObject
 from zope.publisher.browser import TestRequest
-from waeup.sirp.applicants import interfaces
-from waeup.sirp.applicants.vocabularies import (
-    APPLICATION_TYPES, application_types_vocab, GenderSource,
-    )
+from waeup.sirp.applicants.vocabularies import application_types_vocab
+from waeup.sirp.students.vocabularies import GenderSource
 
 class ApplicationCategoriesTestCase(unittest.TestCase):
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_root.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_root.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_root.py	(revision 7063)
@@ -39,6 +39,5 @@
     )
 from waeup.sirp.testing import (
-    FunctionalLayer, FunctionalTestCase, get_all_loggers, remove_new_loggers,
-    remove_logger)
+    FunctionalLayer, FunctionalTestCase, remove_logger)
 
 
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/vocabularies.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/vocabularies.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/vocabularies.py	(revision 7063)
@@ -1,13 +1,8 @@
 """Vocabularies and sources for the application section.
 """
-from datetime import datetime
 from zope.component import getUtility
 from zope.catalog.interfaces import ICatalog
-from zc.sourcefactory.basic import BasicSourceFactory
-from zc.sourcefactory.contextual import BasicContextualSourceFactory
 from waeup.sirp.interfaces import SimpleWAeUPVocabulary
-from waeup.sirp.students.lgas import LGAS
-from waeup.sirp.students.vocabularies import (
-    CertificateSource, GenderSource)
+from waeup.sirp.students.vocabularies import CertificateSource
 
 #: Types of applications we support.
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/workflow.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/workflow.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/workflow.py	(revision 7063)
@@ -2,5 +2,4 @@
 """
 import grok
-from datetime import datetime
 from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
 from hurry.workflow.interfaces import IWorkflowState, IWorkflowTransitionEvent
Index: main/waeup.sirp/trunk/src/waeup/sirp/browser/browser.txt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/browser/browser.txt	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/browser/browser.txt	(revision 7063)
@@ -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/trunk/src/waeup/sirp/browser/resources.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/browser/resources.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/browser/resources.py	(revision 7063)
@@ -38,10 +38,10 @@
 #:
 #: * In the `update()` method of the responsible view/page/form
-#:   require the JavaScript code to be rendered into the page:
+#:   require the JavaScript code to be rendered into the page::
 #:
 #:     from waeup.sirp.browser.resources import datepicker
-#:     ...
-#:     class MyPage(...):
-#:       ...
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
 #:       def update(self):
 #:         datepicker.need()
@@ -51,5 +51,7 @@
 #:
 #: * In your HTML code add some ``<input>`` or ``<div>`` tag with
-#:   ``class`` set to ``datepicker``, like this::
+#:   ``class`` set to ``datepicker``, like this:
+#:
+#:   .. code-block:: html
 #:
 #:     <input type="text" name="entrydate" class="datepicker" />
@@ -85,5 +87,5 @@
 #: ``"datepicker-de-year"``
 #:    same but also adds a select field for year
-#: 
+#:
 #: ``"datepicker-us"``
 #:    renders date in format ``MM/DD/YYYY`` (U.S. format)
@@ -97,11 +99,12 @@
 #: If you want generate alert or confirm messages then you have to perform
 #: two steps:
-#: * In the `update()` method of the responsible view/page/form
-#:   require the JavaScript code to be rendered into the page:
+#:
+#: * In the `update()` method of the responsible view/page/form
+#:   require the JavaScript code to be rendered into the page::
 #:
 #:     from waeup.sirp.browser.resources import datepicker
-#:     ...
-#:     class MyPage(...):
-#:       ...
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
 #:       def update(self):
 #:         warning.need()
@@ -109,21 +112,23 @@
 #:   This way all required JavaScripts will be rendered correctly
 #:   into the HTML page generated.
-#: 
+#:
 #:   In your HTML code add some <input> or <a> tag with onClick event:
 #:
-#:   <input type="submit" name="xyz" value="abc"/>
-#:          onclick="return confirmPost('Are you sure?')">
+#:   .. code-block:: html
+#:
+#:        <input type="submit" name="xyz" value="abc"/>
+#:               onclick="return confirmPost('Are you sure?')">
 warning = ResourceInclusion(waeup_sirp, 'warning.js')
 
 #: If you have many select boxes in a from which have to be selected at the same
-# time you have to perform
-#: two steps:
-#: * In the `update()` method of the responsible view/page/form
-#:   require the JavaScript code to be rendered into the page:
+#: time you have to perform two steps:
+#:
+#: * In the `update()` method of the responsible view/page/form
+#:   require the JavaScript code to be rendered into the page::
 #:
 #:     from waeup.sirp.browser.resources import toggleall
-#:     ...
-#:     class MyPage(...):
-#:       ...
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
 #:       def update(self):
 #:         toggleall.need()
@@ -134,6 +139,7 @@
 #:   In your HTML code add some <input> tag with onClick event:
 #:
-#:   <input type="checkbox" onClick="toggle(this, 'entries')" />
-
+#:   .. code-block:: html
+#:
+#:    <input type="checkbox" onClick="toggle(this, 'entries')" />
 toggleall = ResourceInclusion(waeup_sirp, 'toggleall.js')
 
@@ -141,15 +147,14 @@
 #: ``tabs``.
 #:
-#:
 #: If you want jQuery tabs for some <div>s in your page then you
 #: have to perform two steps:
 #:
 #: * In the `update()` method of the responsible view/page/form
-#:   require the JavaScript code to be rendered into the page:
+#:   require the JavaScript code to be rendered into the page::
 #:
 #:     from waeup.sirp.browser.resources import tabs
-#:     ...
-#:     class MyPage(...):
-#:       ...
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
 #:       def update(self):
 #:         tabs.need()
@@ -159,6 +164,8 @@
 #:
 #: * In your HTML code add some ``<div>`` tag with ``class`` set to
-#    ``tabs``, like this::
-#:    
+#:   ``tabs``, like this:
+#:
+#:    .. code-block:: html
+#:
 #:      <div id="tabs">
 #:        <ul>
@@ -195,5 +202,5 @@
 #: A stylesheet for datatables
 datatables_css = ResourceInclusion(waeup_sirp, 'datatables.css')
-                                  
+
 #: A resource that turns HTML tables into sortable, searchable and :
 #: multi-page widgets using jQuery and the dataTables plugin available
@@ -204,10 +211,10 @@
 #:
 #: * In the `update()` method of the responsible view/page/form
-#:   require the JavaScript code to be rendered into the page:
+#:   require the JavaScript code to be rendered into the page::
 #:
 #:     from waeup.sirp.browser.resources import datatable
-#:     ...
-#:     class MyPage(...):
-#:       ...
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
 #:       def update(self):
 #:         datatable.need()
@@ -217,5 +224,7 @@
 #:
 #: * Assign a ``display dataTable`` class to the HTML table you want to tune:
-#:    
+#:
+#:   .. code-block:: html
+#:
 #:      <table class="display dataTable">
 #:        <thead>
@@ -246,9 +255,9 @@
 #: Register basic YUI based CSS as a resource.
 reset_fonts_grids = ResourceInclusion(
-    waeup_sirp, 'yuirfg.css')    
-    
+    waeup_sirp, 'yuirfg.css')
+
 yuiapp_css = ResourceInclusion(
     waeup_sirp, 'yuiapp.css',
-    depends=[reset_fonts_grids])     
+    depends=[reset_fonts_grids])
 
 #: Register basic WAeUP base CSS (which is based on ``yuiapp.css`` as a
Index: main/waeup.sirp/trunk/src/waeup/sirp/browser/theming.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/browser/theming.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/browser/theming.py	(revision 7063)
@@ -88,10 +88,10 @@
     A named global utility implementing
 
-      :class:`zope.schema.interfaces.IVocabularyFactory
+      :class:`zope.schema.interfaces.IVocabularyFactory`
 
     and registered under the name
 
       'waeup.sirp.browser.theming.ThemesVocabulary'
-    
+
     Interface fields that wish to provide a list of available themes
     can require a 'named vocabulary', i.e. set:
@@ -107,5 +107,5 @@
     grok.implements(IVocabularyFactory)
     grok.name('waeup.sirp.browser.theming.ThemesVocabulary')
-    
+
     def __call__(self, context):
         """Deliver a vocabulary of available themes.
Index: main/waeup.sirp/trunk/src/waeup/sirp/imagestorage.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/imagestorage.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/imagestorage.py	(revision 7063)
@@ -21,197 +21,439 @@
 ##
 """A storage for image files.
+
+A few words about storing files with ``waeup.sirp``. The need for this
+feature arised initially from the need to store passport files for
+applicants and students. These files are dynamic (can be changed
+anytime), mean a lot of traffic and cost a lot of memory/disk space.
+
+**Design Basics**
+
+While one *can* store images and similar 'large binary objects' aka
+blobs in the ZODB, this approach quickly becomes cumbersome and
+difficult to understand. The worst approach here would be to store
+images as regular byte-stream objects. ZODB supports this but
+obviously access is slow (data must be looked up in the one
+``Data.fs`` file, each file has to be sent to the ZEO server and back,
+etc.).
+
+A bit less worse is the approach to store images in the ZODB but as
+Blobs. ZODB supports storing blobs in separate files in order to
+accelerate lookup/retrieval of these files. The files, however, have
+to be sent to the ZEO server (and back on lookups) which means a
+bottleneck and will easily result in an increased number of
+``ConflictErrors`` even on simple reads.
+
+The advantage of both ZODB-geared approaches is, of course, complete
+database consistency. ZODB will guarantee that your files are
+available under some object name and can be handled as any other
+Python object.
+
+Another approach is to leave the ZODB behind and to store images and
+other files in filesystem directly. This is faster (no ZEO contacts,
+etc.), reduces probability of `ConflictErrors`, keeps the ZODB
+smaller, and enables direct access (over filesystem) to the
+files. Furthermore steps might be better understandable for
+third-party developers. We opted for this last option.
+
+**External File Store**
+
+Our implementation for storing-files-API is defined in
+:class:`ExtFileStore`. An instance of this file storage (which is also
+able to store non-image files) is available at runtime as a global
+utility implementing :class:`waeup.sirp.interfaces.IExtFileStore`.
+
+The main task of this central component is to maintain a filesystem
+root path for all files to be stored. It also provides methods to
+store/get files under certain file ids which identify certain files
+locally.
+
+So, to store a file away, you can do something like this:
+
+  >>> from StringIO import StringIO
+  >>> from zope.component import getUtility
+  >>> from waeup.sirp.interfaces import IExtFileStore
+  >>> store = getUtility(IExtFileStore)
+  >>> store.createFile('myfile.txt', StringIO('some file content'))
+
+All you need is a filename and the file-like object containing the
+real file data.
+
+This will store the file somewhere (you shouldn't make too much
+assumptions about the real filesystem path here).
+
+Later, we can get the file back like this:
+
+  >>> store.getFile('myfile.txt')
+  <open file ...>
+
+What we get back is a file or file-like object already opened for
+reading:
+
+  >>> store.getFile('myfile.txt').read()
+  'some file content'
+
+**Handlers: Special Places for Special Files**
+
+The file store supports special handling for certain files. For
+example we want applicant images to be stored in a different directory
+than student images, etc. Because the file store cannot know all
+details about these special tratment of certain files, it looks up
+helpers (handlers) to provide the information it needs for really
+storing the files at the correct location.
+
+That a file stored in filestore needs special handling can be
+indicated by special filenames. These filenames start with a marker like
+this::
+
+  __<MARKER-STRING>__real-filename.jpg
+
+Please note the double underscores before and after the marker
+string. They indicate that all in between is a marker.
+
+If you store a file in file store with such a filename (we call this a
+`file_id` to distuingish it from real world filenames), the file store
+will look up a handler for ``<MARKER-STRING>`` and pass it the file to
+store. The handler then will return the internal path to store the
+file and possibly do additional things as well like validating the
+file or similar.
+
+Examples for such a file store handler can be found in the
+:mod:`waeup.sirp.applicants.applicant` module. Please see also the
+:class:`DefaultFileStoreHandler` class below for more details.
+
+The file store looks up handlers by utility lookups: it looks for a
+named utiliy providing
+:class:`waeup.sirp.interfaces.IFileStoreHandler` and named like the
+marker string (without leading/trailing underscores) in lower
+case. For example if the file id would be
+
+  ``__IMG_USER__manfred.jpg``
+
+then the looked up utility should be registered under name
+
+  ``img_user``
+
+and provide :class:`waeup.sirp.interfaces.IFileStoreHandler`. If no
+such utility can be found, a default handler is used instead
+(see :class:`DefaultFileStoreHandler`).
+
+**Context Adapters: Knowing Your Family**
+
+Often the internal filename or file id of a file depends on a
+context. For example when we store passport photographs of applicants,
+then each image belongs to a certain applicant instance. It is not
+difficult to maintain such a connection manually: Say every applicant
+had an id, then we could put this id into the filename as well and
+would build the filename to store/get the connected file by using that
+filename. You then would create filenames of a format like this::
+
+  __<MARKER-STRING>__applicant0001.jpg
+
+where ``applicant0001`` would tell exactly which applicant you can see
+on the photograph. You notice that the internal file id might have
+nothing to do with once uploaded filenames. The id above could have
+been uploaded with filename ``manfred.jpg`` but with the new file id
+we are able to find the file again later.
+
+Unfortunately it might soon get boring or cumbersome to retype this
+building of filenames for a certain type of context, especially if
+your filenames take more of the context into account than only a
+simple id.
+
+Therefore you can define filename building for a context as an adapter
+that then could be looked up by other components simply by doing
+something like:
+
+  >>> from waeup.sirp.interfaces import IFileStoreNameChooser
+  >>> file_id = IFileStoreNameChooser(my_context_obj)
+
+If you later want to change the way file ids are created from a
+certain context, you only have to change the adapter implementation
+accordingly.
+
+Note, that this is only a convenience component. You don't have to
+define context adapters but it makes things easier for others if you
+do, as you don't have to remember the exact file id creation method
+all the time and can change things quick and in only one location if
+you need to do so.
+
+Please see the :class:`FileStoreNameChooser` default implementation
+below for details.
+
 """
 import grok
-import hashlib
 import os
-import transaction
-import warnings
-from StringIO import StringIO
-from ZODB.blob import Blob
-from persistent import Persistent
+import tempfile
+from hurry.file import HurryFile
 from hurry.file.interfaces import IFileRetrieval
-from waeup.sirp.image import WAeUPImageFile
-from waeup.sirp.utils.helpers import cmp_files
-
-def md5digest(fd):
-    """Get an MD5 hexdigest for the file stored in `fd`.
-
-    `fd`
-      a file object open for reading.
-
+from zope.component import queryUtility
+from zope.interface import Interface
+from waeup.sirp.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
+
+class FileStoreNameChooser(grok.Adapter):
+    """Default file store name chooser.
+
+    File store name choosers pick a file id, a string, for a certain
+    context object. They are normally registered as adapters for a
+    certain content type and know how to build the file id for this
+    special type of context.
+
+    Provides the :class:`waeup.sirp.interfaces.IFileStoreNameChooser`
+    interface.
+
+    This default file name chosser accepts almost every name as long
+    as it is a string or unicode object.
     """
-    return hashlib.md5(fd.read()).hexdigest()
-
-class Basket(grok.Container):
-    """A basket holds a set of image files with same hash.
+    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.
+
+        For the default file store name chooser any name is valid.
+        """
+        if isinstance(name, basestring):
+            return True
+        return False
+
+    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.
+
+        For this default name chooser we return the given name if it
+        is valid or ``unknown_file`` else.
+        """
+        if self.checkName(name):
+            return name
+        return u'unknown_file'
+
+class ExtFileStore(object):
+    """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 :class:`waeup.sirp.interfaces.IExtFileStore`.
+
+    To add and retrieve files from the storage, use the appropriate
+    methods below.
     """
 
-    def _del(self):
-        """Remove temporary files associated with local blobs.
-
-        A basket holds files as Blob objects. Unfortunately, if a
-        basket was not committed (put into ZODB), those blobs linger
-        around as real files in some temporary directory and won't be
-        removed.
-
-        This is a helper function to remove all those uncommitted
-        blobs that has to be called explicitly, for instance in tests.
-        """
-        key_list = list(self.keys())
-        for key in key_list:
-            item = self[key]
-            if getattr(item, '_p_oid', None):
-                # Don't mess around with blobs in ZODB
-                continue
-            fd = item.open('r')
-            name = getattr(fd, 'name', None)
-            fd.close()
-            if name is not None and os.path.exists(name):
-                os.unlink(name)
-            del self[key]
+    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 getInternalId(self, fd):
-        """Get the basket-internal id for the file stored in `fd`.
-
-        `fd` must be a file open for reading. If an (byte-wise) equal
-        file can be found in the basket, its internal id (basket id)
-        is returned, ``None`` otherwise.
-        """
-        fd.seek(0)
-        for key, val in self.items():
-            fd_stored = val.open('r')
-            file_len = os.stat(fd_stored.name)[6]
-            if file_len == 0:
-                # Nasty workaround. Blobs seem to suffer from being emptied
-                # accidentally.
-                site = grok.getSite()
-                if site is not None:
-                    site.logger.warn(
-                        'Empty Blob detected: %s' % fd_stored.name)
-                warnings.warn("EMPTY BLOB DETECTED: %s" % fd_stored.name)
-                fd_stored.close()
-                val.open('w').write(fd.read())
-                return key
-            fd_stored.seek(0)
-            if cmp_files(fd, fd_stored):
-                fd_stored.close()
-                return key
-            fd_stored.close()
-        return None
-
-    @property
-    def curr_id(self):
-        """The current basket id.
-
-        An integer number which is not yet in use. If there are
-        already `maxint` entries in the basket, a :exc:`ValueError` is
-        raised. The latter is _highly_ unlikely. It would mean to have
-        more than 2**32 hash collisions, i.e. so many files with the
-        same MD5 sum.
-        """
-        num = 1
-        while True:
-            if str(num) not in self.keys():
-                return str(num)
-            num += 1
-            if num <= 0:
-                name = getattr(self, '__name__', None)
-                raise ValueError('Basket full: %s' % name)
-
-    def storeFile(self, fd, filename):
-        """Store the file in `fd` into the basket.
-
-        The file will be stored in a Blob.
-        """
-        fd.seek(0)
-        internal_id = self.getInternalId(fd) # Moves file pointer!
-        if internal_id is None:
-            internal_id = self.curr_id
-            fd.seek(0)
-            self[internal_id] = Blob()
-            transaction.commit() # Urgently needed to make the Blob
-                                 # persistent. Took me ages to find
-                                 # out that solution, which makes some
-                                 # design flaw in ZODB Blobs likely.
-            self[internal_id].open('w').write(fd.read())
-            fd.seek(0)
-            self._p_changed = True
-        return internal_id
-
-    def retrieveFile(self, basket_id):
-        """Retrieve a file open for reading with basket id `basket_id`.
-
-        If there is no such id, ``None`` is returned. It is the
-        callers responsibility to close the open file.
-        """
-        if basket_id in self.keys():
-            return self[basket_id].open('r')
-        return None
-
-class ImageStorage(grok.Container):
-    """A container for image files.
+    def getFile(self, file_id):
+        """Get a file stored under file ID `file_id`.
+
+        Returns a file already opened for reading.
+
+        If the file cannot be found ``None`` is returned.
+
+        This methods takes into account registered handlers for any
+        marker put into the file_id.
+
+        .. seealso:: :class:`DefaultFileStoreHandler`
+        """
+        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 getFileByContext(self, context):
+        """Get a file for given context.
+
+        Returns a file already opened for reading.
+
+        If the file cannot be found ``None`` is returned.
+
+        This method takes into account registered handlers and file
+        name choosers for context types.
+
+        This is a convenience method that internally calls
+        :meth:`getFile`.
+
+        .. seealso:: :class:`FileStoreNameChooser`,
+                     :class:`DefaultFileStoreHandler`.
+        """
+        file_id = IFileStoreNameChooser(context).chooseName()
+        return self.getFile(file_id)
+
+    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):
+    """Default storage for files.
+
+    Registered globally as utility for
+    :class:`hurry.file.interfaces.IFileRetrieval`.
     """
-    def _del(self):
-        for basket in self.values():
-            try:
-                basket._del()
-            except:
-                pass
-
-    def storeFile(self, fd, filename):
-        fd.seek(0)
-        digest = md5digest(fd)
-        fd.seek(0)
-        if not digest in self.keys():
-            self[digest] = Basket()
-        basket_id = self[digest].storeFile(fd, filename)
-        full_id = "%s-%s" % (digest, basket_id)
-        return full_id
-
-    def retrieveFile(self, file_id):
-        if not '-' in file_id:
-            return None
-        full_id, basket_id = file_id.split('-', 1)
-        if not full_id in self.keys():
-            return None
-        return self[full_id].retrieveFile(basket_id)
-
-class ImageStorageFileRetrieval(Persistent):
-    grok.implements(IFileRetrieval)
-
-    def getImageStorage(self):
-        site = grok.getSite()
-        if site is None:
-            return None
-        return site.get('images', None)
-
-    def isImageStorageEnabled(self):
-        site = grok.getSite()
-        if site is None:
-            return False
-        if site.get('images', None) is None:
-            return False
-        return True
-
-    def getFile(self, data):
-        # ImageStorage is disabled, so give fall-back behaviour for
-        # testing without ImageStorage
-        if not self.isImageStorageEnabled():
-            return StringIO(data)
-        storage = self.getImageStorage()
-        if storage is None:
-            raise ValueError('Cannot find an image storage')
-        result = storage.retrieveFile(data)
-        if result is None:
-            return StringIO(data)
-        return storage.retrieveFile(data)
-
-    def createFile(self, filename, f):
-        if not self.isImageStorageEnabled():
-            return WAeUPImageFile(filename, f.read())
-        storage = self.getImageStorage()
-        if storage is None:
-            raise ValueError('Cannot find an image storage')
-        file_id = storage.storeFile(f, filename)
-        return WAeUPImageFile(filename, file_id)
+    grok.provides(IFileRetrieval)
+
+grok.global_utility(DefaultStorage, provides=IFileRetrieval)
+
+class DefaultFileStoreHandler(grok.GlobalUtility):
+    """A default handler for external file store.
+
+    This handler is the fallback called by external file stores when
+    there is no or an unknown marker in the file id.
+
+    Registered globally as utility for
+    :class:`waeup.sirp.interfaces.IFileStoreHandler`.
+    """
+    grok.implements(IFileStoreHandler)
+
+    def pathFromFileID(self, store, root, file_id):
+        """Return the root path of external file store appended by file id.
+        """
+        return os.path.join(root, file_id)
+
+    def createFile(self, store, root, filename, file_id, f):
+        """Infos about what to store exactly and where.
+
+        When a file should be handled by an external file storage, it
+        looks up any handlers (like this one), passes runtime infos
+        like the storage object, root path, filename, file_id, and the
+        raw file object itself.
+
+        The handler can then change the file, raise exceptions or
+        whatever and return the result.
+
+        This handler returns the input file as-is, a path returned by
+        :meth:`pathFromFileID` and an instance of
+        :class:`hurry.file.HurryFile` for further operations.
+
+        Please note: although a handler has enough infos to store the
+        file itself, it should leave that task to the calling file
+        store.
+        """
+        path = self.pathFromFileID(store, root, file_id)
+        return f, path, HurryFile(filename, file_id)
Index: main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py	(revision 7063)
@@ -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
@@ -591,2 +593,85 @@
         """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 or ``None`` if the
+        file cannot be found.
+        """
+
+    def getFileByContext(context):
+        """Get raw file data stored for the given context.
+
+        Returns a file descriptor open for reading or ``None`` if no
+        such file can be found.
+
+        This is a convenience method.
+        """
+
+    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/trunk/src/waeup/sirp/tests/test_app.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/tests/test_app.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/tests/test_app.py	(revision 7063)
@@ -6,5 +6,4 @@
 from zope.interface.verify import verifyClass, verifyObject
 from waeup.sirp.app import University
-from waeup.sirp.imagestorage import ImageStorageFileRetrieval
 from waeup.sirp.interfaces import IUniversity
 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
@@ -32,9 +31,4 @@
         return
 
-    def test_images(self):
-        # Make sure we have a image container in a university
-        assert 'images' in self.app.keys()
-        return
-
     def test_IFileRetrieval_utility(self):
         # Make sure we can get a local IFileRetrieval utility
@@ -43,5 +37,4 @@
         assert result is not None
         assert IFileRetrieval.providedBy(result)
-        assert isinstance(result, ImageStorageFileRetrieval)
         return
 
Index: main/waeup.sirp/trunk/src/waeup/sirp/tests/test_imagestorage.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/tests/test_imagestorage.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/tests/test_imagestorage.py	(revision 7063)
@@ -1,16 +1,20 @@
 import os
-import sys
 import tempfile
 import shutil
 import unittest
 from StringIO import StringIO
+from hurry.file import HurryFile
 from hurry.file.interfaces import IFileRetrieval
+from zope.component import (
+    getUtility, provideUtility, queryUtility, provideAdapter)
 from zope.component.hooks import setSite
 from zope.interface.verify import verifyClass, verifyObject
 from waeup.sirp.app import University
 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
-from waeup.sirp.image import createWAeUPImageFile
 from waeup.sirp.imagestorage import (
-    md5digest, Basket, ImageStorage, ImageStorageFileRetrieval,)
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.sirp.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
 
 class HelperFuncsTests(unittest.TestCase):
@@ -22,17 +26,145 @@
         shutil.rmtree(self.workdir)
 
-    def test_md5digest(self):
-        samplefile = os.path.join(self.workdir, 'sample')
-        open(samplefile, 'wb').write('blah')
-        fp = open(samplefile, 'r')
-        digest = md5digest(fp)
-        self.assertEqual(digest, '6f1ed002ab5595859014ebf0951522d9')
-
-class BasketTests(FunctionalTestCase):
+class FileStoreNameChooserTests(FunctionalTestCase):
 
     layer = FunctionalLayer
 
+    def test_iface(self):
+        # we provide the interfaces we promise to do
+        obj = FileStoreNameChooser(None)
+        verifyClass(IFileStoreNameChooser, FileStoreNameChooser)
+        verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_accessible_as_adapter(self):
+        # we can get a file name chooser via adapter
+        chooser = IFileStoreNameChooser(object())
+        self.assertTrue(
+            isinstance(chooser, FileStoreNameChooser))
+        return
+
+    def test_check_name(self):
+        # default file name choosers accept any string
+        chooser = FileStoreNameChooser(object())
+        self.assertEqual(chooser.checkName('Hi there!'), True)
+        self.assertEqual(chooser.checkName(None), False)
+        return
+
+    def test_choose_name(self):
+        # we get a simple string if we do not pass in a valid string
+        chooser = FileStoreNameChooser(object())
+        self.assertEqual(chooser.chooseName('myname'), 'myname')
+        self.assertEqual(chooser.chooseName(None), u'unknown_file')
+        return
+
+class ExtFileStoreTests(unittest.TestCase):
+    # Test external file store (non-functional mode)
+
     def setUp(self):
-        super(BasketTests, self).setUp()
+        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):
+        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 CustomContext(object):
+    pass
+
+class CustomContextFileChooser(object):
+    def __init__(self, context):
+        self.context = context
+
+    def chooseName(self, name=None):
+        return '__mymarker__mysample.txt'
+
+class FunctionalExtFileStoreTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalExtFileStoreTests, self).setUp()
         self.workdir = tempfile.mkdtemp()
         self.samplefile = os.path.join(self.workdir, 'sample')
@@ -40,263 +172,110 @@
         open(self.samplefile, 'wb').write('Hi there!')
         open(self.otherfile, 'wb').write('Hi from other!')
-        self.basket = Basket()
         self.fd = open(self.samplefile, 'r')
         self.fd2 = open(self.otherfile, 'r')
-        self.stderr = StringIO()
-        self.old_stderr = sys.stderr
+        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')
+        # register a file chooser adapter for CustomContext
+        provideAdapter(
+            CustomContextFileChooser,
+            (CustomContext,), IFileStoreNameChooser)
+        return
+
 
     def tearDown(self):
-        sys.stderr = self.old_stderr
-        super(BasketTests, self).tearDown()
-        self.fd.close()
-        shutil.rmtree(self.workdir)
-        self.basket._del() # Remove subojects explicitly
-        del self.basket
-        return
-
-    def test_ifaces(self):
-        pass
-
-    def test_curr_id_empty(self):
-        curr_id = self.basket.curr_id
-        self.assertEqual(curr_id, '1')
-
-    def test_getInternalId_empty(self):
-        basket_id = self.basket.getInternalId(self.fd)
-        self.assertTrue(basket_id is None)
-
-    def test_storeFile_single(self):
-        basket_id = self.basket.storeFile(self.fd, 'sample')
-        self.assertEqual(basket_id, '1')
-        contents = self.basket['1'].open('r').read()
-        self.assertEqual(contents, 'Hi there!')
-
-    def test_storeFile_double(self):
-        basket_id1 = self.basket.storeFile(self.fd, 'sample')
-        basket_id2 = self.basket.storeFile(self.fd, 'sample')
-        self.assertTrue(basket_id1 == basket_id2 == '1')
-        contents = self.basket['1'].open('r').read()
-        self.assertEqual(contents, 'Hi there!')
-
-    def test_storeFile_multiple(self):
-        basket_id1 = self.basket.storeFile(self.fd, 'sample')
-        basket_id2 = self.basket.storeFile(self.fd2, 'sample')
-        self.assertEqual(basket_id1, '1')
-        self.assertEqual(basket_id2, '2')
-        contents1 = self.basket['1'].open('r').read()
-        contents2 = self.basket['2'].open('r').read()
-        self.assertEqual(contents1, 'Hi there!')
-        self.assertEqual(contents2, 'Hi from other!')
-
-    def test_retrieveFile(self):
-        basket_id = self.basket.storeFile(self.fd, 'sample')
-        fd = self.basket.retrieveFile(basket_id)
-        result = fd.read()
-        self.assertEqual(result, 'Hi there!')
-
-    def test_retrieveFile_not_existent(self):
-        result = self.basket.retrieveFile('not-a-valid-basket-id')
-        self.assertTrue(result is None)
-
-    def test_detect_zero_length_blobs(self):
-        # Ensure we get a warning when an empty Blob is found
-        self.basket.storeFile(self.fd, 'sample')
-        self.basket['1'].open('w').write('')
-        self.fd.seek(0)
-        sys.stderr = self.stderr         # Redirect stderr
-        self.basket.storeFile(self.fd, 'sample')
-        sys.stderr = self.old_stderr     # Restore stderr
-        self.stderr.seek(0)
-        self.assertTrue(
-            "EMPTY BLOB DETECTED" in self.stderr.read())
-
-    def test_refill_zero_length_blobs(self):
-        # When we detect an empty Blob, it will be reused
-        self.basket.storeFile(self.fd, 'sample')
-        self.basket['1'].open('w').write('')
-        self.fd.seek(0)
-        sys.stderr = self.stderr         # Redirect stderr
-        self.basket.storeFile(self.fd, 'sample')
-        sys.stderr = self.old_stderr     # Restore stderr
-        contents = self.basket['1'].open('r').read()
-        self.assertEqual(contents, 'Hi there!')
-
-
-class ImageStorageTests(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(ImageStorageTests, 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')
-
-    def tearDown(self):
-        super(ImageStorageTests, self).tearDown()
+        super(FunctionalExtFileStoreTests, self).tearDown()
         self.fd.close()
         self.fd2.close()
         shutil.rmtree(self.workdir)
-        self.storage._del() # Remove subojects explicitly
-        del self.storage
-        return
-
-    def test_ifaces(self):
-        pass
-
-    def test_storeFile(self):
-        full_id = self.storage.storeFile(self.fd, 'sample.txt')
-        self.assertEqual(full_id, '396199333edbf40ad43e62a1c1397793-1')
-
-    def test_storeFile_duplicate(self):
-        full_id1 = self.storage.storeFile(self.fd, 'sample1.txt')
-        full_id2 = self.storage.storeFile(self.fd, 'sample2.txt')
-        full_id3 = self.storage.storeFile(self.fd, 'sample1.txt')
-        self.assertEqual(full_id1, '396199333edbf40ad43e62a1c1397793-1')
-        self.assertEqual(full_id2, '396199333edbf40ad43e62a1c1397793-1')
-        self.assertEqual(full_id3, '396199333edbf40ad43e62a1c1397793-1')
-        contents = self.storage.retrieveFile(
-            '396199333edbf40ad43e62a1c1397793-1').read()
-        self.assertEqual(contents, 'Hi there!')
-
-    def test_storeFile_multiple(self):
-        full_id1 = self.storage.storeFile(self.fd, 'sample1.txt')
-        full_id2 = self.storage.storeFile(self.fd2, 'sample1.txt')
-        self.assertEqual(full_id1, '396199333edbf40ad43e62a1c1397793-1')
-        self.assertEqual(full_id2, '6936fcf8d564f1c5be5a017e650c5e8f-1')
-
-    def test_retrieveFile_not_existent(self):
-        result = self.storage.retrieveFile('not-existent')
-        self.assertTrue(result is None)
-
-    def test_retrieveFile_illegal_marker(self):
-        result1 = self.storage.retrieveFile('really-not-existent')
-        result2 = self.storage.retrieveFile('notexistent')
-        self.assertTrue(result1 is result2 is None)
-
-    def test_retrieveFile(self):
-        full_id = self.storage.storeFile(self.fd, 'sample.txt')
-        result = self.storage.retrieveFile(full_id)
-        content = result.read()
-        self.assertEqual(content, 'Hi there!')
-
-    def test_retrieveFile_multiple(self):
-        full_id1 = self.storage.storeFile(self.fd, 'sample.txt')
-        full_id2 = self.storage.storeFile(self.fd2, 'other.txt')
-        result1 = self.storage.retrieveFile(full_id1)
-        result2 = self.storage.retrieveFile(full_id2)
-        content1 = result1.read()
-        content2 = result2.read()
-        self.assertEqual(content1, 'Hi there!')
-        self.assertEqual(content2, 'Hi from other!')
-
-class ImageStorageFileRetrievalTests(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(ImageStorageFileRetrievalTests, 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')
-        # Set up a single image storage in a site
-        self.getRootFolder()['app'] = University()
-        self.app = self.getRootFolder()['app']
-        if not 'images' in self.app.keys():
-            self.app['images'] = ImageStorage()
-        self.storage = self.app['images']
-        return
-
-
-    def tearDown(self):
-        super(ImageStorageFileRetrievalTests, self).tearDown()
-        self.fd.close()
-        self.fd2.close()
-        shutil.rmtree(self.workdir)
-        self.storage._del() # Remove subojects explicitly
-        return
-
-    def test_ifaces(self):
-        retrieval = ImageStorageFileRetrieval()
-        assert verifyClass(IFileRetrieval, ImageStorageFileRetrieval)
-        assert verifyObject(IFileRetrieval, retrieval)
-        return
-
-    def test_getImageStorage_nosite(self):
-        retrieval = ImageStorageFileRetrieval()
-        storage = retrieval.getImageStorage()
-        self.assertTrue(storage is None)
-        return
-
-    def test_getImageStorage(self):
+        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)
-        retrieval = ImageStorageFileRetrieval()
-        storage = retrieval.getImageStorage()
-        self.assertTrue(storage is self.storage)
-        return
-
-    def test_isImageStorageEnabled_nosite(self):
-        retrieval = ImageStorageFileRetrieval()
-        self.assertTrue(retrieval.isImageStorageEnabled() is False)
-        return
-
-    def test_isImageStorageEnabled(self):
-        setSite(self.app)
-        retrieval = ImageStorageFileRetrieval()
-        self.assertTrue(retrieval.isImageStorageEnabled() is True)
-        return
-
-    def test_getFile_nosite(self):
-        retrieval = ImageStorageFileRetrieval()
-        f = retrieval.getFile('Hi there!')
-        self.assertEqual(f.read(), 'Hi there!')
-        return
-
-    def test_getFile(self):
-        setSite(self.app)
-        retrieval = ImageStorageFileRetrieval()
-        waeup_image = retrieval.createFile('sample.txt', self.fd)
-        full_id = waeup_image.data
-        result = retrieval.getFile(full_id)
-        self.assertEqual(result.read(), 'Hi there!')
-        return
-
-    def test_createFile_nosite(self):
-        retrieval = ImageStorageFileRetrieval()
-        waeup_image = retrieval.createFile('sample.txt', self.fd)
-        self.assertEqual(waeup_image.data, 'Hi there!')
-        return
-
-    def test_createFile(self):
-        # Ensure we can create WAeUPImageFiles when in site
-        setSite(self.app)
-        retrieval = ImageStorageFileRetrieval()
-        waeup_image = retrieval.createFile('sample.txt', self.fd)
-        full_id = waeup_image.data
-        self.assertEqual(full_id, '396199333edbf40ad43e62a1c1397793-1')
-        return
-
-    def test_waeupimagefile(self):
-        # Make sure WAeUPImageFile can use our file retrieval
-        setSite(self.app)
-        myfile = createWAeUPImageFile('sample.jpg', self.fd)
-        contents = myfile.file.read()
-        self.assertEqual(contents, 'Hi there!')
-
-    def test_waeupimagefile_raw(self):
-        # Make sure we can retrieve a file also if it was initialized
-        # with no image storage available
-        myfile = createWAeUPImageFile('sample.jpg', self.fd)
-        setSite(self.app)
-        contents = myfile.file.read()
-        self.assertEqual(contents, 'Hi there!')
+        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_file_by_context(self):
+        # if we register a file name chooser, we can also get a file
+        # by context
+        fs = ExtFileStore()
+        context = CustomContext()
+        file_id = IFileStoreNameChooser(context).chooseName()
+        fs = ExtFileStore()
+        fs.createFile(file_id, StringIO('my sample'))
+        result = fs.getFileByContext(context)
+        self.assertEqual(
+            file_id, '__mymarker__mysample.txt')
+        self.assertEqual(
+            result.read(), 'my sample')
+        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))
Index: main/waeup.sirp/trunk/src/waeup/sirp/university/certificatecontainer.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/university/certificatecontainer.py	(revision 7062)
+++ main/waeup.sirp/trunk/src/waeup/sirp/university/certificatecontainer.py	(revision 7063)
@@ -26,5 +26,5 @@
 
     Each :class:`CertificateContainer` provides
-    :iface:`ICertificateContainer`.
+    :class:`ICertificateContainer`.
     """
     grok.implements(ICertificateContainer)
@@ -35,5 +35,5 @@
 
         The `certificate` must be an object implementing
-        :iface:`waeup.sirp.university.interfaces.ICertificate`. If
+        :class:`waeup.sirp.university.interfaces.ICertificate`. If
         not, a :exc:`TypeError` is raised.
 
@@ -75,5 +75,5 @@
 
         The certificate must be an object implementing
-        :iface:`waeup.sirp.university.interfaces.ICertificate`. If
+        :class:`waeup.sirp.university.interfaces.ICertificate`. If
         not, a :exc:`TypeError` is raised.
 
