Index: in/waeup.ikoba/trunk/src/waeup/ikoba/browser/batchprocessing.txt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/batchprocessing.txt	(revision 12988)
+++ 	(revision )
@@ -1,293 +1,0 @@
-Batch processing via browser
-****************************
-
-Preliminaries:
-
-We define a function that looks up a form with several submit buttons
-for the one with a given value (this functionality is missing in
-zope.testbrowser):
-
-    >>> def lookup_submit_value(name, value, browser):
-    ...   """Find a button with a certain value."""
-    ...   for num in range(0, 100):
-    ...     try:
-    ...       button = browser.getControl(name=name, index=num)
-    ...       if button.value.endswith(value):
-    ...         return button
-    ...     except IndexError:
-    ...       break
-    ...   return None
-
-Create a site:
-
-    >>> from waeup.ikoba.app import Company
-    >>> getRootFolder()['app'] = Company()
-    >>> from zope.component.hooks import setSite
-    >>> setSite(getRootFolder()['app'])
-
-Create a datacenter storage path:
-
-    >>> import os
-    >>> import tempfile
-    >>> dc_path = tempfile.mkdtemp()
-
-Log in:
-
-    >>> from zope.testbrowser.testing import Browser
-    >>> browser = Browser()
-    >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
-    >>> browser.handleErrors = False
-
-Set datacenter path and deselect moving old data:
-
-    >>> browser.open('http://localhost/app')
-    >>> browser.getLink('Data Center').click()
-    >>> browser.getLink('Edit settings').click()
-    >>> browser.getControl(name='newpath').value = dc_path
-    >>> browser.getControl(name='move').value = False
-    >>> browser.getControl(name='save').click()
-
-Set non-usable datacenter path:
-
-    >>> browser.getLink('Edit settings').click()
-    >>> browser.getControl(name='newpath').value = '/'
-    >>> browser.getControl(name='save').click()
-    >>> 'Given storage path cannot be used' in browser.contents
-    True
-    >>> browser.getControl('Back to Data Center').click()
-
-
-Batch processing users
-======================
-
-    >>> browser.open('http://localhost/app/datacenter')
-
-Prepare a CSV file for users:
-
-    >>> open('users.csv', 'wb').write(
-    ... """name,title,public_name,email,phone,roles
-    ... uli,Uli Fouquet,Chief Developer,uli@abc.de,+49-234-567,[]
-    ... henrik,Henrik Bettermann,Admin,henrik@abc.de,+49-234-567,"['waeup.PortalManager', 'waeup.ImportManager']"
-    ... """)
-
-Upload the file:
-
-    >>> import cStringIO
-    >>> browser.getLink('Upload data').click()
-    >>> filecontents = cStringIO.StringIO(
-    ...   open('users.csv', 'rb').read())
-    >>> filewidget = browser.getControl(name='uploadfile:file')
-    >>> filewidget.add_file(filecontents, 'text/plain', 'users.csv')
-    >>> browser.getControl(name='SUBMIT').click()
-
-Step 1: start batch processing:
-
-    >>> browser.getLink('Process data').click()
-    >>> button = lookup_submit_value(
-    ...   'select', 'users_zope.mgr.csv', browser)
-    >>> button.click()
-
-Step 2: select a processor and mode:
-
-    >>> importerselect = browser.getControl(name='importer')
-    >>> importerselect.getControl('User Processor').selected = True
-    >>> modeselect = browser.getControl(name='mode')
-    >>> modeselect.getControl(value='create').selected = True
-    >>> browser.getControl('Proceed to step 3').click()
-
-Step 3: Fix headerlines
-
-We get informed that there are no problems with the current header:
-
-    >>> print browser.contents
-    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-    ...
-    Header fields OK
-    ...
-
-The submit button is enabled:
-
-    >>> browser.getControl('Perform import').disabled
-    False
-
-    >>> browser.getControl('Perform import').click()
-
-Step 4: See import results
-
-The import was successful:
-
-    >>> print browser.contents
-    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-    ...Successfully processed 2 rows...
-    ...Batch processing finished...
-    ...File:...users_zope.mgr.csv...
-
-User henrik has got the global roles:
-
-   >>> henrik = getRootFolder()['app']['users']['henrik']
-   >>> henrik.roles
-   ['waeup.ProductsOfficer', 'waeup.PortalManager', 'waeup.ImportManager']
-
-Pending files
-=============
-
-When an error occurs during an import, two files are generated: a CSV
-file with finished files and a CSV file with pending data. Both are
-stored in the appropriate subdirectories in datacenter. We try to
-create faculties, from which one already exists.
-
-Go to datacenter page:
-
-    >>> browser.open('http://localhost/app/datacenter')
-
-Prepare a CSV file for users:
-
-    >>> open('newusers.csv', 'wb').write(
-    ... """name,title,public_name,email,phone,roles
-    ... isouaba,Issoufou Abba Moussa,Chief Developer,isouaba@abc.de,+49-234-567,[]
-    ... henrik,Henrik Bettermann,Admin,henrik@abc.de,+49-234-567,"['waeup.PortalManager', 'waeup.ImportManager']"
-    ... anne,Anne Palina,,anne@abc.de,+49-234-567,"['waeup.Nonsense']"
-    ... """)
-
-Upload the file:
-
-    >>> import cStringIO
-    >>> browser.getLink('Upload data').click()
-    >>> filecontents = cStringIO.StringIO(
-    ...   open('newusers.csv', 'rb').read())
-    >>> filewidget = browser.getControl(name='uploadfile:file')
-    >>> filewidget.add_file(filecontents, 'text/plain', 'newusers.csv')
-    >>> browser.getControl(name='SUBMIT').click()
-
-Since we now have a user with waeup.ImportManager role, an email has been sent:
-    >>> print browser.contents
-    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-    ...
-    ...All import managers have been notified by email...
-    ...
-
-Step 1: start batch processing:
-
-    >>> browser.getLink('Process data').click()
-    >>> button = lookup_submit_value(
-    ...   'select', 'newusers_zope.mgr.csv', browser)
-    >>> button.click()
-
-Step 2: select a processor and mode:
-
-    >>> importerselect = browser.getControl(name='importer')
-    >>> importerselect.getControl('User Processor').selected = True
-    >>> modeselect = browser.getControl(name='mode')
-    >>> modeselect.getControl(value='create').selected = True
-    >>> browser.getControl('Proceed to step 3').click()
-
-Step 3: Fix headerlines
-
-As there should be no problem with the headers, we can immediately
-perfom the import:
-
-    >>> browser.getControl('Perform import').click()
-
-Two lines could not be imported:
-
-    >>> print browser.contents
-    <!DOCTYPE html PUBLIC...
-    ...
-    ...Processing of 2 rows failed...
-    ...Successfully processed 1 rows...
-    ...
-
-Now there are two files as a result in datacenter storage's root and
-``finished`` dirs:
-
-    >>> pending_file = dc_path + '/newusers_zope.mgr.create.pending.csv'
-    >>> print open(pending_file).read()
-    name,roles,title,public_name,phone,email,--ERRORS--
-    henrik,"['waeup.PortalManager', 'waeup.ImportManager']",Henrik Bettermann,Admin,+49-234-567,henrik@abc.de,This object already exists. Skipping.
-    anne,['waeup.Nonsense'],Anne Palina,<IGNORE>,+49-234-567,anne@abc.de,roles: invalid role
-
-    >>> finished_file = dc_path + '/finished/newusers_zope.mgr.create.finished.csv'
-    >>> print open(finished_file).read()
-    name,roles,title,public_name,phone,email
-    isouaba,<IGNORE>,Issoufou Abba Moussa,Chief Developer,+49-234-567,isouaba@abc.de
-
-The finished-file contains the dataset we could import, while the
-pending file contains the dataset that failed, appended by an error
-message.
-
-
-Fixing the pending file
------------------------
-
-We 'edit' the pending file by removing anne and replacing henrik by gbenga 
-and finish the import this way:
-
-    >>> open(dc_path + '/newusers_zope.mgr.create.pending.csv', 'wb').write(
-    ... """name,title,public_name,email,phone,roles
-    ... gbenga,Jason Bamidele,Chief Developer,gbenga@abc.de,+49-234-567,[]
-    ... """)
-
-Step 1: start batch processing:
-
-    >>> browser.open('http://localhost/app/datacenter')
-    >>> browser.getLink('Process data').click()
-    >>> button = lookup_submit_value(
-    ...   'select', 'newusers_zope.mgr.create.pending.csv', browser)
-    >>> button.click()
-
-Step 2: select a processor and mode:
-
-    >>> importerselect = browser.getControl(name='importer')
-    >>> importerselect.getControl('User Processor').selected = True
-    >>> modeselect = browser.getControl(name='mode')
-    >>> modeselect.getControl(value='create').selected = True
-    >>> browser.getControl('Proceed to step 3').click()
-
-Step 3/4: Fix headerlines and import:
-
-As there should be no problem with the headers, we can immediately
-perfom the import:
-
-    >>> browser.getControl('Perform import').click()
-
-This time everything should work:
-
-    >>> print browser.contents
-    <!DOCTYPE html PUBLIC...
-    ...
-    ...Successfully processed 1 rows...
-    ...
-
-    >>> sorted(os.listdir(dc_path))
-    ['deleted', 'finished', 'logs', 'unfinished']
-
-    >>> os.listdir(dc_path + '/unfinished')
-    []
-
-    >>> sorted(os.listdir(dc_path + '/finished'))
-    ['newusers_zope.mgr.create.finished.csv', 'newusers_zope.mgr.csv', 'users_zope.mgr.create.finished.csv', 'users_zope.mgr.csv']
-
-Processed (finished) Files
-==========================
-
-    >>> browser.open('http://localhost/app/datacenter/processed')
-    >>> 'download?filename=finished/users_zope.mgr.create.finished.csv' in browser.contents
-    True
-
-Log Files
-=========
-
-    >>> browser.open('http://localhost/app/datacenter/logs')
-    >>> 'datacenter.log' in browser.contents
-    True
-    >>> browser.getControl('Show', index=0).click()
-    >>> browser.getControl('Back', index=0).click()
-    >>> browser.getControl('Back to Data Center').click()
-    >>> 'Storage path:' in browser.contents
-    True
-
-
-Clean up:
-
-    >>> import shutil
-    >>> shutil.rmtree(dc_path)
Index: in/waeup.ikoba/trunk/src/waeup/ikoba/browser/breadcrumbs.txt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/breadcrumbs.txt	(revision 12988)
+++ 	(revision )
@@ -1,243 +1,0 @@
-Breadcrumbs
-***********
-
-We use an adapter approach to get a flexible system of creating and
-retrieving breadcrumbs for certain views.
-
-
-How to get existing breadcrumbs
-===============================
-
-We create a company to check the breadcrumb functionality.
-
-    >>> from waeup.ikoba.app import Company
-    >>> root = getRootFolder()
-    >>> root['app'] = Company()
-    >>> app = root['app']
-
-The quick way (if you have a viewname)
---------------------------------------
-
-We can get an ordered list of breadcrumbs for a given context and
-viewname:
-
-    >>> from waeup.ikoba.browser.breadcrumbs import getBreadcrumbList
-    >>> blist1 = getBreadcrumbList(app, 'index')
-    >>> blist1
-    [<waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb object at 0x...>]
-
-A slightly more extensive list for the datacenter:
-
-    >>> blist2 = getBreadcrumbList(app['datacenter'], 'index')
-    >>> from pprint import pprint
-    >>> pprint(blist2)
-    [<waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb object at 0x...>,
-     <waeup.ikoba.browser.breadcrumbs.AdministrationBreadcrumb object at 0x...>,
-     <waeup.ikoba.browser.breadcrumbs.DataCenterBreadcrumb object at 0x...>]
-
-We get a breadcrumb for company, administration area and data
-center in that order.
-
-Each breadcrumb provides a ``title``, ``context``, and ``viewname``
-attribute, so that views and page templates can easily create markup
-out of it:
-
-     >>> pprint([(x.title) for x in blist2])
-     [u'Home', u'Administration', u'Data Center']
-
-     >>> pprint([(x.context, x.viewname) for x in blist2])
-     [(<waeup.ikoba.app.Company object at 0x...>, 'index'),
-      (<waeup.ikoba.app.Company object at 0x...>, 'administration'),
-      (<waeup.ikoba.datacenter.DataCenter object at 0x...>, 'index')]
-
-The administration area breadcrumb might be a surprise, as there is no
-equivalent object in the ZODB. In fact the administration area is only
-a certain view (the 'administration' view) on the company object.
-
-We will show below, how you can define breadcrumbs this way.
-
-As a rule of thumb you should know, that every breadcrumb is not only
-attached for a certain kind of object, but for a tuple of context
-object and a viewname.
-
-This way we can make sure, that there are different breadcrumb lists
-generated for instance for the administration view of Company
-instances and the index view. While the first should look something
-like::
-
-  Home -> Administration
-
-we expect for the latter only::
-
-  Home
-
-
-Another quick way (if you have a complete view)
------------------------------------------------
-
-Another quick approach for use in viewlets, views and similar
-contexts, where you usually have a view available but not a viewname,
-is to use ``getBreadcrumbListForView``.
-
-We create a view (a page in this case, but pages are views) on the
-user administration index page:
-
-    >>> from zope.publisher.browser import TestRequest
-    >>> from zope.component import getMultiAdapter
-    >>> page = getMultiAdapter((app['users'], TestRequest()), name='index')
-
-Now we can get the breadcrumbs for this view:
-
-    >>> from waeup.ikoba.browser.breadcrumbs import getBreadcrumbListForView
-    >>> blist3 = getBreadcrumbListForView(page)
-    >>> [x.title for x in blist3]
-    [u'Home', u'Administration', u'Officers']
-
-..note:: This works only for views created with grokcore.component
-         (i.e. Grok Views, Pages, etc.) but not for simple Zope3
-         views, as the viewname can not easily be extracted from the
-         view for the latter ones.
-
-
-Type-safe retrieval of breadcrumbs for a view
----------------------------------------------
-
-If you want to make sure, that a view for which you want to get
-breadcrumbs is really a Grok view, then you can use the
-IBreadcrumbContainer adapter. This one adapts only grok views.
-
-The returned breadcrumb container supports iteration:
-
-    >>> from waeup.ikoba.browser.interfaces import IBreadcrumbContainer
-    >>> mybccontainer = IBreadcrumbContainer(page)
-    >>> [x.title for x in mybccontainer]
-    [u'Home', u'Administration', u'Officers']
-
-It is, however, not a real Python list but only an iterable
-container. You can also get the real list of breadcrumbs:
-
-    >>> pprint(mybccontainer.getList())
-    [<...breadcrumbs.CompanyBreadcrumb object at 0x...>,
-     <...breadcrumbs.AdministrationBreadcrumb object at 0x...>,
-     <...breadcrumbs.UsersContainerBreadcrumb object at 0x...>]
-
-
-
-The general details
--------------------
-
-Now we can get breadcrumbs for contexts and view names. For example a
-breadcrumb for the 'index' view of our Company object:
-
-    >>> from zope.component import getAdapter
-    >>> from waeup.ikoba.browser.interfaces import IBreadcrumb
-    >>> b1 = getAdapter(app, IBreadcrumb, 'index')
-    >>> b1
-    <waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb object at 0x...>
-
-Breadcrumb objects provide a title:
-
-    >>> b1.title
-    u'Home'
-
-a parent (None for the app object):
-
-    >>> b1.parent is None
-    True
-
-and a viewname:
-
-    >>> b1.viewname
-    'index'
-
-Using a context and a viewname, one can easily compute a complete URL
-in views, viewlets, etc.
-
-    >>> b2 = getAdapter(app['datacenter'], IBreadcrumb, 'index')
-    >>> b2
-    <waeup.ikoba.browser.breadcrumbs.DataCenterBreadcrumb object at 0x...>
-
-    >>> b2.title
-    u'Data Center'
-
-    >>> b2.viewname
-    'index'
-
-As the data center is not root of the hierarchy, it provides a
-non-trivial parent:
-
-    >>> b2.parent
-    (<waeup.ikoba.app.Company object at 0x...>, 'administration')
-
-This result denotes a new context object (the Company instance we
-created above) and a view name ('administration').
-
-Normally, the view name would be 'index', but as some administrative
-components should look to the user as if they were grouped in a common
-subarea of the root (the administration area), we can also set a
-different view than the default as target.
-
-When we like to receive the breadcrumb for this parent, we can do so
-as above:
-
-    >>> context, viewname = b2.parent
-    >>> b3 = getAdapter(context, IBreadcrumb, viewname)
-    >>> b3
-    <waeup.ikoba...breadcrumbs.AdministrationBreadcrumb object at 0x...>
-
-As you can see, we get an AdministrationBreadcrumb, although the
-context object, for which the breadcrumb was created is also the
-Company instance as above:
-
-    >>> b3.context is b1.context
-    True
-
-This administration area now has still a parent, the front-page:
-
-    >>> context, viewname = b3.parent
-    >>> context, viewname
-    (<waeup.ikoba.app.Company object at 0x...>, 'index')
-
-We create last breadcrumb:
-
-    >>> b4 = getAdapter(context, IBreadcrumb, viewname)
-
-This is finally the last piece, the breadcrumb which links to the
-front page. It has no parent:
-
-    >>> b4.parent is None
-    True
-
-
-How to create new breadcrumbs
-=============================
-
-Breadcrumbs are looked up as named adapters. This means, you must
-create a named adapter with the following specifications:
-
- * Derive your breadcrumb class from ``Breadcrumb`` class. This will
-   save you lots of work.
-
- * The adapter context (``grok.context``) must be the object type you
-   want to provide a breadcrumb for. So, if you want to write a
-   Breadcrumb adapter for objects providing ``IMyFunnyType``, write::
-
-     grok.context(IMyFunnyType)
-
- * The adapter name (``grok.name``) must be the same as the name of
-   the view, for which the breadcrumb component should be
-   registered. 
-
-   If you want to write a breadcrumb component for the 'index' view of
-   some content type and derive your class from Breadcrumb, then you
-   can skip this step as 'index' is the default name.
-
- * Set a ``title`` attribute to give the text that should be
-   displayed. Use something like::
-
-     @property
-     def title(self):
-       return self.some_computed_title()
-
-   if you want computed titles.
-
Index: in/waeup.ikoba/trunk/src/waeup/ikoba/browser/browser.txt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/browser.txt	(revision 12988)
+++ 	(revision )
@@ -1,368 +1,0 @@
-Browsing Ikoba
-*************
-
-Here we visit all parts of a Ikoba portal using a browser.
-
-Company
-==========
-
-We can watch universities in the browser.
-
-We create an company object and put into the ZODB root::
-
-  >>> root = getRootFolder()
-  >>> list(root)
-  []
-
-  >>> from waeup.ikoba.app import Company
-  >>> u = Company()
-  >>> root['mycompany'] = u
-  >>> list(root)
-  [u'mycompany']
-
-  >>> from zope.component.hooks import setSite
-  >>> setSite(root['mycompany'])
-
-To make sure, we can 'watch' pages, we first have to initialize out
-test browser::
-
-  >>> from zope.testbrowser.testing import Browser
-  >>> browser = Browser()
-
-Let's get the default view of a company::
-
-  >>> browser.open('http://localhost/mycompany')
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Welcome to WAeUP.Ikoba...
-  ...
-
-We can change to German::
-  >>> browser.getLink('de').click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Anfragen...
-  ...
-
-And then change back to English by clicking on the third link containing 'en'
-behind 'Anfragen' and 'Einloggen'::
-  >>> browser.getLink('en', index=2).click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Enquiries...
-  ...
-
-The contact form for anonymous users is called 'Enquiries'::
-
-  >>> browser.open('http://localhost/mycompany')
-  >>> browser.getLink('Enquiries').click()
-  >>> browser.getControl('Send').click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Required input is missing...
-  ...
-
-  >>> browser.getControl(name='form.fullname').value = "Bob Tester"
-  >>> browser.getControl(name='form.email_from').value = "xx@yy.zz"
-  >>> browser.getControl(name='form.body').value = u'test message'
-  >>> browser.getControl('Send').click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Your message has been sent...
-  ...
-
-Registered users with an email address can request a password change:
-
-  >>> root['mycompany']['users'].addUser('forgetful', 'secret',title='Bob Forgetful',
-  ...                               description='A forgetful user', email='aa@aa.ng')
-  >>> browser.open('http://localhost/mycompany/changepw')
-  >>> browser.getControl(name="form.email").value = 'aa@aa.ng'
-  >>> browser.getControl("Send login credentials").click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...An email with your user name and password has been sent to aa@aa.ng...
-
-Now we login as manager::
-
-  >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
-  >>> browser.handleErrors = False
-
-We can then get an edit view of the configuration container::
-
-  >>> browser.open('http://localhost/mycompany/configuration')
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...<form action="http://localhost/mycompany/configuration/@@index"
-  ...
-
-The edit form contains the default value for the company name::
-
-  >>> 'Sample Company' in browser.contents
-  True
-
-We can perform several actions on the edit form::
-
-  >>> browser.getControl("Save", index=0).click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Form has been saved...
-  ...
-
-  >>> browser.open('http://localhost/mycompany/configuration')
-  >>> browser.getControl("Update plugins").click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Plugins were updated. See log file for details...
-  ...
-
-The default frontpage HTML has been saved in a dictionary
-and is properly rendered on the frontpage of the portal:
-
-  >>> browser.open('http://localhost/mycompany')
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...<h1>Welcome to WAeUP.Ikoba...</h1>
-  ...
-
-The German part is really not being rendered:
-
-  >>> 'Willkommen' in browser.contents
-  False
-
-
-If we change to German so that the German part of frontpage.rst is rendered:
-
-  >>> browser.open('http://localhost/mycompany//@@change_language?lang=de')
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...<h1>Willkommen auf WAeUP.Ikoba...</h1>
-  ...
-
-The English part is really not being rendered:
-
-  >>> 'Welcome' in browser.contents
-  False
-
-Switch back to English:
-
-  >>> browser.open('http://localhost/mycompany//@@change_language?lang=en')
-
-
-Officers
-========
-
-  >>> browser.open('http://localhost/mycompany')
-  >>> browser.getLink('Officers').click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Officers...
-  ...
-
-We can add users:
-
-  >>> browser.getLink("Add user").click()
-  >>> browser.getControl(name="form.name").value = 'bob'
-  >>> browser.getControl(name="form.title").value = 'Bob The User'
-  >>> browser.getControl(name="password").value = 'secret'
-  >>> browser.getControl(name="control_password").value = 'secret'
-  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
-  >>> browser.getControl(name="form.phone.country").value = ['+234']
-  >>> browser.getControl(name="form.phone.area").value = '123'
-  >>> browser.getControl(name="form.phone.ext").value = '45678'
-  >>> browser.getControl("Add user").click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...<td>bob</td>
-  ...
-
-We can edit user bob:
-
-  >>> browser.getControl("Manage", index=0).click()
-  >>> browser.getControl("Save", index=0).click()
-  >>> browser.getControl("Cancel", index=0).click()
-
-We can add site roles which are then displayed on the user container page.
-Since the test browser does not use javascript, we have to add site roles
-manually by setting the roles attribute:
-
-Since JS is not supported by the test browser, the default role has been removed
-when clicking the Save button.
-
-  >>> print root['mycompany']['users']['bob'].roles
-  []
-  >>> root['mycompany']['users']['bob'].roles = ['waeup.UsersManager']
-  >>> print root['mycompany']['users']['bob'].roles
-  ['waeup.UsersManager']
-  >>> browser.open('http://localhost/mycompany/users')
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...<td nowrap>Users Manager <br /></td>
-  ...
-
-Users can't be added twice:
-
-  >>> browser.open('http://localhost/mycompany/users/add')
-  >>> browser.getControl(name="form.name").value = 'bob'
-  >>> browser.getControl(name="form.title").value = 'Bob The User'
-  >>> browser.getControl(name="password").value = 'secret'
-  >>> browser.getControl(name="control_password").value = 'secret'
-  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
-  >>> browser.getControl(name="form.phone.country").value = ['+234']
-  >>> browser.getControl(name="form.phone.area").value = '123'
-  >>> browser.getControl(name="form.phone.ext").value = '45678'
-  >>> browser.getControl("Add user").click()
-  >>> 'The userid chosen already exists' in browser.contents
-  True
-
-Users can be deleted:
-
-  >>> browser.open('http://localhost/mycompany/users')
-  >>> browser.getControl("Remove", index=0).click()
-  >>> 'User account bob successfully deleted' in browser.contents
-  True
-
-
-
-Contact Form
-============
-
-Let's enter the contact form::
-
-  >>> browser.open('http://localhost/mycompany/contactadmin')
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Contact
-  ...
-
-We fill the form  (this will send a real message to
-contact@waeup.org)::
-
-  >>> browser.open('http://localhost/mycompany/contactadmin')
-  >>> browser.getControl(name='form.body').value = "test message"
-  >>> browser.getControl('Send').click()
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
-  ...Your message has been sent...
-  ...
-
-If this test fails, chances are, that the local machine has no SMTP
-server installed.
-
-
-Data Center
-===========
-
-The data center helps us uploading files for later import or similar.
-
-  >>> browser.open('http://localhost/mycompany')
-  >>> browser.getLink('Data Center').click()
-
-Setting the file path
----------------------
-
-A datacenter stores files in a path in filesystem. By default this is
-a directory in the sources:
-
-  >>> print browser.contents
-  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-  ...
-  <b>Storage path:</b> <span>.../parts/test/datacenter</span>
-  ...
-
-Going to 'Settings` we can change the path:
-
-  >>> browser.getLink('Edit settings').click()
-  >>> pathsetting = browser.getControl(name='newpath')
-
-We create a directory and set it as our upload path:
-
-  >>> import os
-  >>> cwd = os.getcwd()
-  >>> uploadpath = os.path.join(cwd, 'testfiles')
-  >>> os.mkdir(uploadpath)
-  >>> pathsetting.value = uploadpath
-
-And submit the form:
-
-  >>> browser.getControl(name='save').click()
-
-We clean up the set directory path, as there might be some files be
-copied some files from installation:
-
-  >>> files = os.listdir(uploadpath)
-  >>> for filename in files:
-  ...   if os.path.isdir(os.path.join(uploadpath, filename)):
-  ...     continue
-  ...   os.unlink(os.path.join(uploadpath, filename))
-
-We also remove any existing 'accesscodes' subdir:
-
-  >>> import shutil
-  >>> for filename in files:
-  ...   if not os.path.isdir(os.path.join(uploadpath, filename)):
-  ...     continue
-  ...   if filename != 'accesscodes':
-  ...     continue
-  ...   shutil.rmtree(os.path.join(uploadpath, filename))
-
-The new upload directory is now empty, except from the logs and other
-standard dirs, which are created automatically:
-
-  >>> sorted(os.listdir(uploadpath))
-  ['deleted', 'finished', 'logs', 'unfinished']
-
-
-Uploading files
----------------
-
-Now we can upload files. Most interesting files might be CSV files,
-that can be imported lateron. We create a CSV file containing faculty
-descriptions:
-
-  >>> open('faculties.csv', 'wb').write(
-  ... """code,title,title_prefix
-  ... FA,Arts,faculty
-  ... FS,Sciences,faculty
-  ... """)
-
-Now we can upload this file. To do this, we first go to the upload
-page:
-
-  >>> browser.getLink('Upload data').click()
-
-and enter the appropriate data in the form:
-
-  >>> filewidget = browser.getControl(name='uploadfile:file')
-  >>> filewidget
-  <Control name='uploadfile:file' type='file'>
-
-A sidenote for developers: by marking the filewidget with the
-``:file`` extension, we tell Zope to handle this field as a file
-widget.
-
-  >>> import cStringIO
-  >>> filecontents = cStringIO.StringIO(
-  ...   open('faculties.csv', 'rb').read())
-  >>> filewidget.add_file(filecontents, 'text/plain', 'myfaculties.csv')
-
-  >>> browser.getControl(name='SUBMIT').click()
-
-The file was indeed uploaded, with the current userid inserted:
-
-  >>> sorted(os.listdir(uploadpath))
-  ['deleted', 'finished', 'logs', 'myfaculties_zope.mgr.csv', 'unfinished']
-
-
-
-Importing a CSV file
---------------------
-
-The import of CSV files is described in batchprocessing.txt.
-
-
-Clean up:
-
-  >>> import os
-  >>> import shutil
-  >>> shutil.rmtree(uploadpath)
-
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/batchprocessing.txt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/batchprocessing.txt	(revision 12989)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/batchprocessing.txt	(revision 12989)
@@ -0,0 +1,293 @@
+Batch processing via browser
+****************************
+
+Preliminaries:
+
+We define a function that looks up a form with several submit buttons
+for the one with a given value (this functionality is missing in
+zope.testbrowser):
+
+    >>> def lookup_submit_value(name, value, browser):
+    ...   """Find a button with a certain value."""
+    ...   for num in range(0, 100):
+    ...     try:
+    ...       button = browser.getControl(name=name, index=num)
+    ...       if button.value.endswith(value):
+    ...         return button
+    ...     except IndexError:
+    ...       break
+    ...   return None
+
+Create a site:
+
+    >>> from waeup.ikoba.app import Company
+    >>> getRootFolder()['app'] = Company()
+    >>> from zope.component.hooks import setSite
+    >>> setSite(getRootFolder()['app'])
+
+Create a datacenter storage path:
+
+    >>> import os
+    >>> import tempfile
+    >>> dc_path = tempfile.mkdtemp()
+
+Log in:
+
+    >>> from zope.testbrowser.testing import Browser
+    >>> browser = Browser()
+    >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+    >>> browser.handleErrors = False
+
+Set datacenter path and deselect moving old data:
+
+    >>> browser.open('http://localhost/app')
+    >>> browser.getLink('Data Center').click()
+    >>> browser.getLink('Edit settings').click()
+    >>> browser.getControl(name='newpath').value = dc_path
+    >>> browser.getControl(name='move').value = False
+    >>> browser.getControl(name='save').click()
+
+Set non-usable datacenter path:
+
+    >>> browser.getLink('Edit settings').click()
+    >>> browser.getControl(name='newpath').value = '/'
+    >>> browser.getControl(name='save').click()
+    >>> 'Given storage path cannot be used' in browser.contents
+    True
+    >>> browser.getControl('Back to Data Center').click()
+
+
+Batch processing users
+======================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for users:
+
+    >>> open('users.csv', 'wb').write(
+    ... """name,title,public_name,email,phone,roles
+    ... uli,Uli Fouquet,Chief Developer,uli@abc.de,+49-234-567,[]
+    ... henrik,Henrik Bettermann,Admin,henrik@abc.de,+49-234-567,"['waeup.PortalManager', 'waeup.ImportManager']"
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('users.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'users.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'users_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('User Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import').disabled
+    False
+
+    >>> browser.getControl('Perform import').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 2 rows...
+    ...Batch processing finished...
+    ...File:...users_zope.mgr.csv...
+
+User henrik has got the global roles:
+
+   >>> henrik = getRootFolder()['app']['users']['henrik']
+   >>> henrik.roles
+   ['waeup.ProductsOfficer', 'waeup.PortalManager', 'waeup.ImportManager']
+
+Pending files
+=============
+
+When an error occurs during an import, two files are generated: a CSV
+file with finished files and a CSV file with pending data. Both are
+stored in the appropriate subdirectories in datacenter. We try to
+create faculties, from which one already exists.
+
+Go to datacenter page:
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for users:
+
+    >>> open('newusers.csv', 'wb').write(
+    ... """name,title,public_name,email,phone,roles
+    ... isouaba,Issoufou Abba Moussa,Chief Developer,isouaba@abc.de,+49-234-567,[]
+    ... henrik,Henrik Bettermann,Admin,henrik@abc.de,+49-234-567,"['waeup.PortalManager', 'waeup.ImportManager']"
+    ... anne,Anne Palina,,anne@abc.de,+49-234-567,"['waeup.Nonsense']"
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('newusers.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'newusers.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Since we now have a user with waeup.ImportManager role, an email has been sent:
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    ...All import managers have been notified by email...
+    ...
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'newusers_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('User Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+As there should be no problem with the headers, we can immediately
+perfom the import:
+
+    >>> browser.getControl('Perform import').click()
+
+Two lines could not be imported:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC...
+    ...
+    ...Processing of 2 rows failed...
+    ...Successfully processed 1 rows...
+    ...
+
+Now there are two files as a result in datacenter storage's root and
+``finished`` dirs:
+
+    >>> pending_file = dc_path + '/newusers_zope.mgr.create.pending.csv'
+    >>> print open(pending_file).read()
+    name,roles,title,public_name,phone,email,--ERRORS--
+    henrik,"['waeup.PortalManager', 'waeup.ImportManager']",Henrik Bettermann,Admin,+49-234-567,henrik@abc.de,This object already exists. Skipping.
+    anne,['waeup.Nonsense'],Anne Palina,<IGNORE>,+49-234-567,anne@abc.de,roles: invalid role
+
+    >>> finished_file = dc_path + '/finished/newusers_zope.mgr.create.finished.csv'
+    >>> print open(finished_file).read()
+    name,roles,title,public_name,phone,email
+    isouaba,<IGNORE>,Issoufou Abba Moussa,Chief Developer,+49-234-567,isouaba@abc.de
+
+The finished-file contains the dataset we could import, while the
+pending file contains the dataset that failed, appended by an error
+message.
+
+
+Fixing the pending file
+-----------------------
+
+We 'edit' the pending file by removing anne and replacing henrik by gbenga 
+and finish the import this way:
+
+    >>> open(dc_path + '/newusers_zope.mgr.create.pending.csv', 'wb').write(
+    ... """name,title,public_name,email,phone,roles
+    ... gbenga,Jason Bamidele,Chief Developer,gbenga@abc.de,+49-234-567,[]
+    ... """)
+
+Step 1: start batch processing:
+
+    >>> browser.open('http://localhost/app/datacenter')
+    >>> browser.getLink('Process data').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'newusers_zope.mgr.create.pending.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('User Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3/4: Fix headerlines and import:
+
+As there should be no problem with the headers, we can immediately
+perfom the import:
+
+    >>> browser.getControl('Perform import').click()
+
+This time everything should work:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC...
+    ...
+    ...Successfully processed 1 rows...
+    ...
+
+    >>> sorted(os.listdir(dc_path))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> os.listdir(dc_path + '/unfinished')
+    []
+
+    >>> sorted(os.listdir(dc_path + '/finished'))
+    ['newusers_zope.mgr.create.finished.csv', 'newusers_zope.mgr.csv', 'users_zope.mgr.create.finished.csv', 'users_zope.mgr.csv']
+
+Processed (finished) Files
+==========================
+
+    >>> browser.open('http://localhost/app/datacenter/processed')
+    >>> 'download?filename=finished/users_zope.mgr.create.finished.csv' in browser.contents
+    True
+
+Log Files
+=========
+
+    >>> browser.open('http://localhost/app/datacenter/logs')
+    >>> 'datacenter.log' in browser.contents
+    True
+    >>> browser.getControl('Show', index=0).click()
+    >>> browser.getControl('Back', index=0).click()
+    >>> browser.getControl('Back to Data Center').click()
+    >>> 'Storage path:' in browser.contents
+    True
+
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(dc_path)
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/breadcrumbs.txt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/breadcrumbs.txt	(revision 12989)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/breadcrumbs.txt	(revision 12989)
@@ -0,0 +1,243 @@
+Breadcrumbs
+***********
+
+We use an adapter approach to get a flexible system of creating and
+retrieving breadcrumbs for certain views.
+
+
+How to get existing breadcrumbs
+===============================
+
+We create a company to check the breadcrumb functionality.
+
+    >>> from waeup.ikoba.app import Company
+    >>> root = getRootFolder()
+    >>> root['app'] = Company()
+    >>> app = root['app']
+
+The quick way (if you have a viewname)
+--------------------------------------
+
+We can get an ordered list of breadcrumbs for a given context and
+viewname:
+
+    >>> from waeup.ikoba.browser.breadcrumbs import getBreadcrumbList
+    >>> blist1 = getBreadcrumbList(app, 'index')
+    >>> blist1
+    [<waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb object at 0x...>]
+
+A slightly more extensive list for the datacenter:
+
+    >>> blist2 = getBreadcrumbList(app['datacenter'], 'index')
+    >>> from pprint import pprint
+    >>> pprint(blist2)
+    [<waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb object at 0x...>,
+     <waeup.ikoba.browser.breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <waeup.ikoba.browser.breadcrumbs.DataCenterBreadcrumb object at 0x...>]
+
+We get a breadcrumb for company, administration area and data
+center in that order.
+
+Each breadcrumb provides a ``title``, ``context``, and ``viewname``
+attribute, so that views and page templates can easily create markup
+out of it:
+
+     >>> pprint([(x.title) for x in blist2])
+     [u'Home', u'Administration', u'Data Center']
+
+     >>> pprint([(x.context, x.viewname) for x in blist2])
+     [(<waeup.ikoba.app.Company object at 0x...>, 'index'),
+      (<waeup.ikoba.app.Company object at 0x...>, 'administration'),
+      (<waeup.ikoba.datacenter.DataCenter object at 0x...>, 'index')]
+
+The administration area breadcrumb might be a surprise, as there is no
+equivalent object in the ZODB. In fact the administration area is only
+a certain view (the 'administration' view) on the company object.
+
+We will show below, how you can define breadcrumbs this way.
+
+As a rule of thumb you should know, that every breadcrumb is not only
+attached for a certain kind of object, but for a tuple of context
+object and a viewname.
+
+This way we can make sure, that there are different breadcrumb lists
+generated for instance for the administration view of Company
+instances and the index view. While the first should look something
+like::
+
+  Home -> Administration
+
+we expect for the latter only::
+
+  Home
+
+
+Another quick way (if you have a complete view)
+-----------------------------------------------
+
+Another quick approach for use in viewlets, views and similar
+contexts, where you usually have a view available but not a viewname,
+is to use ``getBreadcrumbListForView``.
+
+We create a view (a page in this case, but pages are views) on the
+user administration index page:
+
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.component import getMultiAdapter
+    >>> page = getMultiAdapter((app['users'], TestRequest()), name='index')
+
+Now we can get the breadcrumbs for this view:
+
+    >>> from waeup.ikoba.browser.breadcrumbs import getBreadcrumbListForView
+    >>> blist3 = getBreadcrumbListForView(page)
+    >>> [x.title for x in blist3]
+    [u'Home', u'Administration', u'Officers']
+
+..note:: This works only for views created with grokcore.component
+         (i.e. Grok Views, Pages, etc.) but not for simple Zope3
+         views, as the viewname can not easily be extracted from the
+         view for the latter ones.
+
+
+Type-safe retrieval of breadcrumbs for a view
+---------------------------------------------
+
+If you want to make sure, that a view for which you want to get
+breadcrumbs is really a Grok view, then you can use the
+IBreadcrumbContainer adapter. This one adapts only grok views.
+
+The returned breadcrumb container supports iteration:
+
+    >>> from waeup.ikoba.browser.interfaces import IBreadcrumbContainer
+    >>> mybccontainer = IBreadcrumbContainer(page)
+    >>> [x.title for x in mybccontainer]
+    [u'Home', u'Administration', u'Officers']
+
+It is, however, not a real Python list but only an iterable
+container. You can also get the real list of breadcrumbs:
+
+    >>> pprint(mybccontainer.getList())
+    [<...breadcrumbs.CompanyBreadcrumb object at 0x...>,
+     <...breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <...breadcrumbs.UsersContainerBreadcrumb object at 0x...>]
+
+
+
+The general details
+-------------------
+
+Now we can get breadcrumbs for contexts and view names. For example a
+breadcrumb for the 'index' view of our Company object:
+
+    >>> from zope.component import getAdapter
+    >>> from waeup.ikoba.browser.interfaces import IBreadcrumb
+    >>> b1 = getAdapter(app, IBreadcrumb, 'index')
+    >>> b1
+    <waeup.ikoba.browser.breadcrumbs.CompanyBreadcrumb object at 0x...>
+
+Breadcrumb objects provide a title:
+
+    >>> b1.title
+    u'Home'
+
+a parent (None for the app object):
+
+    >>> b1.parent is None
+    True
+
+and a viewname:
+
+    >>> b1.viewname
+    'index'
+
+Using a context and a viewname, one can easily compute a complete URL
+in views, viewlets, etc.
+
+    >>> b2 = getAdapter(app['datacenter'], IBreadcrumb, 'index')
+    >>> b2
+    <waeup.ikoba.browser.breadcrumbs.DataCenterBreadcrumb object at 0x...>
+
+    >>> b2.title
+    u'Data Center'
+
+    >>> b2.viewname
+    'index'
+
+As the data center is not root of the hierarchy, it provides a
+non-trivial parent:
+
+    >>> b2.parent
+    (<waeup.ikoba.app.Company object at 0x...>, 'administration')
+
+This result denotes a new context object (the Company instance we
+created above) and a view name ('administration').
+
+Normally, the view name would be 'index', but as some administrative
+components should look to the user as if they were grouped in a common
+subarea of the root (the administration area), we can also set a
+different view than the default as target.
+
+When we like to receive the breadcrumb for this parent, we can do so
+as above:
+
+    >>> context, viewname = b2.parent
+    >>> b3 = getAdapter(context, IBreadcrumb, viewname)
+    >>> b3
+    <waeup.ikoba...breadcrumbs.AdministrationBreadcrumb object at 0x...>
+
+As you can see, we get an AdministrationBreadcrumb, although the
+context object, for which the breadcrumb was created is also the
+Company instance as above:
+
+    >>> b3.context is b1.context
+    True
+
+This administration area now has still a parent, the front-page:
+
+    >>> context, viewname = b3.parent
+    >>> context, viewname
+    (<waeup.ikoba.app.Company object at 0x...>, 'index')
+
+We create last breadcrumb:
+
+    >>> b4 = getAdapter(context, IBreadcrumb, viewname)
+
+This is finally the last piece, the breadcrumb which links to the
+front page. It has no parent:
+
+    >>> b4.parent is None
+    True
+
+
+How to create new breadcrumbs
+=============================
+
+Breadcrumbs are looked up as named adapters. This means, you must
+create a named adapter with the following specifications:
+
+ * Derive your breadcrumb class from ``Breadcrumb`` class. This will
+   save you lots of work.
+
+ * The adapter context (``grok.context``) must be the object type you
+   want to provide a breadcrumb for. So, if you want to write a
+   Breadcrumb adapter for objects providing ``IMyFunnyType``, write::
+
+     grok.context(IMyFunnyType)
+
+ * The adapter name (``grok.name``) must be the same as the name of
+   the view, for which the breadcrumb component should be
+   registered. 
+
+   If you want to write a breadcrumb component for the 'index' view of
+   some content type and derive your class from Breadcrumb, then you
+   can skip this step as 'index' is the default name.
+
+ * Set a ``title`` attribute to give the text that should be
+   displayed. Use something like::
+
+     @property
+     def title(self):
+       return self.some_computed_title()
+
+   if you want computed titles.
+
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/browser.txt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/browser.txt	(revision 12989)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/doctests/browser.txt	(revision 12989)
@@ -0,0 +1,368 @@
+Browsing Ikoba
+*************
+
+Here we visit all parts of a Ikoba portal using a browser.
+
+Company
+==========
+
+We can watch universities in the browser.
+
+We create an company object and put into the ZODB root::
+
+  >>> root = getRootFolder()
+  >>> list(root)
+  []
+
+  >>> from waeup.ikoba.app import Company
+  >>> u = Company()
+  >>> root['mycompany'] = u
+  >>> list(root)
+  [u'mycompany']
+
+  >>> from zope.component.hooks import setSite
+  >>> setSite(root['mycompany'])
+
+To make sure, we can 'watch' pages, we first have to initialize out
+test browser::
+
+  >>> from zope.testbrowser.testing import Browser
+  >>> browser = Browser()
+
+Let's get the default view of a company::
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Welcome to WAeUP.Ikoba...
+  ...
+
+We can change to German::
+  >>> browser.getLink('de').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Anfragen...
+  ...
+
+And then change back to English by clicking on the third link containing 'en'
+behind 'Anfragen' and 'Einloggen'::
+  >>> browser.getLink('en', index=2).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Enquiries...
+  ...
+
+The contact form for anonymous users is called 'Enquiries'::
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> browser.getLink('Enquiries').click()
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Required input is missing...
+  ...
+
+  >>> browser.getControl(name='form.fullname').value = "Bob Tester"
+  >>> browser.getControl(name='form.email_from').value = "xx@yy.zz"
+  >>> browser.getControl(name='form.body').value = u'test message'
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Your message has been sent...
+  ...
+
+Registered users with an email address can request a password change:
+
+  >>> root['mycompany']['users'].addUser('forgetful', 'secret',title='Bob Forgetful',
+  ...                               description='A forgetful user', email='aa@aa.ng')
+  >>> browser.open('http://localhost/mycompany/changepw')
+  >>> browser.getControl(name="form.email").value = 'aa@aa.ng'
+  >>> browser.getControl("Send login credentials").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...An email with your user name and password has been sent to aa@aa.ng...
+
+Now we login as manager::
+
+  >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+  >>> browser.handleErrors = False
+
+We can then get an edit view of the configuration container::
+
+  >>> browser.open('http://localhost/mycompany/configuration')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<form action="http://localhost/mycompany/configuration/@@index"
+  ...
+
+The edit form contains the default value for the company name::
+
+  >>> 'Sample Company' in browser.contents
+  True
+
+We can perform several actions on the edit form::
+
+  >>> browser.getControl("Save", index=0).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Form has been saved...
+  ...
+
+  >>> browser.open('http://localhost/mycompany/configuration')
+  >>> browser.getControl("Update plugins").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Plugins were updated. See log file for details...
+  ...
+
+The default frontpage HTML has been saved in a dictionary
+and is properly rendered on the frontpage of the portal:
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h1>Welcome to WAeUP.Ikoba...</h1>
+  ...
+
+The German part is really not being rendered:
+
+  >>> 'Willkommen' in browser.contents
+  False
+
+
+If we change to German so that the German part of frontpage.rst is rendered:
+
+  >>> browser.open('http://localhost/mycompany//@@change_language?lang=de')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h1>Willkommen auf WAeUP.Ikoba...</h1>
+  ...
+
+The English part is really not being rendered:
+
+  >>> 'Welcome' in browser.contents
+  False
+
+Switch back to English:
+
+  >>> browser.open('http://localhost/mycompany//@@change_language?lang=en')
+
+
+Officers
+========
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> browser.getLink('Officers').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Officers...
+  ...
+
+We can add users:
+
+  >>> browser.getLink("Add user").click()
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.title").value = 'Bob The User'
+  >>> browser.getControl(name="password").value = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
+  >>> browser.getControl(name="form.phone.country").value = ['+234']
+  >>> browser.getControl(name="form.phone.area").value = '123'
+  >>> browser.getControl(name="form.phone.ext").value = '45678'
+  >>> browser.getControl("Add user").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>bob</td>
+  ...
+
+We can edit user bob:
+
+  >>> browser.getControl("Manage", index=0).click()
+  >>> browser.getControl("Save", index=0).click()
+  >>> browser.getControl("Cancel", index=0).click()
+
+We can add site roles which are then displayed on the user container page.
+Since the test browser does not use javascript, we have to add site roles
+manually by setting the roles attribute:
+
+Since JS is not supported by the test browser, the default role has been removed
+when clicking the Save button.
+
+  >>> print root['mycompany']['users']['bob'].roles
+  []
+  >>> root['mycompany']['users']['bob'].roles = ['waeup.UsersManager']
+  >>> print root['mycompany']['users']['bob'].roles
+  ['waeup.UsersManager']
+  >>> browser.open('http://localhost/mycompany/users')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td nowrap>Users Manager <br /></td>
+  ...
+
+Users can't be added twice:
+
+  >>> browser.open('http://localhost/mycompany/users/add')
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.title").value = 'Bob The User'
+  >>> browser.getControl(name="password").value = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
+  >>> browser.getControl(name="form.phone.country").value = ['+234']
+  >>> browser.getControl(name="form.phone.area").value = '123'
+  >>> browser.getControl(name="form.phone.ext").value = '45678'
+  >>> browser.getControl("Add user").click()
+  >>> 'The userid chosen already exists' in browser.contents
+  True
+
+Users can be deleted:
+
+  >>> browser.open('http://localhost/mycompany/users')
+  >>> browser.getControl("Remove", index=0).click()
+  >>> 'User account bob successfully deleted' in browser.contents
+  True
+
+
+
+Contact Form
+============
+
+Let's enter the contact form::
+
+  >>> browser.open('http://localhost/mycompany/contactadmin')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Contact
+  ...
+
+We fill the form  (this will send a real message to
+contact@waeup.org)::
+
+  >>> browser.open('http://localhost/mycompany/contactadmin')
+  >>> browser.getControl(name='form.body').value = "test message"
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Your message has been sent...
+  ...
+
+If this test fails, chances are, that the local machine has no SMTP
+server installed.
+
+
+Data Center
+===========
+
+The data center helps us uploading files for later import or similar.
+
+  >>> browser.open('http://localhost/mycompany')
+  >>> browser.getLink('Data Center').click()
+
+Setting the file path
+---------------------
+
+A datacenter stores files in a path in filesystem. By default this is
+a directory in the sources:
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  ...
+  <b>Storage path:</b> <span>.../parts/test/datacenter</span>
+  ...
+
+Going to 'Settings` we can change the path:
+
+  >>> browser.getLink('Edit settings').click()
+  >>> pathsetting = browser.getControl(name='newpath')
+
+We create a directory and set it as our upload path:
+
+  >>> import os
+  >>> cwd = os.getcwd()
+  >>> uploadpath = os.path.join(cwd, 'testfiles')
+  >>> os.mkdir(uploadpath)
+  >>> pathsetting.value = uploadpath
+
+And submit the form:
+
+  >>> browser.getControl(name='save').click()
+
+We clean up the set directory path, as there might be some files be
+copied some files from installation:
+
+  >>> files = os.listdir(uploadpath)
+  >>> for filename in files:
+  ...   if os.path.isdir(os.path.join(uploadpath, filename)):
+  ...     continue
+  ...   os.unlink(os.path.join(uploadpath, filename))
+
+We also remove any existing 'accesscodes' subdir:
+
+  >>> import shutil
+  >>> for filename in files:
+  ...   if not os.path.isdir(os.path.join(uploadpath, filename)):
+  ...     continue
+  ...   if filename != 'accesscodes':
+  ...     continue
+  ...   shutil.rmtree(os.path.join(uploadpath, filename))
+
+The new upload directory is now empty, except from the logs and other
+standard dirs, which are created automatically:
+
+  >>> sorted(os.listdir(uploadpath))
+  ['deleted', 'finished', 'logs', 'unfinished']
+
+
+Uploading files
+---------------
+
+Now we can upload files. Most interesting files might be CSV files,
+that can be imported lateron. We create a CSV file containing faculty
+descriptions:
+
+  >>> open('faculties.csv', 'wb').write(
+  ... """code,title,title_prefix
+  ... FA,Arts,faculty
+  ... FS,Sciences,faculty
+  ... """)
+
+Now we can upload this file. To do this, we first go to the upload
+page:
+
+  >>> browser.getLink('Upload data').click()
+
+and enter the appropriate data in the form:
+
+  >>> filewidget = browser.getControl(name='uploadfile:file')
+  >>> filewidget
+  <Control name='uploadfile:file' type='file'>
+
+A sidenote for developers: by marking the filewidget with the
+``:file`` extension, we tell Zope to handle this field as a file
+widget.
+
+  >>> import cStringIO
+  >>> filecontents = cStringIO.StringIO(
+  ...   open('faculties.csv', 'rb').read())
+  >>> filewidget.add_file(filecontents, 'text/plain', 'myfaculties.csv')
+
+  >>> browser.getControl(name='SUBMIT').click()
+
+The file was indeed uploaded, with the current userid inserted:
+
+  >>> sorted(os.listdir(uploadpath))
+  ['deleted', 'finished', 'logs', 'myfaculties_zope.mgr.csv', 'unfinished']
+
+
+
+Importing a CSV file
+--------------------
+
+The import of CSV files is described in batchprocessing.txt.
+
+
+Clean up:
+
+  >>> import os
+  >>> import shutil
+  >>> shutil.rmtree(uploadpath)
+
