Index: main/waeup.ikoba/trunk/src/waeup/ikoba/app.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/app.txt	(revision 12987)
+++ 	(revision )
@@ -1,111 +1,0 @@
-:mod:`waeup.ikoba.app` -- central components for Ikoba
-******************************************************
-
-.. :doctest:
-.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
-
-.. module:: waeup.ikoba.app
-
-.. class:: Company
-
-  The main Ikoba application object is given with
-  :class:`Company`. It provides the main containers as faculties,
-  hostels, etc.
-
-  .. attribute:: name
-
-     A string. The name of a company.
-
-  .. attribute:: faculties
-
-     An arbitrary object containing "faculties". In the case of
-     `Company` it is a container of type
-     `waeup.ikoba.interfaces.IFacultiesContainer`.
-
-
-Creating `Company` instances
-============================
-
-As :class:`Company` instances make use of the Zope Component
-Architecture (ZCA), we have to setup the basic component registries,
-before we can create instances. We do this by simply grokking the
-whole :mod:`waeup` package. This way all interfaces, utilities and adapters
-defined in the package are looked up and registered with the global
-registries (this is done by the testlayer automatically).
-
-Now we can import the :class:`Company` class and create an
-instance:
-
-    >>> from waeup.ikoba.app import Company
-    >>> mycompany = Company()
-    >>> mycompany
-    <waeup.ikoba.app.Company object at 0x...>
-
-Instances of `Company` comply with the interface
-`waeup.ikoba.interfaces.ICompany`:
-
-    >>> from zope.interface.verify import verifyClass
-    >>> from waeup.ikoba.interfaces import ICompany
-    >>> verifyClass(ICompany, Company)
-    True
-
-A freshly created instance provides the attributes promised by the
-interface:
-
-    >>> from waeup.ikoba.app import Company
-    >>> mycompany = Company()
-    >>> mycompany['configuration'].name
-    u'Sample Company'
-
-  >>> mycompany['users']
-  <waeup.ikoba.userscontainer.UsersContainer object at 0x...>
-
-  >>> mycompany['datacenter']
-  <waeup.ikoba.datacenter.DataCenter object at 0x...>
-
-  >>> mycompany['configuration']
-  <waeup.ikoba.configuration.ConfigurationContainer object at 0x...>
-
-
-Ikoba plugins
-=============
-
-waeup.ikoba provides an API to 'plugin' components. Things that should
-be setup at creation time of a Ikoba application can indicate
-that by providing a utility providing IIkobaPlugin.
-
-The plugins are looked up by an created app, which then will call the
-``setup()`` method of each plugin.
-
-   >>> from waeup.ikoba.interfaces import IIkobaPluggable
-   >>> from zope.component import getAdapters, getUtilitiesFor
-   >>> sorted(list(getUtilitiesFor(IIkobaPluggable)))
-   [(u'documents', <waeup.ikoba.documents.container.DocumentsPlugin ...)]
-
-
-We can provide a new plugin like this:
-
-   >>> import grok
-   >>> from waeup.ikoba.interfaces import IIkobaPluggable
-   >>> class MyPlugin(grok.GlobalUtility):
-   ...   grok.implements(IIkobaPluggable)
-   ...   def setup(self, site, name, logger):
-   ...     print "Setup was called for"
-   ...     print site
-   ...   def update(self, site, name, logger):
-   ...     pass
-
-When we register the plugin
-
-   >>> grok.testing.grok_component('MyPlugin', MyPlugin)
-   True
-
-and setup a new Ikoba instance, we will get a message:
-
-   >>> from waeup.ikoba.app import Company
-   >>> site = Company()
-   Setup was called for
-   <waeup.ikoba.app.Company object at 0x...>
-
-Apparently the plugin can do with the Company object whatever it
-likes. That's what plugins are for.
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/authentication.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/authentication.txt	(revision 12987)
+++ 	(revision )
@@ -1,149 +1,0 @@
-Ikoba authentication
-*******************
-
-We need to protect most pieces of our portals from unauthenticated
-access.
-
-Therefore users have to login to access main functionality and they
-are able to log out afterwards.
-
-Before we can check access we have to create an app:
-
-  >>> from zope.component.hooks import setSite # only needed in tests
-  >>> from waeup.ikoba.app import Company
-  >>> root = getRootFolder()
-  >>> u = Company()
-  >>> root['app'] = u
-  >>> setSite(root['app'])                     # only needed in tests
-
-To make sure, we can 'watch' pages, we first have to initialize our
-test browser:
-
-  >>> from zope.testbrowser.testing import Browser
-  >>> browser = Browser()
-  >>> browser.handleErrors = False
-
-Creating users (principals)
-===========================
-
-Before we can login, we have to provide a user (``principal`` in Zope
-terms) with a password (and optional a title or description):
-
-  >>> root['app']['users'].addUser('bob', 'bobsecret',
-  ...                           title='Bob', description='A sample user')
-
-We can also add complete `Account` objects. An `Account` stores the
-user credentials and some metadata persistently:
-
-  >>> from waeup.ikoba.authentication import Account
-  >>> alice = Account('alice', 'alicesecret',roles=['waeup.ManageDataCenter'])
-  >>> root['app']['users'].addAccount(alice)
-
-See ``userscontainer.txt`` for details about the UsersContainer we use here.
-
-Users and local roles
-=====================
-
-Accounts also hold infos about local roles assigned to a user. In the
-beginning, users have the local owner role of their own account object:
-
-  >>> alice.getLocalRoles()
-  {'waeup.local.Owner': [<waeup.ikoba.authentication.Account object at 0x...>]}
-
-User  automatically get the global ProductsOfficer role:
-
-  >>> alice.getSiteRolesForPrincipal()
-  ['waeup.ManageDataCenter', 'waeup.ProductsOfficer']
-
-We can tell an account, that Alice got some role for another object:
-
-  >>> chalet = object()
-  >>> root['app']['chalet'] = chalet
-  >>> alice.notifyLocalRoleChanged(chalet, 'BigBoss', granted=True)
-
-Now Alice is the Big Boss:
-
-  >>> alice.getLocalRoles()
-  {'BigBoss': [<object object at 0x...>]}
-
-When we do not want Alice to be the Big Boss we can tell that too:
-
-  >>> alice.notifyLocalRoleChanged(chalet, 'BigBoss', granted=False)
-  >>> alice.getLocalRoles()
-  {'waeup.local.Owner': [<waeup.ikoba.authentication.Account object at 0x...>]}
-
-We can also use events to trigger such actions. This is recommended
-because we do not neccessarily know where Alice lives:
-
-  >>> from waeup.ikoba.authentication import LocalRoleSetEvent
-  >>> from zope.event import notify
-  >>> notify(LocalRoleSetEvent(chalet, 'BigBoss', 'alice',
-  ...                          granted=True))
-  >>> alice.getLocalRoles()
-  {'BigBoss': [<object object at 0x...>]}
-
-When objects are deleted, local roles are also deleted
-semi-magically. This happens through event subscribers listening to
-IObjectRemovedEvents. The latters are naturally only fired when ZODB
-stored objects are removed. Furthermore this subscriber reads the
-internal local roles table.
-
-
-Logging in via side bar
-=======================
-
-We can access the front page without restrictions:
-
-  >>> browser.open('http://localhost/app')
-  >>> print browser.headers['Status']
-  200 Ok
-
-We have to go to one of the login pages first:
-
-  >>> browser.open('http://localhost/app')
-  >>> browser.getLink('Login').click()
-  >>> print browser.headers['Status']
-  200 Ok
-
-There is a login form on tis page:
-
-  >>> 'form.login' in browser.contents
-  True
-
-  >>> 'form.logout' in browser.contents
-  False
-
-We use this form:
-
-  >>> browser.getControl(name='form.login').value = 'bob'
-  >>> browser.getControl(name='form.password').value = 'invalidpw'
-  >>> browser.getControl('Login').click()
-  >>> 'You entered invalid credentials' in browser.contents
-  True
-
-  >>> browser.getControl(name='form.login').value = 'bob'
-  >>> browser.getControl(name='form.password').value = 'bobsecret'
-  >>> browser.getControl('Login').click()
-
-Now the login form is gone. Instead we have the opportunity to logout:
-
-  >>> 'form.login' in browser.contents
-  False
-
-  >>> logout = browser.getLink('Logout')
-  >>> logout
-  <Link text='Logout' url='http://localhost/app/@@logout'>
-
-The user title is also displayed in the sidebar:
-
-  >>> 'Bob' in browser.contents
-  True
-
-We can also log out afterwards:
-
-  >>> logout.click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Login
-  ...
-
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/catalog.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/catalog.txt	(revision 12987)
+++ 	(revision )
@@ -1,235 +1,0 @@
-:mod:`waeup.ikoba.catalog` -- Cataloging support Ikoba
-****************************************************
-
-.. module:: waeup.ikoba.catalog
-
-Components that support cataloging and searching objects inside a
-Ikoba site.
-
-.. :doctest:
-.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
-
-.. contents::
-
-Classes
-=======
-
-:class:`IkobaQuery`
-------------------
-
-.. class:: IkobaQuery()
-
-   .. attribute:: grok.implements(hurry.query.interfaces.IQuery)
-
-   A `hurry.query.query.Query` compatible query that also supports
-   retrival of plain ``Bree`` result sets as used inside a catalog.
-
-   Like `hurry.query.query.Query` objects, `IkobaQuery` is some kind
-   of a meta query or 'compound query' that can give the cataloged
-   objects (or their int ids) matching one or more 'subqueries'.
-
-   This way you can search for objects (or their int ids) that match
-   several criteria at the same time. See ``examples`` section below.
-
-   A singleton instance of this class is also available as global
-   utility.
-
-   .. method:: searchResults(query)
-
-     Get the cataloged objects determined by ``query``.
-
-   .. method:: apply(query)
-
-     Get the list of int ids (a `BTree` result set) for objects
-     determined by ``query``.
-
-     The list of int ids is less expensive to compute than the
-     complete search results and sufficient, for instance, when you
-     only need the number of objects that match a query and not the
-     objects themselves.
-
-Examples
-========
-
-Getting a general query object
-------------------------------
-
-We can get a IkobaQuery object by asking for an unnamed global utility
-implementing `hurry.query.interfaces.IQuery`:
-
-    >>> from hurry.query.interfaces import IQuery
-    >>> from zope.component import getUtility
-    >>> q = getUtility(IQuery)
-    >>> q
-    <waeup.ikoba.catalog.IkobaQuery object at 0x...>
-
-This query can get 'subqueries' and delivers the objects found or
-their ids. To show this we have to setup a catalog with some entries.
-
-
-Setting up a catalog and feeding it
------------------------------------
-
-    >>> from zope.catalog.interfaces import ICatalog
-    >>> from zope.catalog.catalog import Catalog
-    >>> mycat = Catalog()
-
-We register this catalog with the component architechture as a utility
-named 'mycatalog':
-
-    >>> from zope.component import provideUtility
-    >>> provideUtility(mycat, ICatalog, 'mycatalog')
-
-We setup a special content type whose instances we will catalog later:
-
-    >>> from zope.interface import Interface, Attribute, implements
-    >>> from zope.container.contained import Contained
-    >>> class IMammoth(Interface):
-    ...   name = Attribute('name')
-    ...   age = Attribute('age')
-
-    >>> class Mammoth(Contained):
-    ...   implements(IMammoth)
-    ...   def __init__(self, name, age):
-    ...     self.name = name
-    ...     self.age = age
-    ...   def __cmp__(self, other):
-    ...     return cmp(self.name, other.name)
-
-By including the __cmp__ method we make sure search results can be
-stably sorted.
-
-We also setup a `zope.intid.interfaces.IIntIds` utility. This is not
-necessary for plain catalogs, but when we want to use IkobaQuery (or
-`hurry.query.query.Query` objects), as to get a unique mapping from
-objects (stored in ZODB) to integer numbers (stored in catalogs),
-these query objects lookup a global IIntIds utiliy:
-
-    >>> from zope import interface
-    >>> import zope.intid.interfaces
-    >>> class DummyIntId(object):
-    ...     interface.implements(zope.intid.interfaces.IIntIds)
-    ...     MARKER = '__dummy_int_id__'
-    ...     def __init__(self):
-    ...         self.counter = 0
-    ...         self.data = {}
-    ...     def register(self, obj):
-    ...         intid = getattr(obj, self.MARKER, None)
-    ...         if intid is None:
-    ...             setattr(obj, self.MARKER, self.counter)
-    ...             self.data[self.counter] = obj
-    ...             intid = self.counter
-    ...             self.counter += 1
-    ...         return intid
-    ...     def getObject(self, intid):
-    ...         return self.data[intid]
-    ...     def __iter__(self):
-    ...         return iter(self.data)
-    >>> intid = DummyIntId()
-    >>> from zope.component import provideUtility
-    >>> provideUtility(intid, zope.intid.interfaces.IIntIds)
-
-Now we can catalog some mammoths. Here we create a herd and catalog
-each item of it:
-
-    >>> from zope.catalog.field import FieldIndex
-    >>> mycat['mammoth_name'] = FieldIndex('name', IMammoth)
-    >>> mycat['mammoth_age'] = FieldIndex('age', IMammoth)
-
-    >>> herd = [
-    ...   Mammoth(name='Fred', age=33),
-    ...   Mammoth(name='Hank', age=30),
-    ...   Mammoth(name='Wilma', age=28),
-    ... ]
-
-    >>> for mammoth in herd:
-    ...   mycat.index_doc(intid.register(mammoth), mammoth)
-
-
-Searching for result sets
--------------------------
-
-Finally we can perform queries:
-
-    >>> from hurry.query import Eq
-    >>> from zope.component import getUtility
-    >>> subquery1 = Eq(('mycatalog', 'mammoth_name'), 'Fred')
-
-The latter means: search for objects whose name is ``'Fred'`` in the
-``mammoth_name`` index of a catalog registered as a utility named
-``mycatalog``.
-
-    >>> from hurry.query import Between
-    >>> subquery2 = Between(('mycatalog', 'mammoth_age'), 30, 33)
-
-This means: ask for objects cataloged in an index named 'mammoth_age',
-whose cataloged value is between 30 and 33 (including this values).
-
-    >>> r1 = q.apply(subquery2)
-    >>> r1
-    IFSet([0, 1])
-
-Using ``apply()`` above, we get a set of values stored in an
-``IFBTree``:
-
-    >>> type(r1)
-    <type 'BTrees.IFBTree.IFSet'>
-
-``IFBTree`` objects implement a rather efficient integer to float
-mapping where also integers are allowed as values. For each object
-found (i.e. mammoths whose age is between 30 and 33), we get the
-number of its entry.
-
-To get the real object, we can use intids here, because we setup an
-appropriate IIntIds utility before:
-
-    >>> [intid.getObject(x).name for x in r1]
-    ['Fred', 'Hank']
-
-We can (and should) also use the `searchResults()` method explained
-below to do that.
-
-Retrieving BTree sets can, however, make sense, if you want to know
-only the number of results for a particular query or whether there are
-results at all in a more efficient way:
-
-    >>> len(r1)
-    2
-
-Searching for objects
----------------------
-
-Very often we don't want to know the catalog-internal 'ids' of
-searched objects but the objects themselves.
-
-This can be done by using the ``searchResults`` method of
-``IkobaQuery``:
-
-    >>> r2 = q.searchResults(subquery1)
-    >>> r2
-    <zope.catalog.catalog.ResultSet instance at 0x...>
-
-    >>> list(r2)
-    [<Mammoth object at 0x...>]
-
-We got one result item, we can immediately ask for further infos. To
-access a result item by its index number, we have to turn the
-ResultSet into an ordinary list before:
-
-    >>> entry = list(r2)[0]
-    >>> entry.name, entry.age
-    ('Fred', 33)
-
-We can also use ``subquery2`` as above:
-
-    >>> r3 = q.searchResults(subquery2)
-    >>> [(x.name, x.age) for x in r3]
-    [('Fred', 33), ('Hank', 30)]
-
-or use both queries at once:
-
-    >>> r4 = q.searchResults(subquery1 & subquery2)
-    >>> [(x.name, x.age) for x in r4]
-    [('Fred', 33)]
-
-which will give us, of course, the same result set as with subquery1.
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/datacenter.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/datacenter.txt	(revision 12987)
+++ 	(revision )
@@ -1,290 +1,0 @@
-Ikoba Data Center
-****************
-
-The Ikoba data center cares for managing CSV files and importing then.
-
-.. :doctest:
-.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
-
-Creating a data center
-======================
-
-A data center can be created easily:
-
-    >>> from waeup.ikoba.datacenter import DataCenter
-    >>> mydatacenter = DataCenter()
-    >>> mydatacenter
-    <waeup.ikoba.datacenter.DataCenter object at 0x...>
-
-Each data center has a location in file system where files are stored:
-
-    >>> storagepath = mydatacenter.storage
-    >>> storagepath
-    '/tmp/tmp...'
-
-Beside other things it provides a location to put data of deleted
-items into:
-
-    >>> import os
-    >>> del_path = mydatacenter.deleted_path
-    >>> os.path.isdir(del_path)
-    True
-
-Overall it complies with the `IDataCenter` interface:
-
-    >>> from zope.interface import verify
-    >>> from waeup.ikoba.interfaces import IDataCenter
-    >>> verify.verifyObject(IDataCenter, DataCenter() )
-    True
-
-    >>> verify.verifyClass(IDataCenter, DataCenter)
-    True
-
-Managing the storage path
--------------------------
-
-We can set another storage path:
-
-    >>> import os
-    >>> os.mkdir('newlocation')
-    >>> newpath = os.path.abspath('newlocation')
-    >>> mydatacenter.setStoragePath(newpath)
-    []
-
-The result here is a list of filenames, that could not be
-copied. Luckily, this list is empty.
-
-When we set a new storage path, we can tell to move all files in the
-old location to the new one. To see this feature in action, we first
-have to put a file into the old location:
-
-    >>> open(os.path.join(newpath, 'myfile.txt'), 'wb').write('hello')
-
-Now we can set a new location and the file will be copied:
-
-    >>> verynewpath = os.path.abspath('verynewlocation')
-    >>> os.mkdir(verynewpath)
-
-    >>> mydatacenter.setStoragePath(verynewpath, move=True)
-    []
-
-    >>> storagepath = mydatacenter.storage
-    >>> 'myfile.txt' in os.listdir(verynewpath)
-    True
-
-We remove the created file to have a clean testing environment for
-upcoming examples:
-
-    >>> os.unlink(os.path.join(storagepath, 'myfile.txt'))
-
-Uploading files
-===============
-
-We can get a list of files stored in that location:
-
-    >>> mydatacenter.getPendingFiles()
-    []
-
-Let's put some file in the storage:
-
-    >>> import os
-    >>> filepath = os.path.join(storagepath, 'data.csv')
-    >>> open(filepath, 'wb').write('Some Content\n')
-
-Now we can find a file:
-
-    >>> mydatacenter.getPendingFiles()
-    [<waeup.ikoba.datacenter.DataCenterFile object at 0x...>]
-
-As we can see, the actual file is wrapped by a convenience wrapper,
-that enables us to fetch some data about the file. The data returned
-is formatted in strings, so that it can easily be put into output
-pages:
-
-    >>> datafile = mydatacenter.getPendingFiles()[0]
-    >>> datafile.getSize()
-    '13 bytes'
-
-    >>> datafile.getDate() # Nearly current datetime...
-    '...'
-
-Clean up:
-
-    >>> import shutil
-    >>> shutil.rmtree(newpath)
-    >>> shutil.rmtree(verynewpath)
-
-
-Distributing processed files
-============================
-
-When files were processed by a batch processor, we can put the
-resulting files into desired destinations.
-
-We recreate the datacenter root in case it is missing:
-
-    >>> import os
-    >>> dc_root = mydatacenter.storage
-    >>> fin_dir = os.path.join(dc_root, 'finished')
-    >>> unfin_dir = os.path.join(dc_root, 'unfinished')
-
-    >>> def recreate_dc_storage():
-    ...   if os.path.exists(dc_root):
-    ...     shutil.rmtree(dc_root)
-    ...   os.mkdir(dc_root)
-    ...   mydatacenter.setStoragePath(mydatacenter.storage)
-    >>> recreate_dc_storage()
-
-We define a function that creates a set of faked result files:
-
-    >>> import os
-    >>> import tempfile
-    >>> def create_fake_results(source_basename, create_pending=True):
-    ...   tmp_dir = tempfile.mkdtemp()
-    ...   src = os.path.join(dc_root, source_basename)
-    ...   pending_src = None
-    ...   if create_pending:
-    ...     pending_src = os.path.join(tmp_dir, 'mypendingsource.csv')
-    ...   finished_src = os.path.join(tmp_dir, 'myfinishedsource.csv')
-    ...   for path in (src, pending_src, finished_src):
-    ...     if path is not None:
-    ...       open(path, 'wb').write('blah')
-    ...   return tmp_dir, src, finished_src, pending_src
-
-Now we can create the set of result files, that typically come after a
-successful processing of a regular source:
-
-Now we can try to distribute those files. Let's start with a source
-file, that was processed successfully:
-
-    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
-    ...  'mysource.csv', create_pending=False)
-    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
-    ...                            pending_src, mode='create')
-    >>> sorted(os.listdir(dc_root))
-    ['deleted', 'finished', 'logs', 'unfinished']
-
-    >>> sorted(os.listdir(fin_dir))
-    ['mysource.create.finished.csv', 'mysource.csv']
-
-    >>> sorted(os.listdir(unfin_dir))
-    []
-
-The created dir will be removed for us by the datacenter. This way we
-can assured, that less temporary dirs are left hanging around:
-
-    >>> os.path.exists(tmp_dir)
-    False
-
-The root dir is empty, while the original file and the file containing
-all processed data were moved to'finished/'.
-
-Now we restart, but this time we fake an erranous action:
-
-    >>> recreate_dc_storage()
-    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
-    ...  'mysource.csv')
-    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
-    ...                                 pending_src, mode='create')
-    >>> sorted(os.listdir(dc_root))
-    ['deleted', 'finished', 'logs', 'mysource.create.pending.csv', 'unfinished']
-
-    >>> sorted(os.listdir(fin_dir))
-    ['mysource.create.finished.csv']
-
-    >>> sorted(os.listdir(unfin_dir))
-    ['mysource.csv']
-
-While the original source was moved to the 'unfinished' dir, the
-pending file went to the root and the set of already processed items
-are stored in finished/.
-
-We fake processing the pending file and assume that everything went
-well this time:
-
-    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
-    ...  'mysource.create.pending.csv', create_pending=False)
-    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
-    ...                                 pending_src, mode='create')
-
-    >>> sorted(os.listdir(dc_root))
-    ['deleted', 'finished', 'logs', 'unfinished']
-
-    >>> sorted(os.listdir(fin_dir))
-    ['mysource.create.finished.csv', 'mysource.csv']
-
-    >>> sorted(os.listdir(unfin_dir))
-    []
-
-The result is the same as in the first case shown above.
-
-We restart again, but this time we fake several non-working imports in
-a row.
-
-We start with a faulty start-import:
-
-    >>> recreate_dc_storage()
-    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
-    ...  'mysource.csv')
-    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
-    ...                                 pending_src, mode='create')
-
-We try to process the pending file, which fails again:
-
-    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
-    ...  'mysource.create.pending.csv')
-    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
-    ...                                 pending_src, mode='create')
-
-We try to process the new pending file:
-
-    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
-    ...  'mysource.create.pending.csv')
-    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
-    ...                                 pending_src, mode='create')
-
-    >>> sorted(os.listdir(dc_root))
-    ['deleted', 'finished', 'logs', 'mysource.create.pending.csv', 'unfinished']
-
-    >>> sorted(os.listdir(fin_dir))
-    ['mysource.create.finished.csv']
-
-    >>> sorted(os.listdir(unfin_dir))
-    ['mysource.csv']
-
-Finally, we process the pending file and everything works:
-
-    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
-    ...  'mysource.create.pending.csv', create_pending=False)
-    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
-    ...                                 pending_src, mode='create')
-
-    >>> sorted(os.listdir(dc_root))
-    ['deleted', 'finished', 'logs', 'unfinished']
-
-    >>> sorted(os.listdir(fin_dir))
-    ['mysource.create.finished.csv', 'mysource.csv']
-
-    >>> sorted(os.listdir(unfin_dir))
-    []
-
-The root dir is empty (contains no input files) and only the files in
-finished-subdirectory remain.
-
-
-We can get a list of imported files stored in the finished subfolder:
-
-    >>> mydatacenter.getFinishedFiles()
-    [<waeup.ikoba.datacenter.DataCenterFile object at ...>]
-
-    >>> datafile = mydatacenter.getFinishedFiles()[0]
-    >>> datafile.getSize()
-    '2 bytes'
-
-    >>> datafile.getDate() # Nearly current datetime...
-    '...'
-
-
-Clean up:
-
-    >>> shutil.rmtree(verynewpath)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/app.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/app.txt	(revision 12988)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/app.txt	(revision 12988)
@@ -0,0 +1,111 @@
+:mod:`waeup.ikoba.app` -- central components for Ikoba
+******************************************************
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+.. module:: waeup.ikoba.app
+
+.. class:: Company
+
+  The main Ikoba application object is given with
+  :class:`Company`. It provides the main containers as faculties,
+  hostels, etc.
+
+  .. attribute:: name
+
+     A string. The name of a company.
+
+  .. attribute:: faculties
+
+     An arbitrary object containing "faculties". In the case of
+     `Company` it is a container of type
+     `waeup.ikoba.interfaces.IFacultiesContainer`.
+
+
+Creating `Company` instances
+============================
+
+As :class:`Company` instances make use of the Zope Component
+Architecture (ZCA), we have to setup the basic component registries,
+before we can create instances. We do this by simply grokking the
+whole :mod:`waeup` package. This way all interfaces, utilities and adapters
+defined in the package are looked up and registered with the global
+registries (this is done by the testlayer automatically).
+
+Now we can import the :class:`Company` class and create an
+instance:
+
+    >>> from waeup.ikoba.app import Company
+    >>> mycompany = Company()
+    >>> mycompany
+    <waeup.ikoba.app.Company object at 0x...>
+
+Instances of `Company` comply with the interface
+`waeup.ikoba.interfaces.ICompany`:
+
+    >>> from zope.interface.verify import verifyClass
+    >>> from waeup.ikoba.interfaces import ICompany
+    >>> verifyClass(ICompany, Company)
+    True
+
+A freshly created instance provides the attributes promised by the
+interface:
+
+    >>> from waeup.ikoba.app import Company
+    >>> mycompany = Company()
+    >>> mycompany['configuration'].name
+    u'Sample Company'
+
+  >>> mycompany['users']
+  <waeup.ikoba.userscontainer.UsersContainer object at 0x...>
+
+  >>> mycompany['datacenter']
+  <waeup.ikoba.datacenter.DataCenter object at 0x...>
+
+  >>> mycompany['configuration']
+  <waeup.ikoba.configuration.ConfigurationContainer object at 0x...>
+
+
+Ikoba plugins
+=============
+
+waeup.ikoba provides an API to 'plugin' components. Things that should
+be setup at creation time of a Ikoba application can indicate
+that by providing a utility providing IIkobaPlugin.
+
+The plugins are looked up by an created app, which then will call the
+``setup()`` method of each plugin.
+
+   >>> from waeup.ikoba.interfaces import IIkobaPluggable
+   >>> from zope.component import getAdapters, getUtilitiesFor
+   >>> sorted(list(getUtilitiesFor(IIkobaPluggable)))
+   [(u'documents', <waeup.ikoba.documents.container.DocumentsPlugin ...)]
+
+
+We can provide a new plugin like this:
+
+   >>> import grok
+   >>> from waeup.ikoba.interfaces import IIkobaPluggable
+   >>> class MyPlugin(grok.GlobalUtility):
+   ...   grok.implements(IIkobaPluggable)
+   ...   def setup(self, site, name, logger):
+   ...     print "Setup was called for"
+   ...     print site
+   ...   def update(self, site, name, logger):
+   ...     pass
+
+When we register the plugin
+
+   >>> grok.testing.grok_component('MyPlugin', MyPlugin)
+   True
+
+and setup a new Ikoba instance, we will get a message:
+
+   >>> from waeup.ikoba.app import Company
+   >>> site = Company()
+   Setup was called for
+   <waeup.ikoba.app.Company object at 0x...>
+
+Apparently the plugin can do with the Company object whatever it
+likes. That's what plugins are for.
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/authentication.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/authentication.txt	(revision 12988)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/authentication.txt	(revision 12988)
@@ -0,0 +1,149 @@
+Ikoba authentication
+*******************
+
+We need to protect most pieces of our portals from unauthenticated
+access.
+
+Therefore users have to login to access main functionality and they
+are able to log out afterwards.
+
+Before we can check access we have to create an app:
+
+  >>> from zope.component.hooks import setSite # only needed in tests
+  >>> from waeup.ikoba.app import Company
+  >>> root = getRootFolder()
+  >>> u = Company()
+  >>> root['app'] = u
+  >>> setSite(root['app'])                     # only needed in tests
+
+To make sure, we can 'watch' pages, we first have to initialize our
+test browser:
+
+  >>> from zope.testbrowser.testing import Browser
+  >>> browser = Browser()
+  >>> browser.handleErrors = False
+
+Creating users (principals)
+===========================
+
+Before we can login, we have to provide a user (``principal`` in Zope
+terms) with a password (and optional a title or description):
+
+  >>> root['app']['users'].addUser('bob', 'bobsecret',
+  ...                           title='Bob', description='A sample user')
+
+We can also add complete `Account` objects. An `Account` stores the
+user credentials and some metadata persistently:
+
+  >>> from waeup.ikoba.authentication import Account
+  >>> alice = Account('alice', 'alicesecret',roles=['waeup.ManageDataCenter'])
+  >>> root['app']['users'].addAccount(alice)
+
+See ``userscontainer.txt`` for details about the UsersContainer we use here.
+
+Users and local roles
+=====================
+
+Accounts also hold infos about local roles assigned to a user. In the
+beginning, users have the local owner role of their own account object:
+
+  >>> alice.getLocalRoles()
+  {'waeup.local.Owner': [<waeup.ikoba.authentication.Account object at 0x...>]}
+
+User  automatically get the global ProductsOfficer role:
+
+  >>> alice.getSiteRolesForPrincipal()
+  ['waeup.ManageDataCenter', 'waeup.ProductsOfficer']
+
+We can tell an account, that Alice got some role for another object:
+
+  >>> chalet = object()
+  >>> root['app']['chalet'] = chalet
+  >>> alice.notifyLocalRoleChanged(chalet, 'BigBoss', granted=True)
+
+Now Alice is the Big Boss:
+
+  >>> alice.getLocalRoles()
+  {'BigBoss': [<object object at 0x...>]}
+
+When we do not want Alice to be the Big Boss we can tell that too:
+
+  >>> alice.notifyLocalRoleChanged(chalet, 'BigBoss', granted=False)
+  >>> alice.getLocalRoles()
+  {'waeup.local.Owner': [<waeup.ikoba.authentication.Account object at 0x...>]}
+
+We can also use events to trigger such actions. This is recommended
+because we do not neccessarily know where Alice lives:
+
+  >>> from waeup.ikoba.authentication import LocalRoleSetEvent
+  >>> from zope.event import notify
+  >>> notify(LocalRoleSetEvent(chalet, 'BigBoss', 'alice',
+  ...                          granted=True))
+  >>> alice.getLocalRoles()
+  {'BigBoss': [<object object at 0x...>]}
+
+When objects are deleted, local roles are also deleted
+semi-magically. This happens through event subscribers listening to
+IObjectRemovedEvents. The latters are naturally only fired when ZODB
+stored objects are removed. Furthermore this subscriber reads the
+internal local roles table.
+
+
+Logging in via side bar
+=======================
+
+We can access the front page without restrictions:
+
+  >>> browser.open('http://localhost/app')
+  >>> print browser.headers['Status']
+  200 Ok
+
+We have to go to one of the login pages first:
+
+  >>> browser.open('http://localhost/app')
+  >>> browser.getLink('Login').click()
+  >>> print browser.headers['Status']
+  200 Ok
+
+There is a login form on tis page:
+
+  >>> 'form.login' in browser.contents
+  True
+
+  >>> 'form.logout' in browser.contents
+  False
+
+We use this form:
+
+  >>> browser.getControl(name='form.login').value = 'bob'
+  >>> browser.getControl(name='form.password').value = 'invalidpw'
+  >>> browser.getControl('Login').click()
+  >>> 'You entered invalid credentials' in browser.contents
+  True
+
+  >>> browser.getControl(name='form.login').value = 'bob'
+  >>> browser.getControl(name='form.password').value = 'bobsecret'
+  >>> browser.getControl('Login').click()
+
+Now the login form is gone. Instead we have the opportunity to logout:
+
+  >>> 'form.login' in browser.contents
+  False
+
+  >>> logout = browser.getLink('Logout')
+  >>> logout
+  <Link text='Logout' url='http://localhost/app/@@logout'>
+
+The user title is also displayed in the sidebar:
+
+  >>> 'Bob' in browser.contents
+  True
+
+We can also log out afterwards:
+
+  >>> logout.click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Login
+  ...
+
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/catalog.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/catalog.txt	(revision 12988)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/catalog.txt	(revision 12988)
@@ -0,0 +1,235 @@
+:mod:`waeup.ikoba.catalog` -- Cataloging support Ikoba
+****************************************************
+
+.. module:: waeup.ikoba.catalog
+
+Components that support cataloging and searching objects inside a
+Ikoba site.
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+.. contents::
+
+Classes
+=======
+
+:class:`IkobaQuery`
+------------------
+
+.. class:: IkobaQuery()
+
+   .. attribute:: grok.implements(hurry.query.interfaces.IQuery)
+
+   A `hurry.query.query.Query` compatible query that also supports
+   retrival of plain ``Bree`` result sets as used inside a catalog.
+
+   Like `hurry.query.query.Query` objects, `IkobaQuery` is some kind
+   of a meta query or 'compound query' that can give the cataloged
+   objects (or their int ids) matching one or more 'subqueries'.
+
+   This way you can search for objects (or their int ids) that match
+   several criteria at the same time. See ``examples`` section below.
+
+   A singleton instance of this class is also available as global
+   utility.
+
+   .. method:: searchResults(query)
+
+     Get the cataloged objects determined by ``query``.
+
+   .. method:: apply(query)
+
+     Get the list of int ids (a `BTree` result set) for objects
+     determined by ``query``.
+
+     The list of int ids is less expensive to compute than the
+     complete search results and sufficient, for instance, when you
+     only need the number of objects that match a query and not the
+     objects themselves.
+
+Examples
+========
+
+Getting a general query object
+------------------------------
+
+We can get a IkobaQuery object by asking for an unnamed global utility
+implementing `hurry.query.interfaces.IQuery`:
+
+    >>> from hurry.query.interfaces import IQuery
+    >>> from zope.component import getUtility
+    >>> q = getUtility(IQuery)
+    >>> q
+    <waeup.ikoba.catalog.IkobaQuery object at 0x...>
+
+This query can get 'subqueries' and delivers the objects found or
+their ids. To show this we have to setup a catalog with some entries.
+
+
+Setting up a catalog and feeding it
+-----------------------------------
+
+    >>> from zope.catalog.interfaces import ICatalog
+    >>> from zope.catalog.catalog import Catalog
+    >>> mycat = Catalog()
+
+We register this catalog with the component architechture as a utility
+named 'mycatalog':
+
+    >>> from zope.component import provideUtility
+    >>> provideUtility(mycat, ICatalog, 'mycatalog')
+
+We setup a special content type whose instances we will catalog later:
+
+    >>> from zope.interface import Interface, Attribute, implements
+    >>> from zope.container.contained import Contained
+    >>> class IMammoth(Interface):
+    ...   name = Attribute('name')
+    ...   age = Attribute('age')
+
+    >>> class Mammoth(Contained):
+    ...   implements(IMammoth)
+    ...   def __init__(self, name, age):
+    ...     self.name = name
+    ...     self.age = age
+    ...   def __cmp__(self, other):
+    ...     return cmp(self.name, other.name)
+
+By including the __cmp__ method we make sure search results can be
+stably sorted.
+
+We also setup a `zope.intid.interfaces.IIntIds` utility. This is not
+necessary for plain catalogs, but when we want to use IkobaQuery (or
+`hurry.query.query.Query` objects), as to get a unique mapping from
+objects (stored in ZODB) to integer numbers (stored in catalogs),
+these query objects lookup a global IIntIds utiliy:
+
+    >>> from zope import interface
+    >>> import zope.intid.interfaces
+    >>> class DummyIntId(object):
+    ...     interface.implements(zope.intid.interfaces.IIntIds)
+    ...     MARKER = '__dummy_int_id__'
+    ...     def __init__(self):
+    ...         self.counter = 0
+    ...         self.data = {}
+    ...     def register(self, obj):
+    ...         intid = getattr(obj, self.MARKER, None)
+    ...         if intid is None:
+    ...             setattr(obj, self.MARKER, self.counter)
+    ...             self.data[self.counter] = obj
+    ...             intid = self.counter
+    ...             self.counter += 1
+    ...         return intid
+    ...     def getObject(self, intid):
+    ...         return self.data[intid]
+    ...     def __iter__(self):
+    ...         return iter(self.data)
+    >>> intid = DummyIntId()
+    >>> from zope.component import provideUtility
+    >>> provideUtility(intid, zope.intid.interfaces.IIntIds)
+
+Now we can catalog some mammoths. Here we create a herd and catalog
+each item of it:
+
+    >>> from zope.catalog.field import FieldIndex
+    >>> mycat['mammoth_name'] = FieldIndex('name', IMammoth)
+    >>> mycat['mammoth_age'] = FieldIndex('age', IMammoth)
+
+    >>> herd = [
+    ...   Mammoth(name='Fred', age=33),
+    ...   Mammoth(name='Hank', age=30),
+    ...   Mammoth(name='Wilma', age=28),
+    ... ]
+
+    >>> for mammoth in herd:
+    ...   mycat.index_doc(intid.register(mammoth), mammoth)
+
+
+Searching for result sets
+-------------------------
+
+Finally we can perform queries:
+
+    >>> from hurry.query import Eq
+    >>> from zope.component import getUtility
+    >>> subquery1 = Eq(('mycatalog', 'mammoth_name'), 'Fred')
+
+The latter means: search for objects whose name is ``'Fred'`` in the
+``mammoth_name`` index of a catalog registered as a utility named
+``mycatalog``.
+
+    >>> from hurry.query import Between
+    >>> subquery2 = Between(('mycatalog', 'mammoth_age'), 30, 33)
+
+This means: ask for objects cataloged in an index named 'mammoth_age',
+whose cataloged value is between 30 and 33 (including this values).
+
+    >>> r1 = q.apply(subquery2)
+    >>> r1
+    IFSet([0, 1])
+
+Using ``apply()`` above, we get a set of values stored in an
+``IFBTree``:
+
+    >>> type(r1)
+    <type 'BTrees.IFBTree.IFSet'>
+
+``IFBTree`` objects implement a rather efficient integer to float
+mapping where also integers are allowed as values. For each object
+found (i.e. mammoths whose age is between 30 and 33), we get the
+number of its entry.
+
+To get the real object, we can use intids here, because we setup an
+appropriate IIntIds utility before:
+
+    >>> [intid.getObject(x).name for x in r1]
+    ['Fred', 'Hank']
+
+We can (and should) also use the `searchResults()` method explained
+below to do that.
+
+Retrieving BTree sets can, however, make sense, if you want to know
+only the number of results for a particular query or whether there are
+results at all in a more efficient way:
+
+    >>> len(r1)
+    2
+
+Searching for objects
+---------------------
+
+Very often we don't want to know the catalog-internal 'ids' of
+searched objects but the objects themselves.
+
+This can be done by using the ``searchResults`` method of
+``IkobaQuery``:
+
+    >>> r2 = q.searchResults(subquery1)
+    >>> r2
+    <zope.catalog.catalog.ResultSet instance at 0x...>
+
+    >>> list(r2)
+    [<Mammoth object at 0x...>]
+
+We got one result item, we can immediately ask for further infos. To
+access a result item by its index number, we have to turn the
+ResultSet into an ordinary list before:
+
+    >>> entry = list(r2)[0]
+    >>> entry.name, entry.age
+    ('Fred', 33)
+
+We can also use ``subquery2`` as above:
+
+    >>> r3 = q.searchResults(subquery2)
+    >>> [(x.name, x.age) for x in r3]
+    [('Fred', 33), ('Hank', 30)]
+
+or use both queries at once:
+
+    >>> r4 = q.searchResults(subquery1 & subquery2)
+    >>> [(x.name, x.age) for x in r4]
+    [('Fred', 33)]
+
+which will give us, of course, the same result set as with subquery1.
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/datacenter.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/datacenter.txt	(revision 12988)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/datacenter.txt	(revision 12988)
@@ -0,0 +1,290 @@
+Ikoba Data Center
+****************
+
+The Ikoba data center cares for managing CSV files and importing then.
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+Creating a data center
+======================
+
+A data center can be created easily:
+
+    >>> from waeup.ikoba.datacenter import DataCenter
+    >>> mydatacenter = DataCenter()
+    >>> mydatacenter
+    <waeup.ikoba.datacenter.DataCenter object at 0x...>
+
+Each data center has a location in file system where files are stored:
+
+    >>> storagepath = mydatacenter.storage
+    >>> storagepath
+    '/tmp/tmp...'
+
+Beside other things it provides a location to put data of deleted
+items into:
+
+    >>> import os
+    >>> del_path = mydatacenter.deleted_path
+    >>> os.path.isdir(del_path)
+    True
+
+Overall it complies with the `IDataCenter` interface:
+
+    >>> from zope.interface import verify
+    >>> from waeup.ikoba.interfaces import IDataCenter
+    >>> verify.verifyObject(IDataCenter, DataCenter() )
+    True
+
+    >>> verify.verifyClass(IDataCenter, DataCenter)
+    True
+
+Managing the storage path
+-------------------------
+
+We can set another storage path:
+
+    >>> import os
+    >>> os.mkdir('newlocation')
+    >>> newpath = os.path.abspath('newlocation')
+    >>> mydatacenter.setStoragePath(newpath)
+    []
+
+The result here is a list of filenames, that could not be
+copied. Luckily, this list is empty.
+
+When we set a new storage path, we can tell to move all files in the
+old location to the new one. To see this feature in action, we first
+have to put a file into the old location:
+
+    >>> open(os.path.join(newpath, 'myfile.txt'), 'wb').write('hello')
+
+Now we can set a new location and the file will be copied:
+
+    >>> verynewpath = os.path.abspath('verynewlocation')
+    >>> os.mkdir(verynewpath)
+
+    >>> mydatacenter.setStoragePath(verynewpath, move=True)
+    []
+
+    >>> storagepath = mydatacenter.storage
+    >>> 'myfile.txt' in os.listdir(verynewpath)
+    True
+
+We remove the created file to have a clean testing environment for
+upcoming examples:
+
+    >>> os.unlink(os.path.join(storagepath, 'myfile.txt'))
+
+Uploading files
+===============
+
+We can get a list of files stored in that location:
+
+    >>> mydatacenter.getPendingFiles()
+    []
+
+Let's put some file in the storage:
+
+    >>> import os
+    >>> filepath = os.path.join(storagepath, 'data.csv')
+    >>> open(filepath, 'wb').write('Some Content\n')
+
+Now we can find a file:
+
+    >>> mydatacenter.getPendingFiles()
+    [<waeup.ikoba.datacenter.DataCenterFile object at 0x...>]
+
+As we can see, the actual file is wrapped by a convenience wrapper,
+that enables us to fetch some data about the file. The data returned
+is formatted in strings, so that it can easily be put into output
+pages:
+
+    >>> datafile = mydatacenter.getPendingFiles()[0]
+    >>> datafile.getSize()
+    '13 bytes'
+
+    >>> datafile.getDate() # Nearly current datetime...
+    '...'
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(newpath)
+    >>> shutil.rmtree(verynewpath)
+
+
+Distributing processed files
+============================
+
+When files were processed by a batch processor, we can put the
+resulting files into desired destinations.
+
+We recreate the datacenter root in case it is missing:
+
+    >>> import os
+    >>> dc_root = mydatacenter.storage
+    >>> fin_dir = os.path.join(dc_root, 'finished')
+    >>> unfin_dir = os.path.join(dc_root, 'unfinished')
+
+    >>> def recreate_dc_storage():
+    ...   if os.path.exists(dc_root):
+    ...     shutil.rmtree(dc_root)
+    ...   os.mkdir(dc_root)
+    ...   mydatacenter.setStoragePath(mydatacenter.storage)
+    >>> recreate_dc_storage()
+
+We define a function that creates a set of faked result files:
+
+    >>> import os
+    >>> import tempfile
+    >>> def create_fake_results(source_basename, create_pending=True):
+    ...   tmp_dir = tempfile.mkdtemp()
+    ...   src = os.path.join(dc_root, source_basename)
+    ...   pending_src = None
+    ...   if create_pending:
+    ...     pending_src = os.path.join(tmp_dir, 'mypendingsource.csv')
+    ...   finished_src = os.path.join(tmp_dir, 'myfinishedsource.csv')
+    ...   for path in (src, pending_src, finished_src):
+    ...     if path is not None:
+    ...       open(path, 'wb').write('blah')
+    ...   return tmp_dir, src, finished_src, pending_src
+
+Now we can create the set of result files, that typically come after a
+successful processing of a regular source:
+
+Now we can try to distribute those files. Let's start with a source
+file, that was processed successfully:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                            pending_src, mode='create')
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The created dir will be removed for us by the datacenter. This way we
+can assured, that less temporary dirs are left hanging around:
+
+    >>> os.path.exists(tmp_dir)
+    False
+
+The root dir is empty, while the original file and the file containing
+all processed data were moved to'finished/'.
+
+Now we restart, but this time we fake an erranous action:
+
+    >>> recreate_dc_storage()
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'mysource.create.pending.csv', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    ['mysource.csv']
+
+While the original source was moved to the 'unfinished' dir, the
+pending file went to the root and the set of already processed items
+are stored in finished/.
+
+We fake processing the pending file and assume that everything went
+well this time:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The result is the same as in the first case shown above.
+
+We restart again, but this time we fake several non-working imports in
+a row.
+
+We start with a faulty start-import:
+
+    >>> recreate_dc_storage()
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+We try to process the pending file, which fails again:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+We try to process the new pending file:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'mysource.create.pending.csv', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    ['mysource.csv']
+
+Finally, we process the pending file and everything works:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The root dir is empty (contains no input files) and only the files in
+finished-subdirectory remain.
+
+
+We can get a list of imported files stored in the finished subfolder:
+
+    >>> mydatacenter.getFinishedFiles()
+    [<waeup.ikoba.datacenter.DataCenterFile object at ...>]
+
+    >>> datafile = mydatacenter.getFinishedFiles()[0]
+    >>> datafile.getSize()
+    '2 bytes'
+
+    >>> datafile.getDate() # Nearly current datetime...
+    '...'
+
+
+Clean up:
+
+    >>> shutil.rmtree(verynewpath)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/permissions.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/permissions.txt	(revision 12988)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/permissions.txt	(revision 12988)
@@ -0,0 +1,109 @@
+Ikoba permissions and roles
+**************************
+
+Permissions and roles used in a Ikoba portal.
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+Convenience functions
+=====================
+
+:mod:`waeup.ikoba` offers some convenience functions to handle security
+roles.
+
+:func:`get_all_roles`
+---------------------
+
+Gives us all roles defined in Ikoba. We get tuples of
+kind
+
+  ``(<ROLE-NAME>, <ROLE>)``
+
+where ``<ROLE-NAME>`` is the name under which a role was registered
+with the ZCA (a string) and ``<ROLE>`` is the real role object.
+
+    >>> from waeup.ikoba.permissions import get_all_roles
+    >>> get_all_roles()
+    <generator object...at 0x...>
+
+    >>> sorted(list(get_all_roles()))
+    [(u'waeup.Customer', <waeup.ikoba.customers.permissions.CustomerRole object at 0x...]
+
+:func:`get_waeup_roles`
+-----------------------
+
+Gives us all roles, except the Ikoba specific roles. We can get a list
+with or without local roles:
+
+    >>> from waeup.ikoba.permissions import get_waeup_roles
+    >>> len(list(get_waeup_roles()))
+    18
+
+    >>> len(list(get_waeup_roles(also_local=True)))
+    22
+
+
+:func:`get_waeup_role_names`
+----------------------------
+
+We can get all role names defined in Ikoba (except 'local'
+roles that are meant not to be assigned globally):
+
+    >>> from waeup.ikoba.permissions import get_waeup_role_names
+    >>> list(get_waeup_role_names())
+    [u'waeup.Customer',
+    u'waeup.CustomerImpersonator',
+    u'waeup.CustomersManager',
+    u'waeup.CustomersOfficer',
+    u'waeup.DataCenterManager',
+    u'waeup.DocumentsManager',
+    u'waeup.DocumentsOfficer',
+    u'waeup.ExportManager',
+    u'waeup.ImportManager',
+    u'waeup.PaymentsManager',
+    u'waeup.PaymentsOfficer',
+    u'waeup.PortalManager',
+    u'waeup.ProductsManager',
+    u'waeup.ProductsOfficer',
+    u'waeup.ReportsManager',
+    u'waeup.UsersManager',
+    u'waeup.WorkflowManager',
+    u'waeup.xmlrpcusers1']
+
+:func:`get_users_with_local_roles`
+----------------------------------
+
+We can get all users and their roles for a certain context
+object. This even works for objects that cannot have local roles as
+they are not stored in the ZODB:
+
+    >>> from waeup.ikoba.permissions import get_users_with_local_roles
+    >>> mycontext = object()
+    >>> people_and_roles = get_users_with_local_roles(mycontext)
+    >>> people_and_roles
+    <generator object...at 0x...>
+
+In this case, the result is empty:
+
+    >>> people_and_roles = list(people_and_roles)
+    >>> people_and_roles
+    []
+
+:func:`get_users_with_role`
+---------------------------
+
+We can get all users with a specific role for a certain context
+object:
+
+    >>> from waeup.ikoba.permissions import get_users_with_role
+    >>> mycontext = object()
+    >>> people = get_users_with_role('waeup.portalManager', mycontext)
+    >>> people
+    <generator object...at 0x...>
+
+In this case, the result is empty:
+
+    >>> people = list(people)
+    >>> people
+    []
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/userscontainer.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/userscontainer.txt	(revision 12988)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/userscontainer.txt	(revision 12988)
@@ -0,0 +1,80 @@
+User container for the Ikoba
+****************************
+
+.. :doctest:
+.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
+
+Before we can start, we need some password managers available:
+
+    >>> from zope.app.authentication.placelesssetup import (
+    ...   PlacelessSetup)
+    >>> PlacelessSetup().setUp()
+
+We can create a user container which will hold the useraccounts for
+us:
+
+    >>> from waeup.ikoba.userscontainer import UsersContainer
+    >>> myusers = UsersContainer()
+
+We can add users, just by passing a name, a password, and (optionally)
+a title and a description:
+
+    >>> myusers.addUser('bob', 'bobssecret')
+
+Now, Bob is in the container:
+
+    >>> list(myusers)
+    [u'bob']
+
+We can get Bob's account:
+
+    >>> bob = myusers['bob']
+    >>> bob
+    <waeup.ikoba.authentication.Account object at 0x...>
+
+    >>> bob.name
+    'bob'
+
+    >>> bob.title
+    'bob'
+
+    >>> bob.description
+
+
+As we did not give a title, the name was taken instead
+for title (but not for description). The password, however, is stored encoded:
+
+    >>> bob.password
+    '{SSHA}...'
+
+We can export user accounts:
+
+    >>> from waeup.ikoba.userscontainer import UserExporter
+    >>> import os
+    >>> import tempfile
+    >>> workdir = tempfile.mkdtemp()
+    >>> outfile = os.path.join(workdir, 'myoutput.csv')
+    >>> exporter = UserExporter()
+    >>> site = {'users':myusers}
+    >>> exporter.export_all(site, outfile)
+    >>> result = open(outfile, 'rb').read()
+    >>> 'name,title,public_name,description,email,phone,roles,local_roles,password' in result
+    True
+    >>> '{SSHA}' in result
+    True
+
+We can delete users:
+
+    >>> myusers.delUser('alice')
+
+The container won't complain, although there is no ``alice`` stored
+yet. But we can really delete users:
+
+    >>> myusers.delUser('bob')
+    >>> list(myusers)
+    []
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(workdir)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/permissions.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/permissions.txt	(revision 12987)
+++ 	(revision )
@@ -1,109 +1,0 @@
-Ikoba permissions and roles
-**************************
-
-Permissions and roles used in a Ikoba portal.
-
-.. :doctest:
-.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
-
-Convenience functions
-=====================
-
-:mod:`waeup.ikoba` offers some convenience functions to handle security
-roles.
-
-:func:`get_all_roles`
----------------------
-
-Gives us all roles defined in Ikoba. We get tuples of
-kind
-
-  ``(<ROLE-NAME>, <ROLE>)``
-
-where ``<ROLE-NAME>`` is the name under which a role was registered
-with the ZCA (a string) and ``<ROLE>`` is the real role object.
-
-    >>> from waeup.ikoba.permissions import get_all_roles
-    >>> get_all_roles()
-    <generator object...at 0x...>
-
-    >>> sorted(list(get_all_roles()))
-    [(u'waeup.Customer', <waeup.ikoba.customers.permissions.CustomerRole object at 0x...]
-
-:func:`get_waeup_roles`
------------------------
-
-Gives us all roles, except the Ikoba specific roles. We can get a list
-with or without local roles:
-
-    >>> from waeup.ikoba.permissions import get_waeup_roles
-    >>> len(list(get_waeup_roles()))
-    18
-
-    >>> len(list(get_waeup_roles(also_local=True)))
-    22
-
-
-:func:`get_waeup_role_names`
-----------------------------
-
-We can get all role names defined in Ikoba (except 'local'
-roles that are meant not to be assigned globally):
-
-    >>> from waeup.ikoba.permissions import get_waeup_role_names
-    >>> list(get_waeup_role_names())
-    [u'waeup.Customer',
-    u'waeup.CustomerImpersonator',
-    u'waeup.CustomersManager',
-    u'waeup.CustomersOfficer',
-    u'waeup.DataCenterManager',
-    u'waeup.DocumentsManager',
-    u'waeup.DocumentsOfficer',
-    u'waeup.ExportManager',
-    u'waeup.ImportManager',
-    u'waeup.PaymentsManager',
-    u'waeup.PaymentsOfficer',
-    u'waeup.PortalManager',
-    u'waeup.ProductsManager',
-    u'waeup.ProductsOfficer',
-    u'waeup.ReportsManager',
-    u'waeup.UsersManager',
-    u'waeup.WorkflowManager',
-    u'waeup.xmlrpcusers1']
-
-:func:`get_users_with_local_roles`
-----------------------------------
-
-We can get all users and their roles for a certain context
-object. This even works for objects that cannot have local roles as
-they are not stored in the ZODB:
-
-    >>> from waeup.ikoba.permissions import get_users_with_local_roles
-    >>> mycontext = object()
-    >>> people_and_roles = get_users_with_local_roles(mycontext)
-    >>> people_and_roles
-    <generator object...at 0x...>
-
-In this case, the result is empty:
-
-    >>> people_and_roles = list(people_and_roles)
-    >>> people_and_roles
-    []
-
-:func:`get_users_with_role`
----------------------------
-
-We can get all users with a specific role for a certain context
-object:
-
-    >>> from waeup.ikoba.permissions import get_users_with_role
-    >>> mycontext = object()
-    >>> people = get_users_with_role('waeup.portalManager', mycontext)
-    >>> people
-    <generator object...at 0x...>
-
-In this case, the result is empty:
-
-    >>> people = list(people)
-    >>> people
-    []
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/userscontainer.txt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/userscontainer.txt	(revision 12987)
+++ 	(revision )
@@ -1,80 +1,0 @@
-User container for the Ikoba
-****************************
-
-.. :doctest:
-.. :layer: waeup.ikoba.testing.IkobaUnitTestLayer
-
-Before we can start, we need some password managers available:
-
-    >>> from zope.app.authentication.placelesssetup import (
-    ...   PlacelessSetup)
-    >>> PlacelessSetup().setUp()
-
-We can create a user container which will hold the useraccounts for
-us:
-
-    >>> from waeup.ikoba.userscontainer import UsersContainer
-    >>> myusers = UsersContainer()
-
-We can add users, just by passing a name, a password, and (optionally)
-a title and a description:
-
-    >>> myusers.addUser('bob', 'bobssecret')
-
-Now, Bob is in the container:
-
-    >>> list(myusers)
-    [u'bob']
-
-We can get Bob's account:
-
-    >>> bob = myusers['bob']
-    >>> bob
-    <waeup.ikoba.authentication.Account object at 0x...>
-
-    >>> bob.name
-    'bob'
-
-    >>> bob.title
-    'bob'
-
-    >>> bob.description
-
-
-As we did not give a title, the name was taken instead
-for title (but not for description). The password, however, is stored encoded:
-
-    >>> bob.password
-    '{SSHA}...'
-
-We can export user accounts:
-
-    >>> from waeup.ikoba.userscontainer import UserExporter
-    >>> import os
-    >>> import tempfile
-    >>> workdir = tempfile.mkdtemp()
-    >>> outfile = os.path.join(workdir, 'myoutput.csv')
-    >>> exporter = UserExporter()
-    >>> site = {'users':myusers}
-    >>> exporter.export_all(site, outfile)
-    >>> result = open(outfile, 'rb').read()
-    >>> 'name,title,public_name,description,email,phone,roles,local_roles,password' in result
-    True
-    >>> '{SSHA}' in result
-    True
-
-We can delete users:
-
-    >>> myusers.delUser('alice')
-
-The container won't complain, although there is no ``alice`` stored
-yet. But we can really delete users:
-
-    >>> myusers.delUser('bob')
-    >>> list(myusers)
-    []
-
-Clean up:
-
-    >>> import shutil
-    >>> shutil.rmtree(workdir)
